PS : 本文乃学习整理参考而来 ,目录参考 [ Jvm系列目录 ]
垃圾收集器
在HotSpot中垃圾收集器分类:
1.Serial
2.ParNew
3.Parallel Scavenge
4.Serial Old (PS MarkSweep)
5.Parallel Old
6.CMS(Concurrent Mark Sweep)
7.G1(Gabage-First)
下图是HotSpot中垃圾收集器搭配使用方式:有连线说明可以搭配。[引用图片原文]
1. Serial收集器
是一个单线程的收集器,他在进行垃圾收集时,必须暂停其他的所有的工作线程,直到收集结束。“Stop The World”这项工作是有虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户的所有工作线程停掉,这对很多应用来说是难以接受的。Serial是虚拟机运行于Client模式下的新生代首选方式。
2. ParNew收集器
其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余包括Serial收集器可用的所有控制参数、收集算法、Stop The World、回收策略等都与Serial收集器完全一样。ParNew收集器在单CPU的环境中绝对不会比Serial收集器效果更好。他是许多运行在Server模式下的虚拟机的新生代的首选收集器。但HotSpot默认组合是PS/PS MS。
名词解释:后面的涉及到并发和并行收集器,这里解释在垃圾回收器中的解释。
并行(Parallel):多条垃圾收集线程并行工作,但此时用户线程处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行。
3. Parallel Scavenge收集器
也是使用复制算法的新生代收集器,也是并行的多线程收集器。该收集器关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总耗时时间的比值,及吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行100分钟,其中垃圾收集花掉了1分钟那吞吐量就是99%。停顿时间越短就约适合需要与用户交互的程序,而高吞吐量则可以高效率的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。不过不要认为把这个值设置得够小就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的;系统把新生代调小一些,收集时间变短,但也导致垃圾收集发生更频繁,原本10S收集一次,一次100MS,现在5S收集一次,每次70毫秒。停顿时间的确下降,但吞吐量也降下来了。由于与吞吐量关系密切,Parallel Scavenge收集器还有一个参数-XX:UseAdaptiveSizePolicy(自适应调节),这是一个开关参数,打开这个开关后,不需要手动设置新生带大小及比例、晋升老年代对象的年龄等,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式成为GC自适应的调节策略。自适应调节策略也是PS与ParNew收集器的一个重要区别。
4. Serial Old(PS MarkSweep)收集器
注:Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,并非直接使用了Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现非常接近,所以在官方的许多资料中都是直接以Serial Old代替PS MarkSweep进行讲解。
该收集器是Serial的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义在于给Client模式下的虚拟机使用。如果在Server模式下,那他有两大用途:
1.在JDK1.5以前与PS收集器搭配使用,至今Jvm虚拟机默认的老年代收集器。
2.作为CMS收集器后备预案,在并发收集发生Concurrent Mode Failure时使用
5. Parallel Old 收集器
该收集器是PS收集器的老年代版本,使用多线程和“标记-整理”算法。该收集器在JDK1.6中才开始提供。在此以前,新生代的PS收集器在老年代只能与Serial Old搭配(为什么不能搭CMS?)。由于SO是单线程收集方式,导致PS+SO的吞吐量不一定强过ParNew+CMS。 直到Parallel Old 收集器出现后,“吞吐量优先”收集器有了名副其实的组合。在注重吞吐量及CPU资源敏感的场合,都优先考虑PS + PO。
6 . CMS(Concurrent Mark Sweep) 收集器
特点 :并发,低停顿。是一种以获取最短回收停顿时间为目标的收集器,以获取用户交互体验。该收集器是基于“标记-清除”算法实现的,他的运作过程相对于前面几种收集器更复杂一些,整个过程分为4个步骤:
1.初始标记(initial mark)
2.并发标记(concurrent mark)
3.重新标记(remark)
4.并发清除(concurrent sweep)
其中,初始标记和重新标记步骤仍然需要“Stop The World”,初始标记只是标记一下GC Roots能直接关联到的对像,速度很快。并发标记阶段就是进行GC Roots Tracing的过程。而重新标记阶段则是为了修正并发标记期间因用户继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间比初始标记稍长,但远比并发标记短。由于整个过程中最耗时的并发标记和并发清除过程,收集器线程都可以与用户线程一起工作。所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
三个明显缺点:
①. CMS收集器对CPU资源非常敏感,由于开启一部分收集线程,有可能占用CPU时间大量时间,导致应用变慢。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程占不少于25%的CPU资源。并岁CPU数量增加而减少(不知道计算公式)。但是CPU不足4个时,CMS对用户程序的影响就可能变得很大,如果CPU负载本身比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%。总结,CMS在多CPU硬件配置系统中会收到更好的效果, 反之效果变差。
②. 由于CMS并发清理阶段用户线程还在运行着,还会有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉他们,只能在下次GC时再清理掉。这部分垃圾称为“浮动垃圾”。对于浮动垃圾,仍需要预留出足够的内存空间给用户使用,因此CMS收集器不能像其他收集器那样等到老年代几乎被填满了再进行收集,而是需要为“浮动垃圾”预留空间。在JDK1.5默认设置下,CMS下老年代使用68%的空间就会进行GC。在JDK1.6中,CMS收集器的启动阈值已经提升到了92%。如果CMS运行期间预留的内存无法满足“浮动垃圾”,就会出现“Concurrent ModeFailure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
③. 由于CMS是一款基于“标记-清除”算法实现的收集器,这就意味着收集结束时会出现大量的空间碎片产生。可能出现当剩余空间很大,但没有连续的足够大的空间给一个新对象时,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供一个:-XX +UseCMSCompactAtFullCollection开关(默认开启,换言之,默认下CMS还是会进行碎片整理),用于在CMS收集器进行FullGC时开启内存碎片的合并整理过程。内存整理的过程是无法并发的,空间碎片的问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了一个参数:-XXCMSFullGCsBeforeCompacting,这个参数用于设置多少次不压缩的GC后,来一次带压缩的(默认为0,表示每次进入Full GC都进行碎片整理)。
7. G1(Gabage-First)收集器
G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换*JDK1.5中发布的**CMS收集器,从局部看是基于“复制”算法实现的,意味着G1*不会产生内存空间碎片。与其他GC收集器相比,**G1最大的特点在于:可预测的停顿,这是G1相对于CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
使用G1收集器时,Java堆的内存被划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代概念,但是新生代和和老年代不再是物理隔离的了,他们都是一部分Region的集合。G1之所有能建立可预测的停顿时间模型,是因为他有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面垃圾的价值大小(回收所得空间大小以及回收所需时间的经验值),在后台维护一个优先列表。每次根据允许的收集时间,优先回收价值最大的Region。这种区域划分内存空间以及优先级的区域回收方式,保证了G1收集器在有限时间内可以获取尽可能高的收集效率。
G1把内存“化整为零”的思路实现细节远远没有理解那么简单,从2004年时便已经提出,花费近10年时间才开发出G1的商用版。其中:把Java堆分为多个Region后,垃圾收集是否就能以Region为单位进行?其实不然:Region不可能是孤立的。一个对象分配在某个Region中,他并非只能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性分析确定对象是否存活时,岂不是还得扫描整个Java堆才能保证准确性?这个问题并非在G1中才有,只是G1更加突出而已。在之前的收集器中,如果新生代的对象有到老年代的关联,那么回收新生代时就不得不同时扫描老年代的话,那么Minor GC的效率会下降。而在具体实现中,虚拟机都是使用Remembered Set来避免全堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在堆Reference类型的对象进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reeference引用的对象是否处于不同的Region之中(在之前的收集器中就是检查是否老年代中的对象是否引用了新生代中的对象)。如果是,便通过CardTable把相关信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
G1收集器的运作大致分为:
1.初始标记(Initial Marking)
2.并发标记(Concurrent Marking)
3.最终标记(Final Marking)
4.筛选回收(LiveData Counting and Evacuation)
G1的前几个步骤的运作过程和CMS有很多相似之处。初始阶段只是标记一下GC Roots能直接关联到的对象。并发标记是从GC Roots开始对堆中对象进行可达性分析。这个耗时较长,但是能与用户程序并发执行。最终标记阶段则是为了修正在并发阶段期间因为程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set中,这阶段需要线程停顿,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
以下为输出Jvm所用收集器的代码
public static void main(String args[]) {
List l = ManagementFactory.getGarbageCollectorMXBeans();
for(GarbageCollectorMXBean b : l) {
System.out.println(b.getName());
}
}
输出:
PS Scavenge
PS MarkSweep
关于如何阅读Jvm的GC日志参考:[理解GC日志]
关于GC调试推荐博文:[Java GC 调试手记]