3.4JVM分别对新生代和老年代采用不同的垃圾回收机制
3.4.1新生代的GC
新生代通常存活时间较短,因此基于复制算法来进行回收,所谓复制算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中.
对应于新生代:就是在Eden和其中一个Survivor,复制到另一个之间Survivor空间中,然后清理掉原来就是在Eden和其中一个Survivor中的对象。在执行机制上JVM提供了串行GC(Serial Old)、并行 回收 GC(parallel Old)和并发GC。
并发和并行的区别
新生代常见的垃圾收集器
1)串行GC(Serial GC)
在整个扫描和复制过程采用单线程的方式来进行,主要用在嵌入式等内存比较小的程序,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定
2)并行回收GC(Parallel Scavenge)
多条垃圾收集起并行工作,但是用户线程时阻塞的,适用于交互性比较弱的场景,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)
3)并发GC (ParNew)
用户线程和垃圾回收器并发执行,适用于响应时间优先,交互性比较强的场景,并发GC的触发条件就不太一样。以CMS GC (-XX:+UseConcMarkSweepGC -XX:+UseParNewGC)为例,它主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen做并发收集
3.4.2老年代的GC
老年代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,CMS用标记清除,G1用标记整理。在执行机制上JVM提供了串行GC(Serial Old)、并行 回收 GC(parallel Old)和并发GC。[CMS (-XX:+UseConcMarkSweepGC -XX:+UseParNewGC), G1(-XX:UseG1GC)],具体算法细节还有待进一步深入研究。
老年代常见的垃圾收集器
3.4.2.1 Serial Old(标记整理算法)
3.4.2.2 Parallel Old(标记整理算法)
3.4.2.3 CMS 收集器
优点:并发收集、低停顿。
缺点:
1)CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
2)CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。
3)CMS是一款“标记--清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
CMS过程:
初始标记 :(STW )在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
重新标记 :(STW )这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"根对象"开始向下追溯,并处理对象关联。
并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
相关参数:
-XX:ConcGCThreads:并发的 GC 线程数
-XX:+UseCMSCompactAtFullCollection:FullGC 之后做压缩
-XX:CMSFullGCsBeforeCompaction:多少次 FullGC之后压缩一次
-XX:CMSInitiatingOccupancyFraction:触发 FullGC
-XX:+UseCMSInitiatingOccupancyOnly:是否动态可调
-XX:+CMSScavengeBeforeRemark:FullGC之前先做 YGC
-XX:+CMSClassUnloadingEnable:启用回收Perm 区
3.4.3 G1收集器(介于新生代和老年代之间的垃圾收集器,复制+标记整理算法)
G1 jdk8开始,推荐使用(重点学习,jdk9中已经设为默认垃圾收集器)
G1(global concurrent marking)的执行过程分为五个步骤:
初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
并发标记(Concurrent Marking)
G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
最终标记(Remark,STW)
该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。把Rember Set log的数据合并到Rember Set中
清除垃圾(Cleanup,STW)
G1具备如下特点:
1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,
G1的几个概念
Region
传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址(逻辑地址,下同)是连续的。如下图所示:
而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。如下图所示:
在上图中,我们注意到还有一些Region标明了H,它代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象
为了减少连续H-objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size。
一个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数。如果不设定,那么G1会根据Heap大小自动决定
SATB
Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。
那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:
- 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
- 灰:对象被标记了,但是它的field还没有被标记或标记完。
- 黑:对象被标记了,且它的所有field也被标记完了。
RSet
记录了其他 Region中的对象引用本 Region 中对象的关系,全称是Remembered Set,是辅助GC过程的一种结构,典型的空间换时间工具。逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象),而Card Table则是一种points-out(我引用了谁的对象)的结构
上图中有三个Region,每个Region被分成了多个Card,在不同Region中的Card会相互引用,Region1中的Card中的对象引用了Region2中的Card中的对象,蓝色实线表示的就是points-out的关系,而在Region2的RSet中,记录了Region1的Card,即红色虚线表示的关系,这就是points-into。
G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的
YoungGC
新对象进入 Eden 区
存活对象拷贝到Survivor 区
存活时间达到年龄阈值时,对象晋升到 Old 区
MixedGC
不是 FullGC,选定所有年轻代里的Region,外加根据global concurrent marking统计得出收益高的若干老年代Region
MixedGC时机
-XX:InitiatingHeapOccupancyPercent 堆占有率到达这个数值时触发global concurrent marking,默认45%
-XX:G1HeapWastePercent 在global concurrent marking可以知道区有多少空间可以被回收,YGC和Mixed GC之间判断垃圾占比是否到达此参数,到达了才会发生Mixed GC
-XX:G1MixedGCLiveThresholdPercent
-XX:G1MixedGCCountTarget
-XX:G1OldGCSetRegionThresholdPercent
-XX:+UseG1GC 开启 G1
-XX:G1HeapRegionSize=n, Region 的大小,1-32M,最多2048个
-XX:MaxGCPauseMillis=200 最大停顿时间
-XX:G1NewSizePercent、-XX:G1MaxNewSizePercent
-XX:G1ReservePercent=10 保留防止 to space溢出
-XX:ParallelGCThreads=n SWT线程数
-XX:ConcGCThreads=n 并发线程数=1/4*并行
需要切换到 G1的情况:
1. 50%以上的堆被存活对象占用
2. 对象分配和晋升的速度变化非常大
3. 垃圾回收时间特别长,超过了1秒
3.4.4 GC机制的组合适用
以上各种GC机制是需要组合使用的,指定方式由下表所示:
指定方式 |
新生代GC方式 |
旧生代GC方式 |
-XX:+UseSerialGC |
串行GC |
串行GC |
-XX:+UseParallelGC |
并行回收GC |
并行GC |
-XX:+UseConeMarkSweepGC |
并行GC |
并发GC |
-XX:+UseParNewGC |
并行GC |
串行GC |
-XX:+UseParallelOldGC |
并行回收GC |
并行GC |
-XX:+ UseConeMarkSweepGC -XX:+UseParNewGC |
串行GC |
并发GC |
不支持的组合 |
1、-XX:+UseParNewGC -XX:+UseParallelOldGC 2、-XX:+UseParNewGC -XX:+UseSerialGC |
标题
3.4.5 GC 日志的分析
打印日志相关参数:
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution
例(默认为 ParallelGC, 其它的添加-XX:+UseConcMarkSweepGC或-XX:+UseG1GC即可):
JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log"
CMS日志格式( -XX:+UseConcMarkSweepGC)
G1日志格式YCG
G1日志 global concurrent marking
G1日志 MixedGC
下面的文章是具体实战下的各种情况调优,虽然里面的日志格式不是最新的但是,但是大部分是可以复用的
可视化工具分析GC日志
Universal JVM GC analyzer - Java Garbage collection log analysis made easy 在线工具
GCViewer mvn clean package -Dmaven.test.skip 生成 jar包,双击执行,导入日志即可进入图形化分析页面