深入JVM九:对象的回收判断和垃圾收集算法

如何判断对象可以回收

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。 那么判断对象是否死亡该如何判断,有引用计数法和可达性分析两种策略。

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1,当引用 失效,计数器就减1,任何时候计数器为0的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管 理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。所谓对象之间的相互引用问题,如下面代码所示:除了对象objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们,但是实际运行时,objA和objB均被回收:

/**
 * @Description: 引用计数法测试
 *                 -Xms20m -Xmx20m -Xmn10m -verbose:gc -XX:+PrintGCDetails
 * @Author: binga
 * @Date: 2020/9/9 20:31
 * @Blog: https://blog.csdn.net/pang5356
 */
public class ReferenceCountingTest {

    public static final int _1MB = 1024 * 1024;
    private byte[] m = new byte[2 * _1MB];

    ReferenceCountingTest obj;

    public static void main(String[] args) {
        ReferenceCountingTest objA = new ReferenceCountingTest();
        ReferenceCountingTest objB = new ReferenceCountingTest();

        objA.obj = objB;
        objB.obj = objA;

        objA = null;
        objB = null;

        System.gc();
    }
}

运行结果:

[GC (System.gc()) [PSYoungGen: 6117K->728K(9216K)] 6117K->736K(19456K), 0.0033286 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 728K->0K(9216K)] [ParOldGen: 8K->649K(10240K)] 736K->649K(19456K), [Metaspace: 3259K->3259K(1056768K)], 0.0117936 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 82K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 1% used [0x00000000ff600000,0x00000000ff614920,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 649K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 6% used [0x00000000fec00000,0x00000000feca2448,0x00000000ff600000)
 Metaspace       used 3266K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 357K, capacity 388K, committed 512K, reserved 1048576K

可以看到在Full GC是年轻代收集为0,并且老年代的占用649k,很明显objA和objB被回收了。

可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点, 从这些节点开始向下搜索,找到的对象都标记为非垃圾对象,其余未标记的对象 都是垃圾对象。
那么那些对象可以作为“GC Roots”呢?有以下几种对象可以作为“GC Roots”:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池中的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部引用,如基本数据类型对应的Class对象,一些常驻的异常对象(NullPointerException)等,还有系统类加载器。
  • 所有被同步锁(sychronized关键字)持有的对象。
  • 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

当然出去上述描述的以外,根据所选用的垃圾收集器及回收的区域不同还有其他的对象也可能充当“GC Roots”角色。就像使用分代收集算法收集年轻代,除去上述的“GC Roots”,老年代的对象也可以充当“GC Roots”,因为老年代的对象也可能对年轻代的对象所引用。

JVM中的常见引用类型

在JDK1.2之前,Java中的引用只有一种引用类型,也就是常见的赋值new,如下:

ReferenceCountingTest objA = new ReferenceCountingTest();

但是在JDK1.2之后扩展了引用类型,包括强引用、软引用、弱引用和虚引用。

  • 强引用(Strongly Reference):普通的变量引用。
  • 软引用(Soft Reference):将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象 回收掉。软引用可用来实现内存敏感的高速缓存。
    来看一下软引用的示例:
/**
 * @Description: 引用类型
 *               -Xms20m -Xmx20m -Xmn10m -verbose:gc -XX:+PrintGCDetails -XX:PretenureSizeThreshold=20971520 -XX:+UseConcMarkSweepGC
 * @Author: binga
 * @Date: 2020/9/9 21:00
 * @Blog: https://blog.csdn.net/pang5356
 */
public class ReferenceTypeTest {

    public static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        // 软引用
        SoftReference<User> user1 = new SoftReference<>(new User());
        SoftReference<User> user2 = new SoftReference<>(new User());
//        SoftReference<User> user3 = new SoftReference<>(new User());
//        SoftReference<User> user4 = new SoftReference<>(new User());
    }

    static class User {
        private byte[] m = new byte[4 * _1MB];
    }
}

首先运行结果如下:

