关于gc中对象回收算法的认识

  之前学习java时,笔者看到很多学习资料说,gc判断object存活与否的算法是:给对象添加一个引用计数器,每当有一处地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,当对象计数清零时,对象就会被gc回收。但等笔者开始学习jvm虚拟机后,才明白实际上gc并不是用这种算法实现的,理由如下:

package gc;

public class ReferenceCountingGC {
    public Object instance = null;
    private static final int occupy_1MB=1024*1024;
    private byte[] bigSize = new byte[4*occupy_1MB];

    public static void testGC(){
        ReferenceCountingGC obj1=new ReferenceCountingGC();
        ReferenceCountingGC obj2=new ReferenceCountingGC();
        obj1.instance=obj2;
        obj2.instance=obj1;

        obj1=null;
        obj2=null;

        System.gc();
    }

    public static void main(String [] args){
        ReferenceCountingGC.testGC();
    }
}
[GC (System.gc()) [PSYoungGen: 10854K->480K(38400K)] 10854K->488K(125952K), 0.0014556 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 480K->0K(38400K)] [ParOldGen: 8K->402K(87552K)] 488K->402K(125952K), [Metaspace: 3072K->3072K(1056768K)], 0.0125161 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 38400K, used 333K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
  eden space 33280K, 1% used [0x0000000795580000,0x00000007955d34a8,0x0000000797600000)
  from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
  to   space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
 ParOldGen       total 87552K, used 402K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
  object space 87552K, 0% used [0x0000000740000000,0x00000007400648c8,0x0000000745580000)
 Metaspace       used 3079K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 339K, capacity 388K, committed 512K, reserved 1048576K

可以看到,obj1和obj2互相引用,但他们还是被回收机制给回收了,可见gc底层并不是用引用计数算法实现的。

 

  下面介绍gc底层实现所采用的算法,可达性分析算法(Reachability Analysis),通过一系列被称为“GC Roots”的对象节点作为起始点,当GC root无法到达一个对象时,就可以判断此对象不可用,从而判断此对象可以被回收。

  GC Roots对象包括下面几种:

  1.虚拟机栈(栈帧中的本地变量表)中引用的对象。

  2.方法区中类静态属性引用的对象。

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

  4.本地方法栈中JNI(即一般说的Native方法)引用的对象。

  在判断对象可以被回收后,待回收的对象并不会立刻被gc回收掉,而是会进入一个名为F-queue的队列,在这里,对象有最后一次拯救自己的机会(当然也可能没有,这不好说,后面会解释),虚拟机会自动建立一个FInalize线程(此线程优先级很低),线程会调用finaliz()方法去执行F-queue中所有的对象,如果对象成功与引用链建立上联系,那么就可以免于死亡的命运。下面我写一段示意代码,用this关键字将finaliz()方法引用到对象中,使其建立联系。

package gc;

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOCK=null;

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

    @Override
    protected void finalize() throws Throwable{
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOCK=this;
    }

    public static void main(String [] args)throws Throwable{
        SAVE_HOCK=new FinalizeEscapeGC();

        SAVE_HOCK=null;
        System.gc();
        //因为Finalize线程的优先级很低,因此笔者将线程挂起1s,等待Finalize线程的启动。
        Thread.sleep(1000);
        if (SAVE_HOCK!=null){
            SAVE_HOCK.isAlive();
        }else{
            System.out.println("i am dead");
        }

        SAVE_HOCK=null;
        System.gc();
        Thread.sleep(1000);
        if (SAVE_HOCK!=null){
            SAVE_HOCK.isAlive();
        }else{
            System.out.println("i am dead");
        }
    }
}
finalize method executed!
I am alive XD
i am dead

  可以看到,我们第一次手动启动gc后,对象依旧存活,那为什么一样的代码,第二次对象就死亡了呢?这是因为同一个对象只会执行一次finalize()方法,当第二次面临gc时,就会直接被回收掉,这是从优化内存效率的角度而作出的设计。

  虽然上面我们成功拯救了对象,但在实际情况下,我们不建议这么做。首先,有太多办法阻止对象被回收了,比如合理的使用try{};catch{},根本不需要在回收阶段再去解决这个问题。其次,Finalize线程的优先级很低,在示例代码中,我们是将线程挂起了1s来等待Finalize线程被开启,在实际开发中,这几乎不可能实现,会严重的影响系统的运行效率,再者,gc并不是一定要执行finalize()方法,因为如果有一个方法执行起来很慢,或者直接阻塞了线程,那岂不是会有内存泄露的隐患。因此,gc并没有保证一定会等待finaliz()执行完再去回收对象。

 

  在笔者看过的很多技术书籍中,都认为方法区(Method Are,永生代(permanent generation)概念在JDK1.8里已被Oracle移除)是没有gc机制的。但事实上,这么说是错误的。在Java虚拟机规范中,明确了虚拟机可以不在方法区内运行gc。这是因为在方法区内运行gc的效率很低,因为在jvm的新生代(Young generation)中,执行一次gc至少可以回收70~90的内存,大多数对象都是朝生夕死的,而方法区的回收效率远远低于堆的回收效率。因此,从效率考虑,一般不会在方法区内运行gc。

  但这并不意味着我们不能在方法区内回收对象,事实上,方法区中的废弃常量和无用的类还是有必要回收的(比如常见的String字符串池问题),尤其是频繁自定义类加载器的场景下,虚拟机必须要具有类卸载的功能,以保证方法区的内存不会溢出。判断常量是否废弃很简单,而判断对象是否无用的条件就有些许苛刻了,必须要同时满足三个条件:

  1.该类必须没有存活着的实例,也就是说,所有该类的实例都被gc从堆上回收了。

  2.加载该类的类加载器(ClassLoader)已经被回收了。

  3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  当类被判断为无用类后,虚拟机就可以对类执行回收了。不过可以并不意味着虚拟机一定要这么去做,事实上,HotSpot虚拟机提供了-Xnoclassgc参数来让用户控制是否回收,也可以用-verbose:class以及-XX+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose.class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,而-XX:+TraceClassUnLoading需要FastDebug版的虚拟机去支持。

 

 

  在JDK1.8里,永生代的已经被Oracle公司永久的删除了,取而代之的是类元数据(Class  MetaData),在本地的内存中进行分配,并显式管理元数据的空间。

       从OS请求空间,然后分成块(piece);

       类加载器从它的块中分配元数据的空间(一个块被绑定到一个特定的类加载器);

       当类没有被加载到ClassLoader时,它的块就被回收,内存空间就会被释放出来以供再次使用;

       元数据使用由mmap分配的空间,而不是由malloc分配的空间;

  并且Hotspot虚拟机在JDK1.8中提供给用户两个新的参数来管理内存,这两个参数是:"-XX:maxMetaspaceSize:指定类元数据区的最大内存大小","-XX:MetaspaceSize:指定类元数据区的内存阈值--超过将触发垃圾回收机制"。

 

 

 

 

  感谢周志明先生所著的深入理解Java虚拟机,笔者深受周志明先生的启发,故写下本文总结一下jvm中gc的知识。

 

posted @ 2019-05-23 11:40  最好是风梳烟沐  阅读(423)  评论(0编辑  收藏  举报