JVM(Java Virtual Machine)即Java虚拟机,Java代码都是在JVM上运行的,所以了解JVM是成为Java高手的毕竟之路。
本系列内容将对JVM的知识进行介绍,是从头学习JVM知识的笔记。
本系列内容根据自己的学习和理解的基础上,并参考《深入理解Java虚拟机》一书介绍的知识所写。如果有写的不对的地方,请各位多多提点。
从头开始学习JVM(六)—— 垃圾收集器
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是具体实现。
接下来将介绍7种收集器,如下图所示,若两个收集器之间有连线,就说明它们可以搭配使用。注:没有万能的收集器,只能从中选择适合的收集器。
Serial 收集器
这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。它采用的是复制算法。
虽然它需要全部暂停(Stop The World),用户体验感差。但是它简单高效,没有其他线程开销专心收集效率自然高。
ParNew 收集器
可以认为是 Serial 收集器的多线程版本。即用多线程进行垃圾收集外,其他的参数或者策略与Serial 收集器一样。
ParNew 收集器相对 Serial 收集器提高了资源利用率,可以在多个CPU上执行GC,但是每个CPU上的效率都不会超过 Serial 收集器的。除此之外,它还可以配合CMS收集器进行收集。
Parallel Scavenge 收集器
这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器,看似与ParNew 收集器类似。Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量(Throughput),所以也常称为“吞吐量优先”收集器。高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)
虚拟机会根据当前系统运行情况收集性能监控信息,动态调整对应参数以提供合适的停顿时间或者最大吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。自适应调节策略也是Parallel Scavenge 收集器与ParNew 收集器的一个重要区别。
Serial Old 收集器
Serial Old 收集器是 Serial 收集器的老年代版本,单线程,使用 标记 - 整理算法。
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记-整理算法
CMS 收集器
CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记-清除 算法实现。
CMS 收集器较之前几种运作更复杂一些。运作步骤如下:
- 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象,需要Stop The World。
- 并发标记(CMS concurrent mark):进行 GC Roots Tracing,操作时间长。
- 重新标记(CMS remark):修正并发标记期间的变动部分,需要Stop The World,但停顿比初始标记时间长,但比并发标记时间短。
- 并发清除(CMS concurrent sweep),操作时间长。
CMS 收集器是第一款真正意义上的并发(Concurrent)收集器。整个过程虽然并发标记和并发清除耗时最长,但是可以和用户线程一起并发工作。
CMS收集器的优点是并发、低停顿,也常被称为并发低停顿收集器(Concurrent Low Pause Collector)。
CMS收集器有以下3个缺点:
- 对CPU资源非常敏感。在并发阶段,会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,吞吐量降低。其实,面向并发设计的程序都对CPU资源比较敏感。
- 无法收集浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在并发得运行鬓发清除阶段时,用户线程还在运行就自然会产生新的垃圾,这一部分此次因为并发新产生的此次回收无法收集的垃圾就是“浮动垃圾”。因为不能让老年代被垃圾填满,JDK1.6后达到92%后就会默认触发Concurrent Mode Failure。
- 基于标记-清除算法,会存在大量不连续的空间碎片。为了解决这个问题,CMS默认开启当老年区内存不足以存储大内存对象时提前触发一次Full GC。
并行与并发
复习一下并行与并发的知识,并说明一下并行收集与并发收集。
线程中的并行与并发:
- 并行,即当系统有一个以上的CPU时,两个进程同时进行,互不抢占资源。
- 并发是指两个或多个事件在同一时间间隔发生。在操作系统中,是指一个时间段中有几个程序都处于已启动到运行完毕之间,且他们都处于同一个处理机上。
并发并不是真正的“同时进行”,而是CPU把一个时间段划分成几个时间片断(时间区间),然后在其间来回切换,CPU处理得飞快,感觉就像是同时进行。
收集器的并行与并发:
- 并行(Parallel),指多条垃圾收集线程并行工作,此时用户线程处于等待状态。
- 并发(Concurrent),指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。
G1 收集器
面向服务端的垃圾回收器,使命是替换掉CMS收集器。G1收集器将内存布局分为多个大小相等的独立区域(Region),虽然还保留新生区和老年区,但是它可以整体按Region进行回收。
在G1收集器中,每个Region中使用Remembered Set来避免全堆扫描的。
G1的特点:
- 并行与并发。并行和并发的方式都可以,目的充分利用资源来缩短停顿(Stop The World)的时间。
- 分代收集。仍然保留分代的概念,用于记录对象的生存周期。
- 空间整合。从G1整体来看是使用了 标记-整理算法;从局部(Region)来看是基于复制算法。
- 可预测停顿。能建立可预测的停顿时间模型,能让使用明确指定在一个长度为M毫秒的时间片里,几乎是实时Java(RTSJ)的垃圾收集器的特征了。
运作步骤:
- 初始标记(Initial Marking):标记 GC Roots 能直接关联到的对象,且修改TAMS(Next Top at Mark Start)的值,让下一阶段可以正确使用Region创建新对象。此阶段需要停顿,但是耗时很短。
- 并发标记(Concurrent Marking):从GC Roots进行可达性分析找出存活对象,耗时较长,可并发执行。
- 最终标记(Final Marking):修正并发标记期间因用户程序继续运作导致的修改,需停顿,可并行执行。
- 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间制定回收计划,回收部分Region。可以并发执行。
Minor、Major、Full GC
Minor GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
当发生Minor GC事件的时候,有一些需要注意的地方:
- 当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
- 写指针总是停留在所使用内存池的顶部。内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。
- Eden 和 Survivor 区不存在内存碎片。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。
- Minor GC 都会触发停顿(stop-the-world),停止应用程序的线程。对于大部分应用程序,停顿导致的延迟常常感觉是可以忽略不计的,实际上真相是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年区空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。
Major GC 和 Full GC
《深入理解Java虚拟机》一书中对Major GC/Full GC的定义:
老年区GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随着至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
大家应该注意到,目前,这些术语无论是在 JVM 规范还是在垃圾收集研究论文中都没有正式的定义。但是我们可以根据已知的基础之上做出如下定义:
- Major GC 是清理老年区。
- Full GC 是清理整个堆空间—包括年轻区和老年区,还有元数据区。
一般情况下,Major GC 通常是由 Minor GC 触发的,很难将它们完全分离开来,许多现代垃圾收集机制会清理部分老年区空间,所以使用“cleaning”一词只是部分正确。
在发生Minor GC时,虚拟机会检查每次晋升进入老年区的大小是否大于老年区的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设 置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年区还有很多内存,所以,最好不要这样做)。
理解GC日志
//待补充
GC 参数总结
垃圾收集器相关常用参数
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机运行在Client模式下的默认值,打开后使用Serial+Serial Old的组合回收。 |
UseParNewGC | 打开后使用ParNew+Serial Old的组合回收。 |
UseConcMarkSweepGC | 打开后使用ParNew+CMS+Serial Old的组合回收。Serial Old作为CMS出现Concurrent Mode Failure的后备收集器。 |
UseParallelGC | 虚拟机运行在Server模式下的默认值,打开后使用Parallel Scavenge+Serial Old的组合回收。 |
UseParallelOldGC | 打开后使用Parallel Scavenge+Parallel Old的组合回收。 |
SurvivorRatio | 新生区中Eden与Survivor区域容量的比值,默认为8,即Eden:Survivor=8:1 |
PretenureSizeThreshold | 设置这个参数后,大于这个参数的对象直接在老年区分配 |
MaxTenuringThreshold | 晋升到老年区的对象年龄,每经过一次Minor GC新生区对象年龄+1 |
UseAdaptiveSizePolicy | 动态调整Java堆中各个区域的大小以及进入老年区的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年区剩余空间不足以应付新生区所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | GC时间占总时间的比率,默认值99,即允许1%的GC时间。仅在使用Parallel Scavenge生效 |
MaxGCPauseMillis | 设置GC最大停顿时间。仅在使用Parallel Scavenge生效 |
CMSInitiatingOccupancyFraction | 设置CMS在老年代空间被使用多少后触发Full GC,默认值68%。仅在CMS有效 |
UseCMSCompactAtFullCollection | 设置CMS在完成垃圾收集后是否要进行一次碎片整理(Full GC把空间碎片整理下)。仅在CMS有效 |
CMSFullGCsBeforeCompaction | 设置CMS在进行若干次垃圾收集后是否要进行一次碎片整理(Full GC把空间碎片整理下)。仅在CMS有效 |