[GC (Allocation Failure) [ParNew: 6281K->679K(9216K), 0.0042997 secs] 6281K->4777K(19456K), 0.0044023 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 par new generation   total 9216K, used 4830K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  50% used [0x00000000fec00000, 0x00000000ff00dbf8, 0x00000000ff400000)
  from space 1024K,  66% used [0x00000000ff500000, 0x00000000ff5a9c90, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 concurrent mark-sweep generation total 10240K, used 4098K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 3271K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K

可以看到老年代占用了大约是4MB,而年轻代的Eden占用一半,也就是4MB,这是因为在为user分配内存空间时,内存不够触发了Minor GC,当收集完user1在Survivor区放不下所有移到了老年代。紧接着将user3的注释放开,运行结果如下:

[GC (Allocation Failure) [ParNew: 6281K->674K(9216K), 0.0045242 secs] 6281K->4772K(19456K), 0.0046222 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 4770K->126K(9216K), 0.0059199 secs] 8868K->8975K(19456K), 0.0059642 secs] [Times: user=0.03 sys=0.02, real=0.01 secs] 
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8849K(10240K)] 13071K(19456K), 0.0002242 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-mark-start]
Heap
 par new generation   total 9216K, used 4277K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  50% used [0x00000000fec00000, 0x00000000ff00dbf8, 0x00000000ff400000)
[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
  from space 1024K,  12% used [0x00000000ff400000, 0x00000000ff41f960, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 concurrent mark-sweep generation total 10240K, used 8849K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 3269K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K

又触发了一下GC,这次GC是CMS对老年代的收集(原因是触发了老年代所剩空间小于年轻代对象总和),可以看到这次收集后,user1和user2均进入了老年代(老年代空间占用大约为8MB)。再次将user4分配的注释去掉,运行结果如下:

[GC (Allocation Failure) [ParNew: 6117K->667K(9216K), 0.0042561 secs] 6117K->4765K(19456K), 0.0043720 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 4763K->126K(9216K), 0.0052967 secs] 8861K->8975K(19456K), 0.0053473 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8849K(10240K)] 13071K(19456K), 0.0001923 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-mark-start]
[GC (Allocation Failure) [ParNew: 4222K->4222K(9216K), 0.0000242 secs][CMS[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 (concurrent mode failure): 8849K->8840K(10240K), 0.0066530 secs] 13071K->12936K(19456K), [Metaspace: 3262K->3262K(1056768K)], 0.0067548 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [CMS: 8840K->630K(10240K), 0.0038140 secs] 12936K->630K(19456K), [Metaspace: 3262K->3262K(1056768K)], 0.0038702 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
Heap
 par new generation   total 9216K, used 4260K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff0290f0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 concurrent mark-sweep generation total 10240K, used 630K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 3269K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K

此次经过了三次GC,但是此时Eden占用约4MB,这是为user4分配的,但是老年代只占用了630k,那么user1、user2和user3占用的12MB被回收了,这是因为GC完后内存不足进行了回收。

  • 弱引用(Weak Reference):将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差
    不多,GC会直接回收掉,很少用。示例如下:
/**
 * @Description: 弱引用测试
 *              -Xms20m -Xmx20m -Xmn10m -verbose:gc -XX:+PrintGCDetails -XX:PretenureSizeThreshold=20971520 -XX:+UseConcMarkSweepGC
 * @Author: binga
 * @Date: 2020/9/9 21:37
 * @Blog: https://blog.csdn.net/pang5356
 */
public class WeakReferenceTest {

    public static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        WeakReference<User> user1 = new WeakReference<>(new User());
        WeakReference<User> user2 = new WeakReference<>(new User());
    }

    static class User {
        private byte[] m = new byte[4 * _1MB];
    }
}

运行结果如下:

[GC (Allocation Failure) [ParNew: 6281K->676K(9216K), 0.0018594 secs] 6281K->676K(19456K), 0.0035887 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 par new generation   total 9216K, used 4827K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  50% used [0x00000000fec00000, 0x00000000ff00dbf8, 0x00000000ff400000)
  from space 1024K,  66% used [0x00000000ff500000, 0x00000000ff5a9370, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 concurrent mark-sweep generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 3270K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K

可以看到在为user2时Eden空间不足触发了Minor GC,在GC时直接将user1进行了回收。

  • 虚引用(Phantom Reference):虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎 不用

finalize最终判定对象是否存活

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们 暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。 标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

  1. 第一次标记并进行一次筛选。 筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖
    finalize方法,对象将直接被回收。
  2. 第二次标记 。如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最 后一次
    机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的 成员变量,那在第
    二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

其流程如下图所示:
在这里插入图片描述
接下来通过一个实例来深入了解一下finalize的机制:

/**
 * @Description: finalize方法测试
 * @Author: binga
 * @Date: 2020/9/10 11:21
 * @Blog: https://blog.csdn.net/pang5356
 */
public class FinalizeTest {

    public static  FinalizeTest SAVE_HOOK = null;

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeTest();
        SAVE_HOOK = null;
        // 触发GC
        System.gc();
        // 因为finalize被一个地优先级的线程执行,所以等待500ms
        Thread.sleep(500);

        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println(" i am died");
        }

        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);

        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println(" i am died");
        }
    }

    public void isAlive() {
        System.out.println("i am still alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        // 自救
        SAVE_HOOK = this;
    }
}

