前言:要弄清Java虚拟机GC的整个过程,就得弄明白Java虚拟机用什么来进行GC?Java虚拟机在哪里GC?什么时候GC?GC什么?
GC(Garbage Collection)垃圾收集,JVM一个非常重要的功能。本文将围绕着JVM的GC这个动作展开,来过一遍GC的整个运作过程。
JVM是GC的发起者,准确说是VMThread是GC的发起者,那用什么来进行GC呢?很显然,用到的是垃圾收集器来进行GC的。而垃圾收集器,可以根据堆中的分代,分为不同类型的垃圾收集器。
新生代(Young generation)
老年代(Tenured generation)
G1是一个特殊的垃圾收集器,既可以作为新生代的垃圾收集器,也可以作为老年代的垃圾收集器。
更加详细的垃圾收集器的知识,可以阅读《深入理解JVM》这本书。
JVM的GC动作只在两个地方回收————堆和方法区。(JDK8的metaspace的GC,这里暂不讨论。如果还有其他内存区域发生垃圾回收,请指正)
在堆取进行GC
首先,得了解堆是什么,有什么作用? 言简意赅——总结Java内存区域和常量池
这里需要知道,JVM的GC采用了分代思想,所以堆被分成了新生代和老年代,而新生代又被细分为Eden区和From survivor和To survivor区。当类被JVM加载后,Java应用程序运行然后某个对象被new,这个对象就会被分配到堆中的新生代的Eden区(对象优先被分配到Eden区),如果Eden区域没有足够的内存来分配给该对象,就会触发minor GC来清除已经“死亡”的对象,这样才能将新清出的内存分配给该对象,经过GC都还存活的对象,会被移至From survivor区中。由于新生代采用的复制算法,Eden区存活对象和From survivor区的存活对象将被复制到To survivor区中。在To survivor区中的对象,每经过一次GC,对象中的“年龄计数器”就会加1,如果超过了晋升为老年代的年龄阈值时(默认为15)对象就会晋升到老年代中。
由于老年代里存放的都是大对象、存活时间较久的对象,因此老年代一般都是用标记-整理算法或标记-清除算法。当老年代对象没法再分配内存时,会触发一次Major GC(Full GC),用于回收那些已经“死亡”的对象。
下图为堆中分代
在方法区中进行GC
在堆中进行GC一般可以回收70%~95%的空间,相比在方法区中进行GC效率是非常低的。但是效率低不代表不进行GC。永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
在上文中,已经讲到了JVM什么时候进行GC。就是在Eden区没有足够内存分配给对象的时候进行Minor GC,在老年代没法再分配内存给大对象以及“老”对象的时候进行的Major GC(full GC)。这里谈一谈Minor GC和Major GC:
这里,首先得了解对象在什么情况下会被进行GC?
是“真正死亡”的对象吗?
那么,对象怎么才能被判定为“真正死亡”呢?JVM是通过可达性分析来进行分析的。可达性分析就是通过从GC Roots为起点,判断是否有一个引用链与被判断的对象相连,如果相连这代表这个对象还“活着”,是可用的。然而,一个对象被真正判定为“死亡”,需要进行两次标记(被标记为可回收对象),然后在下一次JVM进行GC的时候,才被真正的回收掉。如果一个对象没有与GC Roots的引用链相连接,也并没有被真正判定为“死亡”,而是进行第一次标记,然后在第二次标记之前会进行“自救”过程。所谓的“自救”就是在第二次被标记之前,需要重新与引用链相连接。在第一次被标记后,对象会进行筛选,筛选的条件为是否有必要进行执行finalize()方法。如果没有必要执行finalize()方法,则就等待第二次被标记;如果有必要执行finalize()方法,则会先将对象放置在一个叫F-Queue的队列中,然后等待虚拟机通过Finalizer线程去触发对象的finalize()方法,然后在finalize()方法里面,对象就开始自救的过程。对象自救可以通过把this赋值给某个类变量或者对象的成员变量,就实现了和引用链重新连接的目的——自救成功。因此在第二次标记的时候就会把对象从F-Queue队列中移除,然后虚拟机会在F-Queue中进行第二次小规模的标记,然后就等待下一次GC的来临。
所以,从上面分析的结果来看,JVM进行GC的对象,就是没有和引用链上相连的并且经过第一次标记,没有“自救”成功的对象。