前言:上一篇文章讲到了关于JVM的运行时数据区划分,大概阐述了JVM中各种类型的数据,内存是如何分配的。今天我将总结一下有关JVM垃圾回收的那些事,也是本人面试时经常被问到的话题。
目录结构:
-
如何判断对象还活着
-
垃圾收集算法
-
垃圾收集器
如何判断对象还活着
上一章已经提到,在堆中存放着Java中几乎所有的对象实例包括数组实例,垃圾收集器在回收垃圾之前,需要将对象的状态进行检查,那些对象“还活着”,哪些对象"已经死了".
引用计数法:
很多的教科书判断对象是否还活着,主要是给对象增加一个引用计数器,每当有一个对象引用该对象时,计数器值就加1,当引用失效时,计数器值就减1。任何时刻引用计数器值为0时,就是对象不会再被引用的情况。
但引用计数法有一个致命的缺陷,那就是无法解决对象之间的循环引用的问题。
例如:
1 public class Test{ 2 public Object instance=null; 3 private static final int_1MB=1024*1024; 4 private byte[] bigSize=new byte[2*int_1MB]; 5 6 public static void main(String args[]){ 7 Test objA=new Test(); 8 Test objB=new Test(); 9 10 objA.instance=objB; 11 objB.instance=objA; 12 objA=null; 13 objB=null; 14 System.gc(); 15 } 16 }
运行结果:
GC (System.gc()) [PSYoungGen: 6979K->872K(33280K)] 6979K->880K(110080K), 0.0022520 secs Full GC (System.gc()) [PSYoungGen: 872K->0K(33280K)] [ParOldGen: 8K->733K(76800K)] 880K->733K(110080K), [Metaspace: 3218K->3218K(1056768K)], 0.0091230 secs Heap PSYoungGen total 33280K, used 860K [0x00000000daf00000, 0x00000000dd400000, 0x0000000100000000) eden space 28672K, 3% used [0x00000000daf00000,0x00000000dafd7230,0x00000000dcb00000) from space 4608K, 0% used [0x00000000dcb00000,0x00000000dcb00000,0x00000000dcf80000) to space 4608K, 0% used [0x00000000dcf80000,0x00000000dcf80000,0x00000000dd400000) ParOldGen total 76800K, used 733K [0x0000000090c00000, 0x0000000095700000, 0x00000000daf00000) object space 76800K, 0% used [0x0000000090c00000,0x0000000090cb74c0,0x0000000095700000) Metaspace used 3232K, capacity 4496K, committed 4864K, reserved 1056768K class space used 352K, capacity 388K, committed 512K, reserved 1048576K
从后台运行结果可以看到:PSYoungGen: 872K->0K ,JVM并没有因为这两个对象互相引用就不回收了,这也从侧面说明JVM并不是通过引用计数法来判断对象是否存活。
可达性分析法
在高级程序设计语言(Java , C#)的实现中,都是通过可达性分析来判断对象是否还活着。可达性分析是指:从一系列被称为"GC root"的对象节点出发,从这些节点开始向下搜索,搜索所走过的路程被称为引用链;当一个对象到GC Root节点没有任何一条引用链可以相连时,则称这个对象时不可用的。
在Java语言中,可作为GC Root的对象包括以下几个:
-
虚拟机栈中引用的对象
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中JNI 引用的对象
即便是可达性分析算法中不可达的对象,也并非是“非死不可的”,这时候他们暂时处于“缓刑状态”,要真正宣告一个对象死亡,至少要经过两次标记而过程:如果对象在进行了可达性分析后没有与GC Root相链接的节点,那它将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已被虚拟机调用过,虚拟机将这两种情况视为"没有必要执行".
如果这个对象判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列里,并在稍候由虚拟机自动建立,低优先级的Finalizer线程去执行垃圾回收。
垃圾收集算法
标记-清理算法
算法为标记和清理两个阶段:首先标记出所有需要回收的对象,在标记完以后统一回收所有别标记的对象。
他的不足有两点:
1.效率低,标记和清理两个过程的效率都不高。
2.标记完并清理完后会形成大量不连续的内存碎片, 空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象是,无法找到足够大的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
为了解决标记清理算法的效率问题,一种称为"复制"的收集算法出现了,它可将内存安容量划分为大小相等的两块,每次只使用其中的一块,当这一块用完了,就将还存活的对象复制到另一块上去,然后再把已使用内存空间一次清理掉。虽然这种算法高效,运行简单;但它的代价是可使用内存缩小为原来的一半,利用率降低了。
现在商用的虚拟机都采用复制算法来回收新生代。
标记-整理算法
该算法的标记过程与之前讲的标记-清理算法一致,但后续清理过程不是直接对可回收对象进行清理,而是让虽有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
当前商用的虚拟机都采用的是分代收集算法,这种算法就是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特性采用最适合的收集算法。在新生代中,每次垃圾收集都发现有大批对象死亡,只有少数存活,那就选用复制算法,只需要付出少量的存活对象的复制成本就可以完成比较高效的垃圾收集。二老年代中因为对象存活率高,没有额外空间对他进行内存分配担保,就必须使用”标记-清理“或”标记-整理“算法来进行回收。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现;
Serial收集器
Serial收集器是最基本,发展最悠久的收集器,在JDK1.3以前是虚拟机新生代收集的唯一选择。这个收集器是一个单线程的收集器,但他的单线程的意义并不仅仅说明它只会使用一个CPU资源或一条收集线程去完成垃圾收集,它在工作的时候,必须停止其他所有工作的线程,美其名曰"stop the world",直到垃圾收集工作完成。
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余与Serial相关的可控参数、收集算法、回收策略都是完全一样的。
Parallel Scavenge收集器
该收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程的收集器。它跟其他的收集器的关注点不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的关注点是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗的时间的比值,Parallel Scavenge 收集器提供了两个参数用于控制吞吐量,分别是 -XX:MaxGCPauseMillis(最大垃圾手机停顿时间)和吞吐量大小 -XX:GCTimeRadio参数。
CMS收集器
CMS收集器是一种一获取最短停顿时间为目标的收集器。目前很大一部分Java应用集中在互联网站或B/S系统的服务器上,这类应用尤其重视服务器响应速度,CMS收集器就非常适合这种应用场景.
CMS收集器是基于"标记-清除"算法实现的,它的运作过程大致分为以下四步:
-
初始标记
-
并发标记
-
重新标记
-
并发清除
G1收集器
该收集器是当今收集器技术发展最前沿的成果之一,G1收集器是一款面向服务端应用的垃圾收集器。相比其他收集器,G1收集器具备以下优势:
-
并行和并发
-
分代收集
-
空间整合
-
可预测的停顿