运行结果如下:

finalize method executed
i am still alive
 i am died

可以看到第一次在finalize执行时实现了自救,但是第二次则被回收了,这是因为finalize只执行一次,所第二次没有必要执行finalize方法了,所以无法实现自救从而被回收了。

如何判断一个类是无用类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢? 类需要同时满足下面3个条件才能算是 “无用的类” :

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何 实例。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

标记清除算法

算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完 成后统一回收所有被标记的对象。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题:

  1. 效率问题。标记和清除的两个过程效率都不高。这是因为当堆中存在大量需要回收的对象时需要进行大量的标记和清除动作,从而导致随着对象数量的增长从而收集效率越发的低。
  2. 空间问题。标记清除后会产生大量不连续的碎片,可能会导致大对象无法找到连续空间从而提早的触发GC。

在这里插入图片描述

标记复制算法

为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两 块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到 另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。这种算法针对内存空间中每次存活的少量对象时,效率是比较高的,因为只需要复制少量的对象。但是不幸的是该算法将内存空间一分为二,造成了内存空间的大量浪费。
在这里插入图片描述

标记整理算法

根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一 样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一段移动,然后直接清理掉边界以外的内存。

在这里插入图片描述

分代收集理论

分代收集理论并不是一种垃圾收集算法,而是根据JVM中对象的存活特性从而将堆内存进行分代从而实现局部内存回收的思想。其实基于以下几种假说:

  1. 弱分代假说:绝大多数对象都是朝生夕死的。
  2. 强分代假说:熬过多次垃圾收集过程的对象就难以消亡。

基于以上两种假说,很多垃圾收集器都是将堆内存划分为年轻代和老年代,新创建的对象都是在年轻代分配,当对象达到分代年龄(熬过多次垃圾收集过程)则晋升到老年代。相应的对年轻代和老年代分别进行局部垃圾收集。同时针对年轻代和老年代收集时分别使用不同的收集算法。但是局部内存垃圾回收就会引入一个问题,就是跨代引用。在列举“GC Roots”时出去罗列的情况还包括局部内存垃圾回收时跨代引用的对象。针对这种情况又相应的有一假说:

  1. 跨代引用假说:跨代引用相对于同代引用只是占少数。

这是因为相互引用的对象更倾向于“同生共死”。基于此,所以在处理跨代引用时,只需要在年轻代创建一个数据结构,而这个数据结构将老年代分为若干个小块内存,在这个数据结构中记录着老年代那块内存存在这对年轻代的引用,那么在对年轻代进行收集时,统计跨代引用扫描老年代时只需要访问这个特殊的数据结构从而找到指定的老年代区域进行扫描即可,而不用对老年代进行全量的扫描。而这种数据结构就是记忆集(Remembered Set)。
如图所示:
在这里插入图片描述
假设将老年代分为9小块,编号分别为0-8,而在年轻代有一个Remembered Set数据结构,其占用空间为9bit(假设),而其从高位到低位分别与老年代分的编号想对应,每一bit位的1代表老年代的小分区对年轻代有引用,而0则代表没有引用,那么在对年轻代进行收集时,只需要访问Remembered Set即可知道老年代的0、4、5、8区域对年轻代有引用则只需扫描这四个区域即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值