本文主要针对JVM中的GC算法和实现做了一些说明,并将平时遇到或收藏的在大数据相关组件中出现的GC问题解决案例整理到这里,便于平时回顾学习。
一、需要管理和回收的内存
JVM中的“程序计数器”的内存随线程结束而回收,“虚拟机栈”和“本地方法栈”的内存随方法结束而回收,即类结构确定下来,在编译期内存分配也基本确定。因此,”堆内存“和”方法区(非堆内存、hotspot永久代)“是需要在运行期动态分配和垃圾收集器管理的。
1、堆内存:
python等语言采用“引用计数法”来确定堆内存中的对像是否被使用,计数为0则回收。该方法实现简单效率高,但不能解决java中对象间相互“循环引用”的问题,故引入“可达性分析算法”,任何对象和GC Roots之间必须存在一个“引用链”,否则该对象不可用。
无论哪种方式,关键还是在“引用”。JDK1.2+,引用分为强引用(不会回收)、软引用(SoftReference类,第二次gc时内存紧张会回收)、弱引用(WeakReference类,无论内存是否紧张下一次gc前都会回收)、虚引用(PhantomReference类,用于回收时接收系统通知)。
对象确定死亡前,需要经过“两次标记过程”。可达性分析确定对象和GC Roots之间没有引用链则进行第一次标记,并将需要执行finalize()方法的对象放入F-queue中,等待jvm的finalizer线程去执行,对F-queue中的对象进行第二次标记,仍然没有引用链则被真正回收。
2、方法区:这部分主要回收废弃常量和无用的类。
二、典型垃圾回收算法
1、Mark-Sweep(标记清除算法)
首先标记出所有需要回收的对象,然后统一回收所有被标记的对象。不足之处,标记和清除效率低下,其次产生大量内存碎片导致分配大对象时没有足够的连续空间而再次触发GC操作。
2、Copy(复制算法)
为了解决“Mark-Sweep”的缺陷,将可用内存分为两半,其中一块内存用完将存活的对象复制到另外一块内存依次存放,不产生碎片。不足之处,内存牺牲掉一半,其次存活对象较多导致复制的数据量大,效率低下。
3、Mark-Compact(标记整理算法)
为了解决“Copy”的缺陷,标记过程和Mark-Sweep的标记过程类似,回收阶段主要是将存活的对象都向一端移动,然后端边界以外的(一端最后一个存活的对象内存之后的部分)内存直接清除。
4、Generation-Collection(分代收集)
将java堆内存分为新生代和老年代,根据各个年代的特点采用适当的算法。如新生代对象“朝生夕灭”,故采用copy算法;老年代对象存活率高、没有格外空间对其分配担保,故采用Mark-Compact算法。
hotspot将新生代分为两个较小的survivor空间和一个较大的eden空间,每次只使用其中一个survivor和eden空间,默认比例为8:1。当上一次回收后存活的对象,在另一个survivor空间放不下的话,老年代可以进行分配担保(Handle promotion),使得这些存活对象进入老年代。
三、hotspot垃圾收集器
jdk7u14+的hotspot虚拟机中的收集器,下图表示了不同分代的收集器可以搭配使用的关系。(备注:并发:垃圾收集和用户线程同时工作;并行:多个垃圾收集,用户线程等待)
1、Serial/Serial old收集器
Serial负责新生代(复制算法),Serialold负责老年代(标记整理算法)。两者都是单线程方式进行垃圾回收,需要暂停所有用户线程(Stop the world)。Serial是client模式下的默认新生代收集器,简单高效,单cpu下没有线程交互开销,收集效率高。Serial Old收集器可与ParallelScavenge搭配使用,同时可作为CMS在并发收集发生Concurrent Mode Failure时使用的后备方案。
2、ParNew/CMS收集器
ParNew收集器是Serial的多线程版本,其他控制参数和Serial一样。ParNew是Server模式下的首选新生代收集器,可与CMS(并发收集,垃圾收集和用户线程基本同时工作)搭配使用。ParNew在单cpu下,效果没有Serial好。Cpu数量添加,默认开启收集线程数和cpu数量相同。
CMS是一种获取最短回收提顿时间为目标的收集器,侧重服务的响应速度,也被称为并发低停顿收集器。但是存在三个方面的不足:
(1)CMS默认启动回收线程数为(cpu数量+3)/4,cpu越多效果越好,但cpu不足4个时,一半运算能力分到执行收集器线程,导致用户线程执行速度降低;
(2)CMS处理浮动垃圾。CMS在并发清理时用户线程同时产生垃圾,这些垃圾(浮动垃圾)需等到下次GC再清理。因此,CMS触发时机(-XX:CMSInitiatingOccupancyFraction,触发百分比)不能等到老年代空间用得太满,否则CMS运行需要内存,再加上同时产生的浮动垃圾,会出现”Concurrent Mode Failure”,之后会临时启用Serial Old,stop theworld时间就很长了。
(3)CMS毕竟是标记清除算法实现,会产生空间碎片。通过-XX:UseCMSCompactAtFullCollection开启内存碎片整理(默认开启),无法并发但停顿时间变长,通过-XX:CMSFullGCsBeforeCompaction=0(默认为0),开启每次进入FullGC时都进行碎片整理。
3、Parallel Scavenge/Parallel Old收集器
Parallel Scavenge负责新生代收集,收集器也经常被称为“吞吐量优先”收集器,适合后台运算而不需要太多交互的任务,-XX:MaxGCPauseMillis设置停顿时间和-XX:GCTimeRatio设置吞吐量大小。而CMS是尽可能缩短stop the world的时候,适合与用户交互的任务。
Parallel Scavenge从复制算法和并行方面和ParNew类似,主要区别是ParallelScavenge具有自适应调节策略,使用-XX:+UseAdaptiveSizePolicy,无需手工指定-Xmn、-XX:SurvivorRatio、-XX:PretenureSizeThreshold等细节参数。
Parallel Old是ParallelScavenge的老年代版本,使用多线程和标记整理算法。
该搭配组合适合注重吞吐量和CPU资源敏感的场合。
4、G1收集器
G1是一款面向服务端应用的垃圾收集器。具有并行并发、分代收集、空间整合以及可预测停顿的特点。将整个Java堆分为多个大小相等的Region(底层还保留新生代和老年代的概念,不再物理隔离),统一进行管理。优先回收价值最大的Region,使得在有限时间内尽可能提高收集效率(Garbage First)。
四、大数据领域案例收集
1、 HBaseRegionServer异常退出问题分析(苏宁云商-shufflefamily),对CMS的一次调优;
2、 HBase最佳实践之CMS GC调优(HBASE技术社区)
3、持续中…
五、参考资料
1、《深入理解java虚拟机-jvm高级特性与最佳实践》,周志明;
2、Java垃圾回收机制,博客园-海子;
3、JVM垃圾收集器参数说明,博客园-LZ;
4、JVM垃圾收集器多维度对比,博客园-LZ。