JVM 垃圾回收原理
JVM垃圾回收从大体上可以分为两个阶段:
找出垃圾对象;
执行垃圾对象回收。
找出垃圾对象
垃圾回收要做到:
回收不再使用的对象,即垃圾对象;如果不能很好回收,则会造成内存泄漏;
不回收还在使用的对象,即非垃圾对象;如果错误回收,程序运行就会出错;
所以,如何找到垃圾对象就很关键了。
关于如何找出垃圾对象,讨论得比较多的有两个算法:
引用计数法
可达性分析算法
引用计数法
引用计数法,即记录每个对象被引用的次数;对象被引用时,引用次数+1, 引用失效时,引用-1。当对象的引用次数为0时,则该对象为垃圾对象,可被回收。
当有两个没有被真正使用了,但因为这两个对象相互相引用而导致他们的引用次数都不为0时,引用计数法就无法标识出他们是垃圾对象了。这会造成内存泄漏,所以这种方法是不可取的。
可达性分析算法
可达性分析,也称为根搜索,即从一批一定不会是垃圾,且在系统中处于第一级引用的对象开始,往下搜索出他们的引用链,这些引用链上的对象,都不是垃圾;非引用链上的对象,就可以判定为没有被引用了,可判定为垃圾。
那些一定不会是垃圾,且在系统中处于第一级引用的对象,也称为GC ROOTS。那GC ROOTS应该有哪些,才能做到不少,也不漏呢?
GC ROOTS有(注:这里列举的可能也不全,避免误人子弟,特别说明):
虚拟机栈中的对象引用;
方法区中类静态属性的引用;
方法区中常量对象的引用;
本地方法栈中JNI对象的引用;
执行垃圾回收
找出垃圾后,就是执行垃圾回收了。
早期的JVM GC过程,是全程停止用户线程,只执行GC线程进行垃圾回收的,也就是著名的STW,Stop The World。但后期JVM的优化,只需要在标识阶段停止用户线程,而在执行回收阶段GC线程可以与用户线程并行。
为什么在标记阶段一定要STW呢?那是因为采用的是垃圾标记算法是可达性分析法,把可达的对象识别出来做为非垃圾,其他的都归为垃圾。如果GC线程一边在标识,用户线程一边在创建新的引用,那GC线程就没有标识了。
对象一旦失去引用之后,就不可能再被引用了,即垃圾对象不可能再转为非垃圾对象,所以执行回收是可以与用户线程并行的。
垃圾回收主要发生在JVM的堆区。堆区比较大,全量执行标识及回对,耗时比较长,且这个时间内用户线程是停止的。那如何才能加快垃圾回收的时间,减少用户线程停止的时间呢?
垃圾分代回收
JVM堆区比较大,全量回收时间比较长。那可局部回收呢?
世界很奇妙,很多地方都可以看到“二八原则”的影子。那JVM里,是否80%的对象生命周期都很短(如方法体内的局部变量),且占用的内存空间又很少,20%都不到。
那基于上面的“二八原则”,可否在堆区划分一小块区域出来,专用于小的,创建频繁且生命周期较短的对象。然后针对这块区域,可以加大垃圾回收的频率;因为这块堆区较小,所以垃圾回收的时间也较短,基本不会对用户线程造成大的影响。
这就是垃圾分代回收的思想。按不同的对象的大小,及生命周期的长短特点,可以把JVM堆为分新生代和老年代。新生代的空间比较小,执行回收的频率也比较高,用于存放较小的对象,绝大部分的生命周期较短的对象都会在新生代被快速回收。老年代的空间比较大,用于存放大对象,或者生命周期较长的对象(如果在新生代中经历多次垃圾回收都存活下来,则是生命周期较长的对象,一般是全局引用的对象)。
新生代的对象画像:
新创建对象优先放新生代;
小的且生命周期较短的对象
老年代码的对象画像:
大的对象;
生命周期较长的对象;
新创建的对象,会优先放在新生代,如果是比较大的对象,则直接放老年代;新生代会频繁执行垃圾回收,如果有对象在新生代中经历多次垃圾回收都存活下来,则会转移到老年代。如果新生代和老年代都没有足够的空间可用,则会执行全量垃圾回收,即Full GC。
如果执行了Full GC,堆空间还是不够,怎么办? 那就是OOM了。
垃圾并行回收
垃圾回收的效率是JVM不断探讨和优化的点。
从最开始的单线程回收模型,到多线程回收,再到并发回收。这都是不断围绕着快速垃圾回收优化过程。
评论