关于JVM那点事 -- 对象判断

近期又看了看《深入理解JVM》书籍,觉得之前看过许多资料与书本上面的东西还是有一定的区别,书本里面还是有点东西的。于是赶紧来和小伙伴们分享分享~

如何判断对象可以回收

既然说的是垃圾回收那些,肯定首先要去判定哪些是可以回收的,这里就有大家们常说的引用计数法以及可达性分析法

引用计数法

引用计数法 - > 如果一个对象被其他地方引用到一次,计数器的值就会+1。反之,当引用的地方失效的时候,计数器的值就会减去-1。当计数器的值为0的时候,则代表着这个对象没有地方引用到。同时,相信大家也知道它的弊端,那就是不能完美的解决循环依赖的问题。

Demo:

package com.montos.test.first;

public class TestReferenceCounting {
   public Object instance = null;
   private byte[] bigSize= new byte[2*1024*1024];
   public static void main(String[] args) {
       TestReferenceCounting objA = new TestReferenceCounting();
       TestReferenceCounting objB = new TestReferenceCounting();
       objA.instance = objB;
       objB.instance = objA;
       objA = null;
       objB =null;
       // objA 与 objB 是否会被回收️?
       System.gc();
   }
}
复制代码

上面就看看当程序运行之后,对象objA与对象objB会不会进行回收? gc.log

0.148: [GC (System.gc()) 0.149: [SoftReference, 0 refs, 0.0000372 secs]0.149: [WeakReference, 12 refs, 0.0000104 secs]0.149: [FinalReference, 74 refs, 0.0000356 secs]0.149: [PhantomReference, 0 refs, 0 refs, 0.0000104 secs]0.149: [JNI Weak Reference, 0.0000129 secs][PSYoungGen: 6717K->560K(76288K)] 6717K->568K(251392K), 0.0022097 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
0.150: [Full GC (System.gc()) 0.151: [SoftReference, 0 refs, 0.0001038 secs]0.152: [WeakReference, 4 refs, 0.0000127 secs]0.152: [FinalReference, 0 refs, 0.0000085 secs]0.152: [PhantomReference, 0 refs, 0 refs, 0.0000086 secs]0.152: [JNI Weak Reference, 0.0000103 secs][PSYoungGen: 560K->0K(76288K)] [ParOldGen: 8K->371K(175104K)] 568K->371K(251392K), [Metaspace: 2964K->2964K(1056768K)], 0.0065876 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
0.157: Total time for which application threads were stopped: 0.0092869 seconds, Stopping threads took: 0.0000445 seconds
Heap
PSYoungGen      total 76288K, used 1966K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)
 eden space 65536K, 3% used [0x000000076ab00000,0x000000076aceb9e0,0x000000076eb00000)
 from space 10752K, 0% used [0x000000076eb00000,0x000000076eb00000,0x000000076f580000)
 to   space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
ParOldGen       total 175104K, used 371K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
 object space 175104K, 0% used [0x00000006c0000000,0x00000006c005ce08,0x00000006cab00000)
Metaspace       used 2983K, capacity 4496K, committed 4864K, reserved 1056768K
 class space    used 327K, capacity 388K, committed 512K, reserved 1048576K
复制代码

从上面的日志可以看到,此程序是执行了两次gc过程,且都是由System.gc()进行触发。第一次gc触发了年轻代的回收情况。通过6717K->568K可以看出jvm并没有因为两者互相引用而不进行回收(我们这里每个对象里面还包含着一个2M大小的byte数组)。也说明了当前的虚拟机内并不是通过引用计数器来判断对象是否存活的。

可能有些小伙伴们注意到,我们这里只是生成了两个对象objA以及objB。那么堆内存的回收应该是在4M左右的内存回收开始,为什么会在6M内存开始进行回收? ===> 我这里是执行的是main函数(静态方法),而不是运行在容器内部的一个Bean。所以当运行起来的时候,就已经在堆上生成了一个主类的实例对象。

可达性分析算法

可达性分析算法 - > 通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径被称为引用链,当一个对象到"GC Roots"没有任何引用链相连,则证明此对象是不可用的,则可以进行回收操作。如下图:

上图有包含root节点的则代表底下的对象都是继续存活的,而右边的子树上面的对象则被判断是可回收的。

在虚拟机栈中能作为GC Roots的对象包括:

  • 虚拟机(栈帧中的本地变量表)栈中引用的对象
  • 方法去中类静态属性引用的对象
  • 方法去中常量引用的对象
  • 本地方法栈中JNI(一般说的Native方法)引用的对象

引用之谈

无论是通过上面两种的回收算法,判定对象是否存活都与“引用”有关。JDK1.2之前,java中的引用定义为:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。那么按照这种定义方式,对象则只有被引用以及没被引用两种状态表达。JDK1.2之后则对其进行了扩充:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)以及虚引用(Phantom Reference)。

  • 强引用:类似“Object obj = new Object()” 这类的引用,只要强引用还存在,垃圾收集器则永远不会回收掉被引用的对象。
  • 软引用:描述一些还有用但非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。(JDK1.2之后,提供了SoftReference类来实现软引用)。
  • 弱引用:描述非必须对象的,但是强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。(JDK1.2之后,提供了WeakReference类来实现弱引用)。
  • 虚引用:是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。(JDK1.2之后,提供了PhantomReference类来实现虚引用)。

