垃圾回收与垃圾收集器
垃圾回收(Garbage Collection):JVM清除无用对象,释放内存空间的抽象定义
- Minor GC:年轻代GC
- Major GC:老年代GC
垃圾收集器(Garbage Collector):垃圾回收的具体实现
两者都简称GC
STW(Stop The World)
JVM在执行某些整堆操作前的动作,挂起几乎所有线程(正在进行GC的线程除外)造成的全局停止现象
JVM通过在线程中添加SafePoint,达到STW的目的
SafePoint
在线程的特定位置插入通知线程停止的指令
位置
- 循环末尾
- 方法返回前
- 调用方法后
- 抛出异常的位置
造成STW的动作
-
GC
-
VM Threads 的某些特殊的 VM Operation
1,JIT相关操作
2,Class Redefinition(热加载类,如AOP的动态代理)
3,Biased Lock Revocation(取消偏斜锁)-XX:-UseBiasedLocking,禁用偏斜锁
4,Various Debug Operation
常用参数
参数 | 描述 | 备注 |
---|---|---|
-XX:+PrintGCApplicationStoppedTime | GC日志内打印JVM所有停顿时长 | JDK1.7.40版本前,不打印时间戳 |
-XX:+PrintGCApplicationConcurrentTime | GC日志内打印JVM两次停顿间的运行时长 | JDK1.7.40版本前,不打印时间戳 |
-XX:+PrintSafepointStatistics | 打印SafePoint,VM Operation,线程数等相关信息 | |
-XX:+PrintSafepointStatisticsCount | 打印到达SafePoint时各阶段所消耗时长 | 重点关注 vmop:执行VM Operation时长 |
引用
- Java引用分为强引用,软引用,弱引用,幻象引用(虚引用)
- 所有引用队列均是Java.lang.ref.Refrence的子类
强引用(对应强可达)
- 最常见的对象引用,GC不会回收强引用的对象
- 当超出了引用的作用域或显示的设置为null时,引用关系解除,表示对象可以被GC
软引用(对应软可达)
- 软引用的对象不会直接被GC,只有当JVM认为内存不足时才会尝试回收软引用对象
- JVM会确保在OOM之前进行回收
- 软引用通常可用来做内存敏感的缓存
弱引用(对应弱可达)
- 弱引用仅用作提供一种对象的访问途径,无法阻止对象被GC
- 通过弱引用获取对象时,如果对象还在则直接使用,否则重新实例化
幻象引用(对应幻象可达)
- 必须配合引用队列使用,通过引用get只能返回null
- 一般用于监控对象的创建、销毁
引用队列(ReferenceQueue)
- 引用队列可以配合软引用、弱引用、幻象引用使用,当引用对象要被JVM回收时,会将其加入到引用队列中
- 通过引用队列,可以了解JVM GC的情况
吞吐量(Throughput)
- CPU运行用户程序时间(User Program Time,UPT)占CPU总运行时间的比例
- 在GC场景下,吞吐量 = UPT / (UPT + GC)
对象存活判断
引用计数
- 对象新增引用则引用计数+1,释放引用则引用计数-1,引用计数为0时表示对象可以被GC
- 仅通过引用计数来判断对象存活,无法解决循环引用问题
可达性分析
- JVM随机选取GC Roots对象,从GC Roots对象开始标记其余对象的可达性
- 任何GC Roots不可达的对象表示可以被GC
GC Roots对象选取
1,虚拟机栈,本地方法栈引用的对象
2,静态属性引用的对象
3,常量
可达性
1,强可达:对象不可被回收
2,软可达:对象在Full GC前会被回收
3,弱可达:对象可被回收
4,幻象可达:对象已被执行finalize
垃圾回收算法
标记 - 清除
- 标记清除算法分为“标记”“清除”两步,“标记”可以被GC的对象,“清除”已被“标记”的对象
- 对象被“清除”后会产生内存碎片,内存碎片会导致内存空间的利用率不足(对象分配的内存需要是连续的内存空间)
标记 - 压缩
- 即标记 - 清除 - 压缩
- 在标记清除算法上添加了内存压缩算法。“压缩”已被清除的内存空间,移动所有存活的对象使被使用的内存空间保持连续
- “压缩”非常耗时
复制
- 即标记 - 清除 - 复制
- 复制算法逻辑上将内存划分为两块,同一时间仅使用其中一块。GC时先进行标记清除,随后将存活的对象全部复制到另一块空闲的内存中,复制后对象自然使用的是连续的内存空间
- 复制算法省去了“压缩”移动对象的时间消耗,但内存空间利用率有所降低
垃圾收集器(Garbage Collector)
- Java目前主要在用的GC有Serial,ParNew,Paralle,CMS,G1,ZGC
- JDK9中CMS已经被标记废弃,默认使用G1
Serial
- 单线程串行GC,GC时是STW的
- 分为作用于年轻代的 Serial 和作用于老年代的 Serial Old
Serial
- 使用复制算法,即Eden + Survivor From => Survivor To
Serial Old
- 使用标记压缩算法,使内存空间利用率最大化
常用参数
参数 | 描述 | 备注 |
---|---|---|
-XX:+UseSerialGC | 启用 Serial + Serial Old GC |
ParNew
- 作用于年轻代的GC
- Serial 的多线程版本,除多线程执行GC外,其余特性与 Serial 基本一致(共用了大部分代码)
常用参数
参数 | 描述 | 备注 |
---|---|---|
-XX:+UseParNewGC | 启用 ParNew GC | |
-XX:ParallelGCThreads | 设置并行线程数量,默认:≈CPU数 | N = cpus <= 8 ? cpus : 3 + (cpus * 5 / 8) |
Paralle
- 分为使用复制算法作用于年轻代的 Paralle Scavenge 和使用标记整理算法作用于老年代的 Paralle Old
- 是一种基于目标的多线程GC,以吞吐量为目标参数。吞吐量越大GC时间越短,GC触发越频繁
Paralle Scavenge
- 与 ParNew 相似,最显著的区别在于 Paralle Scavenge 关注的是吞吐量而非STW。通过目标参数可直接设置吞吐量要求
- 一般应用在与用户交互不多对STW要求不是特别高的后台计算程序上(如批处理,科学计算等)
- 可启用GC自适应调节策略
GC自适应调节策略
- JVM运行时根据系统运行情况自动调整 -Xmn,-XX:SurvivorRation,-XX:PretenureSizeThreshold
等参数 - 通过参数(UseAdptiveSizePolicy )开启,启用后只需要设置好 -Xmx 和目标吞吐量即可,其他运行时细节调整均由JVM负责完成
Paralle Old
- 多线程的 Serial Old,同样关注的是吞吐量
常用参数
参数 | 描述 | 备注 |
---|---|---|
-XX:+UseParallelGC | 启用 Parallel Scavenge GC | |
-XX:+UseParallelOldGC | 启用 Parallel Old GC | |
-XX:MaxGCPauseMillis | GC最大STW时间(ms),JVM尽量保证每次GC时间不超过该参数值 | 与Paralle的吞吐量目标背道而驰,不建议使用 |
-XX:GCTimeRatio | 目标吞吐量(0<N<100),默认:99 | 99%的UPT |
-XX:+UseAdptiveSizePolicy | 启用GC自适应调节策略 |
CMS(Concurrent Mark Sweep,JDK1.9被标记废弃)
- 并发标记清除GC,年轻代和老年代采用不同的GC算法,以尽可能短的STW为设计目标
- 年轻代GC近似ParNew
- 老年代GC主要由初始标记、并发标记、重新标记,并发清除等阶段完成。初始标记、重新标记阶段是STW的
- CMS不会直接压缩内存,需配置参数启用并设置压缩周期,否则易触发Full GC
老年代GC
1,初始标记(STW)
- 单线程标记与GC Roots、年轻代对象直接关联的老年代对象
- JDK1.8后可启用多线程并行(-XX:+CMSParallelInitialMarkEnabled)
2,并发标记
- 并行标记所有可达的对象
- 由于不是STW的,对象的可达性可能在被标记后发生变更
3,预清理(二次并发标记)
- 重新标记可达性发生变更的对象
4,可中断预清理
- 尽可能多的执行Minor GC,以降低重新标记阶段STW的时间
阶段跳过
Eden使用空间 和 使用率均超过阈值
参数 | 描述 | 备注 |
---|---|---|
-XX:CMSScheduleRemarkEdenSizeThreshold | Eden使用空间阈值,默认:2MB | |
-XX:CMSScheduleRemarkEdenPenetration | Eden使用率阈值,默认:50 | % |
阶段退出
超过Minor GC执行次数 或 超过最大执行时间
参数 | 描述 | 备注 |
---|---|---|
-XX:CMSMaxAbortablePrecleanLoops | Minor GC执行次数阈值,默认:0 | 不限次数 |
-XX:CMSMaxAbortablePrecleanTime | 阶段最大执行时间,默认:5000 | ms |
5,重新标记(STW)
- 重新标记年轻代、GC Roots、并发标记阶段发生变更的对象
参数 | 描述 | 备注 |
---|---|---|
-XX:+CMSScavengeBeforeRemark | 执行重新标记前,至少执行1次Minor GC | 建议启用 |
6,并发清除
- 清除所有不可达的对象
7,并发重置
- 重置CMS内部状态、数据结构,等待下一次触发
Full GC
- 在CMS执行周期内,老年代、方法区内存空间不足时会触发Full GC
并发模式失败(Concurrent mode failure)
- 老年代内存空间不足,无法分配大对象、新晋升的年轻代对象时抛出
晋升失败(Promotion failed)
- 执行Minor GC时,对象晋升至老年代,但老年代内存不足
- 抛出并发模式失败错误
常用参数
参数 | 描述 | 备注 |
---|---|---|
-XX:+UseConcMarkSweepGC | 启用CMS GC | |
-XX:ParallelGCThreads | 设置年轻代GC线程数 | 参考ParNew |
-XX:ParallelCMSThreads | 设置老年代GC线程数,默认:≈1/4 年轻代GC线程数 | (ParallelGCThreads + 3) / 4 |
-XX:+CMSParallelInitialMarkEnabled | 初始标记启用多线程并行 | > JDK1.8 |
-XX:CMSInitiatingOccupancyFraction | 老年代内存使用比例超过参数值时触发GC,默认:92 | % |
-XX:+UseCMSInitiatingOccupancyOnly | 启用动态检查 | |
-XX:+UseCMSCompactAtFullCollection | 启用Full GC后内存压缩功能 | |
-XX:+CMSFullGCsBeforeCompaction | 执行N次Full GC后,执行一次内存压缩,默认:0 | 每次都压缩 |
动态检查
- JVM根据最近的回收历史,估算下一次老年代内存耗尽的时间
- 接近估算时间时触发GC
G1(Garbage First,JDK1.9默认)
- 目标(STW时间,MaxGCPauseMillis)导向的分区复制GC,年轻代和老年代采用不同的GC算法
- 移除了年轻代、老年代物理上的内存划分
- 年轻代GC近似ParNew
- 老年代GC由并发周期、混合GC两阶段完成,整堆(年轻代+老年代)内存使用率超过阈值(InitiatingHeapOccupancyPercent)时触发
- 并发周期采用SATB(逻辑快照)算法降低重新标记阶段的耗时
- G1的GC Roots对象选取与其他GC不同(详见根分区扫描)
G1模型
分区(Region)
- 默认将内存划分为2048个大小相等的分区,可设置参数(G1HeapRegionSize)调整分区大小
- 分区不明确服务于年轻代、老年代,G1可动态调整
参数 | 描述 | 备注 |
---|---|---|
-XX:G1HeapRegionSize | 分区大小,1MB ~ 32MB之间 | 必须是2的幂次方 |
卡片(Card Table)
- 每个分区划分为多个512B的卡片,卡片为内存最小可用粒度
- 所有卡片由Card Table字节数组统一维护
RSet(Remembered Set)
例:Old分区R1内对象A,引用了分区R2内对象B。则分区R1的RSet同时记录A引用B
- 记录老年代分区引入对象的信息
- 老年代分区标记时扫描RSet,降低耗时
巨型对象(Humongous Object)
- 超过分区大小50%的对象被认定为巨型对象,直接分配在老年代
- 超过分区大小的巨型对象分配在连续的多个分区上(开始巨型 + 连续巨型 * N),且独占分区
- 连续分区不足时,触发Full GC
CSet(Collection Set)
- 清理阶段记录需要回收的分区,供混合GC阶段使用
- 存活对象占比大于阈值(G1MixedGCLiveThresholdPercent)则不记录CSet
参数 | 描述 | 备注 |
---|---|---|
-XX:+G1MixedGCLiveThresholdPercent | 设置不记录CSet存活对象占比阈值,默认:85 | JDK1.6、1.7,默认:65 |
SATB(Snapshot-At-The-Beginning,逻辑快照算法)
粗浅介绍,了解其大概用意。详细算法逻辑(P + N两段标记、bitmap、内存屏障等)可自行了解
- 逻辑不可变快照,假定快照内容不会变更
- 在分区末尾插入TAMS标记,并发标记阶段仅标记TAMS前的对象
- TAMS后分配 和 TAMS前发生变更的对象,通过日志将对象旧值记录在SATB缓冲区内
三色标记
黑色:已被标记可达的对象(如根对象)
灰色:黑色对象直接引用但其分叉尚未扫描完成的对象
白色:尚未被标记的对象(并发周期结束,仍是白色的则是可回收对象)
并发周期
- 标记出可被垃圾回收的老年代对象
- 主要由初始标记、并发标记、重新标记,清理等阶段完成
- 初始标记、重新标记、清理阶段是STW的
初始标记(initial-mark,STW)
- 设置SATB的TAMS标记
- 由于需要STW遂与年轻代GC合并,初始标记前先执行Minor GC
根分区扫描(root-region-scan)
- 由于已执行过Minor GC,Survivor To内都是强可达对象,将其全部设置为GC Roots对象
- 标记所有GC Roots直接引用的对象
- 该阶段禁止Minor GC
并发标记(concurrent-mark)
- 并行标记所有可达的对象
- 过程中发生变更的对象记录在SATB缓冲区内
重新标记(remarking,STW)
- 重新标记SATB缓冲区内的对象
清理(cleanup,STW)
- 将需回收分区记录CSet,并按存活度(存活对象占比)升序排列
- 直接回收巨型对象
- 空闲分区识别(直接释放整个分区并加入到空闲队列)
- 将无用的类从Metaspace(元数据区、方法区)中卸载
混合GC(Mixed GC,混合收集,STW)
- 执行多次(G1MixedGCCountTarget),每次执行回收后将分区内存活对象复制到另一个空闲的分区上
- Minor GC与CSet回收同时进行,直到CSet被全部(几乎)回收为止
- 回收结束后,恢复正常年轻代GC
- 可回收对象百分比小于堆废物百分比(G1HeapWastePercent),则跳过混合GC
参数 | 描述 | 备注 |
---|---|---|
-XX:G1MixedGCCountTarget | 设置执行混合GC次数,默认:8 | 若混合GC单次STW时间过长,可增加该参数 |
-XX:G1HeapWastePercent | 设置堆废物百分比,默认:5 |
疏散失败
- Minor GC清除完成时,没有足够的空闲空间复制对象
常用参数
参数 | 描述 | 备注 |
---|---|---|
-XX:+UseG1GC | 启用G1 GC | |
-XX:MaxGCPauseMillis | 设置最大STW时间,越大GC频率越低但吞吐量也会下降,默认:200ms | JVM会根据目标值动态调整内存模型 |
-XX:InitiatingHeapOccupancyPercent | 设置并发周期触发百分比,默认:45(%) | 一轮GC结束后,堆使用空间小于该值以最优 |
-XX:ParallelGCThreads | 设置年轻代GC线程数 | 参考ParNew |
-XX:ConcGCThreads | 设置老年代GC线程数,默认:≈1/4 年轻代GC线程数 | (ParallelGCThreads + 3) / 4 |
-XX:G1ReservePercent | 设置整堆预留空闲空间百分比,降低疏散失败风险,默认:10 | % |
G1调优
- 在避免疏散失败和Full GC的前提下,以尽可能短的STW提供较高的吞吐量
1,不要设置Xmn、NewRaio参数,会导致目标(MaxGCPauseMillis)失效
2,主要通过目标(MaxGCPauseMillis)作为调优手段,90%的GC可以达到该时间为最优
3,提高并行线程数(ConcGCThreads),加快GC速度,但CPU压力也会随之增加
4,降低并发周期触发百分比(InitiatingHeapOccupancyPercent),提前启动GC
5,提高CSet对象存活占比阈值(G1MixedGCLiveThresholdPercent),使CSet记录更多的分区以提高GC的效果
6,提高混合GC执行次数(G1MixedGCCountTarget),降低单次执行的STW时长
ZGC(Z Garbage Collector,> JDK11)
- 升级版的G1
- STW不超过10ms(128G内存,STW 1.68ms)
- 支持Numa架构
- 目前仅支持64位的Linux系统
GC示例图
Minor GC(年轻代GC)
Copy To(复制)
Promotion(晋升)
- 示例年龄:9,JVM默认:15
Major GC(老年代GC)
Mixed GC(混合GC)
- 同时回收年轻代、老年代
方法区回收
- 非即时的回收废弃常量、无用的类
- 设置参数(ClassUnloadingWithConcurrentMark)启用动态代理类即时回收
参数 | 描述 | 备注 |
---|---|---|
-XX:+ClassUnloadingWithConcurrentMark | 启用动态代理类即时回收(自定义类加载器本身被回收时回收其加载的类) | JDK8u40后默认启用 |
废弃常量
- 运行时常量池内无任何引用的字面量
无用的类
- 类所有实例已被回收
- 类加载器已被回收
- 没有任何地方访问类的Class对象
逃逸分析(Escape Analysis)
- 通过逃逸分析,确认不会逃逸的对象直接在线程栈上分配(Java对象默认分配在堆上)
- 线程结束的同时销毁对象,降低GC压力
方法逃逸
- 方法内定义的对象,作为返回参数逃逸出方法的作用域
线程逃逸
- 对象赋值给线程共享的变量