打卡学习JVM,第十二天
本人学习过程中所整理的代码,源码地址
以下大部分内容摘自《深入理解Java虚拟机》
枚举根节点
- 当执行停顿下来后,并不需要一不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在Hotspot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的
安全点(Safe Point)
- 在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高
- 实际上,HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停
- Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint
- 对于Sefepoint,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)
- 抢占式中断
- 不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上
- 主动式中断
- 当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方
现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件
安全区域(Safe Region)
- 使用Safe Point似乎已经完美地解决了如何进入GC的问题,但实际情况却并不一定。Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决
- 安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safe Point。在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止
CMS收集器
- 以获取最短回收停顿时间为目标,多数应用于互联网站或者B/S系统的服务器端上
- CMS是基于标记-清除 算法实现的,整个过程分为4个步骤:初始标记(只是标记一下GC Roots能直接关联到的对象),并发标记(进行GC Roots Tracing),重新标记,并发清除
- 初始标记和重新标记需要STW
- 重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短
- 并发收集、低停顿
- 缺点:
1、CMS收集器对CPU资源非常敏感
2、CMS收集器无法处理浮动垃圾(Floating Garbage,就是指在之前判断该对象不是垃圾,由于用户线程同时也是在运行过程中的,所以会导致判断不准确的, 可能在判断完成之后在清除之前这个对像已经变成了垃圾对象,所以有可能本该此垃圾被回收但是没有被回收,只能等待下一次GC再将该对象回收,所以这种对像就是浮动垃圾),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。如果在应用中老年代增长不是太快,可能适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能。要是CMS运行期间预留的内存无法满足程序需要时,虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低
3、收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前进行一次Full GC。CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长
空间分配担保
- 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。当大量对象在Minor GC后仍然存活,就需要老年代进行空间分配担保,把Survivor无法容纳的对象直接进入老年代。如果老年代判断到剩余空间不足(根据以往每一次回收晋升到老年代对象空间的平均值作为经验值),则进行一次Full GC。
CMS收集器收集步骤
- Phase1 :Initial Mark(初始标记)
需要STW,这个阶段的目标是:标记那些直接被GC Root引用或者被年轻代存活对象所引用的所有对象
- Phase2 : Concurrent Mark (并发标记)
遍历老年代,然后标记所有存活的对象,它会根据上个阶段找到的GC Roots遍历查找,它会与用户的应用程序并发运行,并不是老年代所有的存活对象都会被标记,因为在标记期间用户的程序可能会改变一些引用
- Phase3 : Concurrent Preclean(并发预先清除)
与应用的线程并发运行,一些对象的引用可能会发生变化,但是这种情况发生时,JVM会将包含这个对象的区域(Card)标记为Dirty,这也就是Card Marking
在pre-clean阶段,那些能够从Dirty对象到达的对象也会被标记,这个标记做完后,dirty card标记就会被清除了
- Phase4 : Concurrent Abortable Preclean(并发可能失败的预先清除)
并发阶段,这个阶段是为了尽量承担STW中最终标记阶段的工作。这个阶段持续时间依赖于很多的因素,由于是在重复做很多相同的工作,直接满足一些条件(如重复迭代的次数、完成的工作量或者时钟时间等)
- Phase5 : Final Remark(最终重新标记)
第二个STW阶段,这个阶段的目标是标记老年代所有的存活对象,由于之前的阶段是并发执行的,GC线程可能跟不上应用程序的变化,为了完成标记老年代所有存活对象的目标,STW就非常有必要了
通常CMS的Final Remarj阶段会在年轻代尽可能干净的时候运行,目的是为了减少连续STW发生的可能性(年轻代存活对象过多也会导致老年代涉及的存活对象过多),这个阶段会比前面几个阶段更复杂一些
至此为止,标记阶段已经完成,开始通过清除算法清除
- Phase6 : Concurrent Sweep(并发清除)
与用户的应用程序并发运行,这个阶段是为了清除那些不再使用的对象,回收它们的占用空间为将来使用
- Phase7 : Concurrent Reset(并发重置)
并发执行,它会重设CMS内部的数据结构,为下次的GC做准备
总结:CMS通过将大量工作分散到并发处理阶段来减少STW时间,在这块做得非常优秀,但CMS收集器无法处理浮动垃圾,并且会导致空间碎片,导致无法分配大对象;对于堆比较大的应用,GC的时间难以预估
测试用例:
VM Option:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC
public class MyTest5 {
public static void main(String[] args) throws InterruptedException {
int size = 1024 * 1024;
byte[] myAlloc1 = new byte[4 * size];
System.out.println("111111");
byte[] myAlloc2 = new byte[4 * size];
System.out.println("222222");
byte[] myAlloc3 = new byte[4 * size];
System.out.println("333333");
Thread.sleep(1000);
byte[] myAlloc4 = new byte[2 * size];
System.out.println("444444");
}
}
打印日志:
111111
[GC (Allocation Failure) [ParNew: 6322K->685K(9216K), 0.0023811 secs] 6322K->4783K(19456K), 0.0024408 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
222222
[GC (Allocation Failure) [ParNew: 5020K->153K(9216K), 0.0036918 secs] 9119K->9002K(19456K), 0.0037384 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8848K(10240K)] 13098K(19456K), 0.0002609 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-mark-start]
333333
[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (CMS Final Remark) [YG occupancy: 4405 K (9216 K)][Rescan (parallel) , 0.0001423 secs][weak refs processing, 0.0000232 secs][class unloading, 0.0004092 secs][scrub symbol table, 0.0007216 secs][scrub string table, 0.0001729 secs][1 CMS-remark: 8848K(10240K)] 13253K(19456K), 0.0015660 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
444444
Heap
par new generation total 9216K, used 7798K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 93% used [0x00000000fec00000, 0x00000000ff3771a8, 0x00000000ff400000)
from space 1024K, 15% used [0x00000000ff400000, 0x00000000ff426710, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
concurrent mark-sweep generation total 10240K, used 8847K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3945K, capacity 4568K, committed 4864K, reserved 1056768K
class space used 434K, capacity 460K, committed 512K, reserved 1048576K