对象的生存还是死亡

在上面介绍的可达性分析算法中不可达的对象,也不一定“非死不可”,他们暂时处于一种“缓刑”阶段,要真正的宣布一个对象死亡,至少要经过两次标记过程:

  1. 对象进行可达性分析后发现没有与GC Roots相连接的引用链,则进行第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖此方法,或者此方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
  2. 如果该对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中。稍后虚拟机会自动建立的、低优先级的Finalizer线程去执行(只是会去触发这个方法,并不等待运行结束,防止有执行缓慢或者发生死循环的情况而导致整个回收系统崩溃)它。避免回收只要在第二次标记时对象重新与引用链上的任何一个对象建立关联即可。

Demo:

public class FinalizesxEscapeGC {
    public static FinalizesxEscapeGC save = null;

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


    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize executed");
        save = this; // 为什么需要用到类的静态变量?
    }

    public static void main(String[] args) throws InterruptedException {
        save = new FinalizesxEscapeGC();

        // 对象第一次拯救自己
        save = null;
        System.gc();
        Thread.sleep(500);

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

        // 第二次拯救失败
        save = null;
        System.gc();
        Thread.sleep(500);

        if (save != null) {
            save.isAlive();
        } else {
            System.out.println("i am dead");
        }
    }
}
复制代码

结果输出:

finalize executed
i am alive
i am dead
复制代码

从上面的Demo中可以看出,对象save逃过了第一次垃圾回收,而在第二次的时候被回收了。因为任何一个对象的finalize()方法都只会被系统自动调用一次,不会在进行第二次调用。

回收方法区

很多人认为方法区(永久代)是没有垃圾收集的,Java虚拟机规范中确实也说过可以不要求虚拟机在此区域中实现垃圾收集,并且在此区域中进行垃圾收集的性价比并不是很高。在此区域中垃圾收集主要有两部分:废弃常量以及无用的类

  • 废弃常量
    当常量池中一个常量没有被任何地方引用,如果这时候发生内存回收,而且必要的话,这个常量就会被系统清理出常量池。

  • 无用的类

    • 该类的所有实例都已经被回收。
    • 加载该类的ClassLoader已经被回收。
    • 该类对应的java.lang.Class对象没有在任何地方被引用。

    满足上面三个条件的无用类才可以进行回收。这里说的仅仅是"可以",而不是和对象一样,不使用了就必然会回收。

这篇只是简单的介绍JVM里面判断对象生存还是死亡的问题,同时里面介绍了两种回收算法以及判断对象是否生存的问题。下一篇结合下垃圾回收算法再来详细说明。

摘录自:《深入理解Java虚拟机》

猜你喜欢

转载自juejin.im/post/5e81ed3df265da480a1a9ecf