一、垃圾收集算法
1.1 标记-清除
首先标记处需要清理的对象,然后回收所有被标记的对象,缺点在于:空间碎片,标记清除后内存中仍存在地址不连续的对象(内存碎片),如果内存碎片过多,会导致为大对象分配空间时无可用空间,触发又一次的GC。
1.2 标记-整理
当标记完待回收对象后,让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存,**不是直接对可回收对象进行清理。**缺点是整理需要花费一定时间。
1.3 复制
复制算法将内存划分为相等的两块,每次只使用其中一块。当这一块内存用完时,就将还存活的对象复制到另一块上面,然后将已经使用过的内存空间一次清理掉。缺点是内存使用率降为一半,对象存活率较高时,需要多次进行复制操作,效率变低。
1.4 分代收集算法
新生代采用复制算法,在老年代采用“标记-清除”或者“标记-整理”算法。新生代分为Eden区和两个相同大小的Survivor区,所有新创建的对象都分配在Eden区域中。当Eden区域满后会触发minor GC,将Eden区仍然存活的对象复制到其中一个Survivor区域中,另外一个Survivor区中的存活对象也复制到这个Survivor区域中,并始终保持一个Survivor区是空的。一般建议Young区地大小为整个堆的1/4。下面分别展示了新生代初始化-->Young GC-->执行完毕时的状态。使用了复制算法,但是并没有依据1/2划分内存,JVM调整Eden & Survivor使用比率的参数是-XX:SurvivorRatio。SurvivorRatio=3表示Eden:From:To = 3:1:1
老年代存放新生代Survivor满后触发minor GC后仍然存活的对象,当Eden区满后会将存活的对象放入Survivor区域,如果Survivor区存不下这些对象,GC收集器就会将这些对象直接存放到Old区中,如果Survivor区中的对象足够老(-XX:MaxTenuring决定),也直接存放到Old区中。如果Old区满了,将会触发Full GC回收整个堆内存。
二、垃圾收集器
新生代的垃圾回收器包括Serial、ParNew、Parallel Scavenge,老年代的垃圾回收器包括CMS、Serial Old、Parallel Old。其中新生代的三种垃圾回收器都采用了复制算法。
2.1 Serial(触发STOP THE WORLD)
Serial收集器是一个单线程收集器(-XX:+UseSerialGC),这个“单线程”不只是说它只会使用一个CPU或者一条线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它垃圾收集结束。它对于运行在client模式下的虚拟机来说是一个不错的选择。
2.2 ParNew(触发STOP THE WORLD)
ParNew收集器是Serial收集器的多线程版本(-XX:+UseParNewGC -XX:ParallelGCThreads),它能够与CMS收集器配合工作,因此,在运行在Server模式下的虚拟机中,ParNew收集器是首选的新生代收集器。阿里的生产机器使用了ParNew + CMS 的组合。
ParNew收集器是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC强制指定。使用-XX:ParallelGCThreads可以限制垃圾收集的线程数。
2.3 Parallel Scavenge(触发STOP THE WORLD)
这也是一个并行的新生代垃圾收集器,不同于其他收集器(以尽可能缩短垃圾收集时用户线程的停顿时间为目的),它是唯一一个以达到一个可控制的吞吐量为目标的垃圾收集器。
throughput = 运行用户代码的时间 / 总时间(垃圾收集时间+运行用户代码的时间)。
在后台运算的任务中,不需要太多的交互,保证运行的高吞吐量可以高效地利用CPU时间,尽快完成程序的运算任务。
Parallel Scavenge 收集器可以使用自适应调节策略,使用-XX:+UserAdaptiveSizePolicy选项之后,就不需要指定-Xmn、-XX:SurvivorRatio等参数,虚拟机可以根据当前系统的运行情况动态收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
2.4 CMS(Concurrent Mark Sweep)
CMS收集器是一款并发收集器,以获取最短回收停顿时间为目标。包括四个阶段:初始标记(STW)——>并发标记——>重新标记(STW)——>并发清除。初始标记为了标记GC Root能够直接关联的对象;并发标记为了并行标记其他存活的对象;重新标记为了修正并发标记期间用户线程继续运行到值
CMS触发条件:
- 老年代使用率达到阈值
CMSInitiatingOccupancyFraction
- 如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发
- 新生代的晋升担保失败(新生代对象晋升到老年代时,发现空间不够,提早进行一次老年代的回收,防止下次进行YGC的时候发生晋升失败)
- 永久代的使用率达到阈值
CMSInitiatingPermOccupancyFraction
,默认92%,前提是开启CMSClassUnloadingEnabled
收集过程:
1、CMS用于处理老年代,当老年代没有足够的空间来容纳新分配或提升的对象,并行收集器开始做垃圾收集。由于在垃圾收集期间程序依然在运行&分配对象,CMS为了保证程序在使用完堆内存之前完成垃圾清理工作,CMS需要提前启动。那么什么时间点启动是比较合适的呢,用户可以通过设置UseCMSInitiatingOccupancyOnly、CMSClassUnloadingEnabled、CMSInitiatingOccupancyFraction参数告诉JVM时间点。
-XX:CMSInitiatingOccupancyFraction=<value>,该值代表老年代堆空间的使用率。比如,value=80意味着第一次CMS垃圾收集会在老年代被占用80%时被触发。通常CMSInitiatingOccupancyFraction的默认值为68(之前很长时间的经历来决定的)。
-XX:+UseCMSInitiatingOccupancyOnly,命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期,当该标志被开启时,**JVM通过CMSInitiatingOccupancyFraction的值进行每一次CMS收集,而不仅仅是第一次。如果不设置该参数,**系统会根据统计数据自行决定什么时候触发cms gc;因此有时会遇到设置了80%比例才cms gc,但是50%时就已经触发了,就是因为这个参数没有设置的原因;
**-XX:+CMSClassUnloadingEnabled,**CMS收集器默认不会对永久代进行垃圾回收。如果希望对永久代进行垃圾回收,可用设置该标志。
2、当cms gc开始时,首先的阶段是CMS-initial-mark,此阶段是初始标记阶段,是stop the world阶段,因此此阶段标记那些从 GC Root 可直达的存活对象;CMS-initial-mark:2517267K(3145728K)表示开始标记时,老年代共有3145728K,已占用2517267K,2696206K表示堆内存已占用空间,5068160K表示堆内存大小。
3、下一个阶段是CMS-concurrent-mark,此阶段是和应用线程并发执行的,并发标记主要作用是从上一个阶段标记的对象开始,并行去标记所有可达存活对象。此阶段会打印CMS-concurrent-mark-start,CMS-concurrent-mark。
4、下一个阶段是CMS-concurrent-preclean,此阶段主要进行一些预清理,因为标记和应用线程是并发执行的,因此会有些对象的状态在初始标记后会改变,此阶段正是解决这个问题。首先会重新扫描新生代,例如在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象;其次老年代中有对象内部引用发生变化,重新标记那些在并发标记阶段引用被更新的对象(晋升到老年代的对象、原本就在老年代的对象)。由于后续Rescan阶段会触发stop the world,为了使暂停的时间尽可能的短,也需要preclean阶段先做一部分工作以节省时间。此阶段会打印CMS-concurrent-preclean-start,CMS-concurrent-preclean。
5、下一阶段是CMS-concurrent-abortable-preclean阶段(可中断的预清理),加入此阶段的目的是使cms gc更加可控一些,作用也是执行一些预清理,以减少Rescan阶段造成应用暂停的时间。该阶段存在的意义是尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。参数有:
-XX:CMSMaxAbortablePrecleanTime=<value>:当abortable-preclean阶段执行达到这个时间<value>时才会结束,我们系统的设置为5000毫秒 。
-XX:CMSScheduleRemarkEdenSizeThreshold(默认2m):控制abortable-preclean阶段什么时候开始执行,即当eden使用达到此值时,才会开始abortable-preclean阶段。
-XX:CMSScheduleRemarkEdenPenetratio(默认50%):控制abortable-preclean阶段什么时候结束执行。
6、下一个阶段是CMS FInal Remark,即重新标记,会触发第二次的STW。此时的重新标记包含:
- 遍历新生代对象,重新标记;
- 根据GC Roots,重新标记;
- 遍历老年代理引用发生变化的对象,重新标记。
如果新生代的使用率很高,需要遍历处理的对象也很多,对于耗时来说较长 (因为可能大量的对象是暂时存活的,而且这些对象也可能引用大量的老年代对象,造成很多应该回收的老年代对象而没有被回收,遍历递归的次数也增加不少)。如果在AbortablePreclean阶段中能够恰好的发生一次YGC,这样就可以避免扫描无效的对象。
如果在AbortablePreclean阶段没来得及执行一次YGC,怎么办?
CMS算法中提供了一个参数:CMSScavengeBeforeRemark
,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。
不过,这种参数有利有弊,利是降低了Remark阶段的停顿时间,弊的是在新生代对象很少的情况下也多了一次YGC,最可怜的是在AbortablePreclean阶段已经发生了一次YGC,然后在该阶段又傻傻的触发一次。
在FInal Remark阶段,可能存在一些被标记了但已不再存活的对象,这些对象在本次CMS中不会被清理,等待下次CMS时处理。
YG occupancy:472390K(1922432K),指执行时新生代的已占用(总共有)内存情况。 CMS remark:2517267K(3145728K),指执行时老年代已占用(总共有)内存情况。后面已占用堆内存2989657K = 2517267K + 472390K,堆内存5068160K =3145728K + 1922432K。
7、下一个阶段是CMS-concurrent-sweep,进行并发的垃圾清理。
8、最后是CMS-concurrent-reset,为下一次cms gc重置相关数据结构。
在CMS时,需要关注两种异常:concurrent mode failure & promotion failed。
concurrent mode failure:当CMS执行过程中,新生代晋升的对象在老年代中找不到可用空间(老年代空间不足),造成concurrent mode failure。可能的场景包含:
- 老年代中存活的数据太大,以致老年代没有足够空间支持分配;
- 如果长时间频繁出现,有可能是老年代设置太小或者CMSGC后没有进行压缩的原因导致;
- 应用本身行为变化,导致JVM无法充分的预估新晋升对象的大小。(如:突然有一个非常大的对象,以致新生代无法存放,而老生代空间虽然大于平时预估对象大小,但是此对象老生代还是无法存放)
promotion-failed:当young gc时,有部分young代对象仍然可用,但是S1或S2放不下,因此需要放到old代,但此时old代空间无法容纳此对象,造成promotion-failed。
解决方法是修改参数:
- promotion failed – concurrent mode failure
现象:Minor GC后, 救助空间容纳不了剩余对象,将要放入老年带,老年带有碎片或者不能容纳这些对象,就产生了concurrent mode failure, 然后进行stop-the-world的Serial Old收集器。
解决:-XX:+UseCMSCompactAtFullCollection & -XX:CMSFullGCsBeforeCompaction=1,在执行1次full gc之后,cms gc执行完毕后内存碎片压缩。通常调小CMSFullGCsBeforeCompaction。
2. concurrent mode failure
现象:CMS是和业务线程并发运行的,在执行CMS的过程中有业务对象需要在老年带直接分配,例如大对象,但是老年带没有足够的空间来分配,所以导致concurrent mode failure, 然后需要进行stop-the-world的Serial Old收集器。
解决:-XX:CMSMaxAbortablePrecleanTime=5000,缩小CMS-concurrent-abortable-preclean阶段的时间
-XX:CMSInitiatingOccupancyFraction=80,提前触发cms gc,防止gc处理不及时。
2.5 Minor GC
触发条件:当新生代的Eden区满的时候触发 Minor GC
Minor GC存在一个问题就是,老年代的对象可能引用新生代的对象,在标记存活对象的时候,就需要扫描老年代的对象,如果该对象拥有对新生代对象的引用,那么这个引用也会被作为 GC Roots。这相当于就做了全堆扫描。
HotSpot 给出的解决方案是 一项叫做 卡表 的技术。如下图所示:
卡表的具体策略是将老年代的空间分成大小为 512B的若干张卡,并且维护一个卡表,卡表本省是字节数组,数组中的每个元素对应着一张卡,其实就是一个标识位,这个标识位代表对应的卡是否可能存有指向新生代对象的引用,如果可能存在,那么我们认为这张卡是脏的,即脏卡。如上图所示,卡表3被标记为脏。
在进行Minor GC的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的老年代指向新生代的引用加入到 Minor GC的GC Roots里,当完成所有脏卡的扫描之后,Java 虚拟机便会将所有脏卡的标识位清零。这样虚拟机以空间换时间,避免了全表扫描。
2.6 Serial Old(兜底角色)
它主要的两大用途:1. 配合Parallel Scavenge收集器;2. 作为CMS收集器在并发收集出现Concurrent Mode Failure时使用的后备预案(CMS发生了concurrent mode fail之后退化成了Serial Old收集器,它是单线程的标记-压缩收集器)。
2.7 Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。在注重吞吐量和CPU资源敏感的场合,优先考虑使用Parallel Scavenge + Parallel Old收集器的组合,切记Parallel Scavenge 是无法与CMS收集器组合使用的
参考:http://www.importnew.com/27822.html
https://my.oschina.net/hosee/blog/674181
https://blog.csdn.net/ITer_ZC/article/details/41825395
https://juejin.im/post/5b8d2a5551882542ba1ddcf8
https://www.jianshu.com/p/2a1b2f17d3e4
来源:oschina
链接:https://my.oschina.net/u/2302503/blog/1632775