JVM(二)垃圾回收器与内存分配策略

上一节内容回顾 :JVM(一)内存区域与内存溢出异常

1. 垃圾回收策略

回收 针对的是线程共享内存(堆,方法区)

判断对象是否存活?

a. 引用计数法(Python,C++)

  • 给每个对象附加一个引用计数器,每当有引用指向当前对象时,计数器 +1;每当有引用不再指向当前对象时,计数器 -1
  • 任意时刻,引用计数器值为0的对象,被标记为 “不在存活”
  • 缺点 :无法解决循环引用问题(我中有你,你中有我)

b. 可达性分析算法(Java,C#)

核心思想:

  • 通过一系列称为GC Roots对象开始向下搜索,若到指定对象有路可走(即"可达"),认为此对象存活;
  • 若从任意一个GC Roots对象到目标对象均不可达,认为目标对象已经不再存活.

在这里插入图片描述

在Java语言中,可作为GC Roots的对象包含下面几种 :

  1. 虚拟机栈中的临时变量引用的对象

    扫描二维码关注公众号,回复: 8898172 查看本文章
  2. 本地方法栈中JNI(Native方法)引用的对象

  3. 方法区中类静态变量引用的对象

  4. 方法区中常量引用的对象

2. 引用的扩充

JDK 1.2之后关于引用的扩充:强软弱虚,引用强度依次扩充

强引用 :

程序中普遍存在的,GC Roots对象所指向的引用都属于强引用

Person per = new Person();

只要当前对象被任意一个强引用指向,即便内存不够用也不能回收此对象

软引用 :

有用但不必须的对象(缓存对象)

  • 当前内存够用时,不回收对象;

  • 当前内存不够用时,回收对象.

JDK 1.2之后用SoftReference来表示软引用

   Person per = new Person(); //必须先创建强引用对象
   SoftReference<Person> softReference = new SoftReference<>(per);
   per = null;
   System.gc;

弱引用 :

描述非必须对象,强度若于软引用

  • 被弱引用指向的对象,只能存活到下次GC之前;
  • 当GC开始时,不管内存是否够用,都会回收被弱引用指向的对象

JDK 1.2之后用WeakReference来表示弱引用

Person per = new Person(); //必须先创建强引用对象
WeakReference<Person> weakReference = new WeakReference<>(per);
per = null;//该对象指向为空,可以被回收
System.gc;

虚引用 :

最弱的引用关系,完全对对象的生存周期不造成影响,并且无法通过虚引用取得对象

为对象设置虚引用的目的 :该对象在被GC回收之前由系统发回 回收通知

JDK 1.2之后用PhantomReference来表示虚引用

3. 对象的自我拯救

当一个对象到GC Root不可达时,并不是"当场去世",而是进行自我拯救

finalize() 对象的一次自我拯救机会

JVM在进行GC之前,需要判断回收对象所在的类 是否覆写了finalize()

  • 若没覆写,此对象直接回收
  • 若覆写了finalize()
    • finalize() 未被JVM调用,JVM则会调用finalize() ;对象在此调用过程中与GC Roots有路可达,此对象不再被回收(自救成功)
    • finalize() 已被JVM调用,此对象被回收
      在这里插入图片描述
package iqqcode.Study.GCRoots;

import java.util.concurrent.TimeUnit;

/**
 * @Author: Mr.Q
 * @Date: 2019-09-05 19:37
 * @Description:finalize()对象的自我拯救
 *
 */

public class FinalizeTest {
    private static FinalizeTest test;

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize method execute!");
        test = this; //test等于当前调用finalize()的对象
    }

    public static void main(String[] args) throws InterruptedException {
        test = new FinalizeTest();
        test = null; //此时test对象没有被强引用指向,无引用指向
        //JVM检查 是否覆写且调用过finalize()
        //对象在此调用过程中与`GC Roots`有路可走,此对象不再被回收
        System.gc();
        TimeUnit.SECONDS.sleep(1);
        if(test == null) {
            System.out.println("Now I'm dead...");
        }else {
            System.out.println("I'm Alive!");
        }

        test = null;//此时无引用指向,可以被垃圾回收
        //JVM检查 覆写且调用过finalize()
        //不再调用finalize(),判断对象死亡
        //finalize() 对象的一次自我拯救机会
        System.gc();
        TimeUnit.SECONDS.sleep(1);
        if(test == null) {
            System.out.println("Now I'm dead...");
        }else {
            System.out.println("I'm Alive!");
        }
        //TODO
    }
}

在这里插入图片描述

4. 方法区的回收

方法区回收(永久代回收)------废弃常量和无用类

方法区的GC频率非常低

5. 垃圾回收算法(堆上)

I. 标记清除算法

算法分为"标记"和"清除"两个阶段 :

  • 首先标记(根据可达性分析算法来标记)出所有需要回收的对象
  • 在标记完成后统一回收所有被标记的对象

在这里插入图片描述

标记-清除算法的不足主要有两个 :

  1. 效率问题 : 标记和清除这两个过程的效率都不高
  2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,导致gc频繁发生

II. 复制算法(新生代回收算法)

(所有对象和数组对象)

  • 新生代: 对象默认在此区域产生,大部分对象都在此区域,该区域的特点是==“朝生夕死”== (存活率低)
  • 老年代: 存活率高
  • "复制"算法是为了解决"标记-清理"的效率问题.
  • 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象 复制 到另一块上面,然后再把已经使用过的内存区域一次清理掉

在这里插入图片描述
这样做虽然提高了回收效率,但是降低了内存的利用率(100%的内区域变成了50%)

新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间

而是将内存(新生代内存)分为一块较大的 Eden(伊甸区) 空间和两块较小的 Survivor(幸存区) 空间,每次使用 Eden 和其中一块Survivor(Survivor区域一个称为From区,另一个称为To区域)

画图说明

HotSpot 实现的复制算法流程如下:

  1. Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区,然后清理掉Eden区的所有空间. 当Eden区即将满时,再次触发Minor gc,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将EdenFrom区域清空
  2. 当后续Eden又发生Minor gc的时候,会对EdenTo区域进行垃圾回收. 存活的对象复制到From区域,并将EdenTo区清空. 之后依次循环
  3. 部分对象会在FromTo区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

III. 标记-整理算法(老年代回收算法)

核心思想相较于标记清除算法------整理阶段先让存活对象向一端移动,而后清理掉端边界以外的内存

Question : 为何老年代不采用复制算法?

Answer :新生代中的绝大多数对象都是朝生夕死,所以每次复制存活的对象较少;而老年代中大部分对象都存活,反复复制反而效率低下

IV. 分代收集策略(JavaGC)

将堆空间分为新生代(-Xmn)与老年代(堆的大小 - 新生代)空间,其中新生代采用复制算法,老年代采用标记整理算法

面试题 : 请问了解Minor GC和Full GC么,这两种GC有什么不一样吗?

  1. Minor GC 又称为新生代 GC : 指的是发生在新生代的垃圾收集. 因为Java对象大多都具备朝生夕灭的特性,因此 Minor GC (采用复制算法)非常频繁,一般回收速度也比较快.
  2. Full GC 又称为 老年代 GC 或者 Major GC : 指发生在老年代的垃圾收集. 出现了 Major GC,经常会伴随至少一次的 Minor GC(并非绝对,在 Parallel Scavenge 收集器中就有直接进行 Full GC 的策略选择过程). Major GC 的速度一般会比 Minor GC 慢10倍以上.
发布了77 篇原创文章 · 获赞 118 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_43232955/article/details/100997167