如何判断对象可以回收
- 引用计数法
- 可达性分析算法
- Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着GC ROOT对象为起点的引用链找到该对象,找不到,可以回收
- 那些对象可以作为GC ROOT
- 当前正在运行的方法栈参数、静态变量、常量以及本地方法(Native)所引用的对象,都可以作为 GC Roots。
- 四种引用
- 强引用:从不回收,除非不可达
- 软引用:内存不足时(图片缓存)
- 弱引用:只要 GC 扫描到就回收
- 虚引用:任何时候都可能被回收
- 终接器引用:第一次入队,第二次再次判断引用类型,判断是否删除
软引用、弱引用引用的对象被回收后会放入引用队列,之后判断是否释放
回收算法
- 标记+清除
- 速度快
- 内存碎片
- 标记+整理
- 速度慢
- 没有内存碎片
- 复制:划分两个区域,一个一直为空,一个用,发生GC后复制数据到另一侧
- 没有内存碎片
- 需要占用双倍内存空间
分代回收
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加1并且交换from和to
- minor gc会引发stop the world(STW),暂停其他用户线程,等垃圾回收结束用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升到老年代,最大寿命是15,当幸存区内存紧张,会提前进入老年代
- 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍然不足,那么触发full gc,STW时间更长
当对象寿命达到15,就会晋升到老年代
当新生代和老年代空间都不足时会触发Full GC(整个清理)
相关VM参数
查表
子线程的out of memery 不会导致主线程结束
垃圾回收器
- 串行(-XX+UseSerialGC=Serial+serialOld)
- 单线程
- 堆内存较小,适合个人电脑
- 吞吐量优先(-XX:+UseParallelGC -XX:+UseParallelOldGC)
- 多线程
- 堆内存较大,多核cpu
- 让单位时间内STW的时间最短
- 响应时间优先(-XX:UseConcMarkSweepGC -XX:+UseParNewGC SeriaOld)
- 多线程
- 堆内存较大,多核cpu
- 尽可能让单次STW的时间最短


串行参数解释
Serial:工作在新生代(复制算法)
SerialOld:工作在老年代(标记+整理)
吞吐量参数解释
parallel:并行(垃圾回收线程执行,用户线程阻塞)
useParallelGC:新生代,并行垃圾回收
useParallelOldGC:老年代
响应时间参数解释
concurrent:并发(用户线程和垃圾回收线程同时执行)
初始标记(用户线程阻塞)->并发标记(用户线程运行)->重新标记(并行)->清理(用户线程运行)
CMS可能并发失败,此时退化为串行垃圾回收器
CMS是作用于老年代的一种并发垃圾回收器(标记+清除)
浮动垃圾:并发运行时产生的垃圾
需要预留空间保留浮动垃圾
-XX:CMSInitiatingOccupancyFraction=percent
在并行标记时,先进行新生代垃圾回收
-XX:+CMSScavengeBeforeRemark
CMS 退化为 Full GC 主要有两种情况,一种是由于采用标记-清除算法导致内存碎片严重,大对象分配失败触发 Full GC;另一种是在并发标记期间产生大量浮动垃圾,导致回收速度跟不上分配速度,从而发生并发失败,最终退化为 Full GC。
G1垃圾回收器
适用场景:
- 同时注重吞吐量和低延迟,默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的region
- 整体上是标记+整理算法,两个区域之间是复制算法
1.8要开启 -XX:+UseG1GC
1.9之后为默认
G1垃圾回收阶段
G1 的垃圾回收主要分为 Young GC、并发标记和 Mixed GC 三个阶段。Young GC 会暂停用户线程,采用复制算法回收新生代对象。
当堆使用率达到一定阈值时,会触发并发标记,初始标记阶段会借助一次 Young GC 完成,随后进行并发标记和最终标记(Remark)。
在标记完成后,G1 会执行 Mixed GC,在回收新生代的同时选择部分回收价值较高的老年代 Region,而不是全部回收。
回收老年代不会全部回收,会根据暂停目标选择回收价值最高的区域
当垃圾回收速度<垃圾产生速度
会并发失败,退化为串行垃圾回收器
Young Collection跨代引用问题
老年代引用新生代问题
遍历老年代找到GC root很慢
解决:卡表(标记GC root脏卡)
- 卡表与Remembered Set
- 在引用变更时通过post-write barrier+dirty card queue
- concurrent refinement threads 更新remembered Set
卡表:记录"哪里发生了引用变化"
RSet:记录"这些变化中,谁引用了我"
卡表用于记录堆中哪些 Card 发生了引用修改,而 RSet 则以 Card 为粒度,记录来自其他 Region 的哪些 Card 可能包含指向当前 Region 的引用。
Remark
初始标记:标记的是 GC Roots 直接指向的活对象。
并发标记:从初始标记出的对象开始,沿着引用链向下追踪,标记所有可达的对象。
Remark:标记的是 在并发期间被遗漏或新产生的活对象。
当引用被覆盖时,把"旧引用指向的对象"放入队列,并在 remark 阶段进行补标
总结:
在 G1 的并发标记过程中,当对象引用被覆盖时,写屏障会将旧引用对象记录到 SATB 队列中。在 remark 阶段,GC 会处理该队列,对其中对象进行重新标记(补标),以保证在标记开始时仍然可达的对象不会被遗漏。标记完成后,未被标记的对象才会在后续阶段被回收。
CMS 在并发标记阶段采用三色标记算法,为了避免黑对象指向白对象导致的漏标问题,引入了增量更新机制。当引用发生变化时,写屏障会将黑对象重新标记为灰色并加入队列,在重新标记阶段对这些对象重新扫描,从而保证当前对象图的正确性。
| CMS | G1 | |
|---|---|---|
| 思想 | 增量更新 | SATB |
| 关注点 | 新引用(黑→白) | 旧引用(被删除) |
| 写屏障记录 | 黑对象本身 | 旧引用对象 |
| 是否重扫 | ✅ 必须重扫 | ❌ 不需要 |
| 是否允许黑→白 | ❌ 不允许 | ✅ 允许 |
| 是否可能漏标 | ❌(修正后) | ❌ |
| 是否产生浮动垃圾 | 少 | 多 |
三色标记法:从 GC Roots 出发,如何一步步遍历整个对象图
在并发情况下如何保证不漏标:就是上面两个方法(增量更新(CMS)、SATB(G1))
CMS:
初始标记(STW)
并发标记
重新标记(STW)
并发清除
G1:
Initial Mark(STW,借助 Young GC)
并发标记
Remark(STW)
Cleanup
Mixed GC(STW,复制存活对象)
JDK 8u20字符串去重
优点:节省大量内存
缺点:略微多占用cpu时间,新生代回收时间略微增加
因为字符串是不可变对象,因此堆中字符串对象值相同,我们可以去重方(指向同一个对象)
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复
- 如果他们值一样,让他们引用用一个char[]
- 注意,与String.intern()不一样
- String.intern()关注的是字符串对象
- 而字符串去重关注的是char[]
- 在JVM内部,使用了不同的字符串表
JDK 8u40并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
默认开启
JDK 8u60回收巨型对象
- 一个对象大于region的一半时,称之为巨型对象
- G1不会对巨型对象进行复制
在 G1 中,巨型对象的回收主要依赖并发标记阶段判断其可达性,一旦不可达,在后续阶段会整体释放对应的 Humongous Region
JDK9并发标记起始时间的调整
- 并发标记必须在老年代空间不足前完成,否则退化为fullGC
- JDK9之前需要使用-XX:InitiatingHeapOccupancyPercent
- JSK9可以动态调整
- -XX:InitiatingHeapOccupancyPercent来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空挡空间
JDK9更高效的回收
看oracle官方文档
GC调优
常见调优场景
- 内存
- 锁竞争
- cpu占用
- io
代码问题
查看fullgc前后内存占用,考虑以下几个问题
- 数据是不是太多
- resultSet=statement.executeQuery("select * from 大表")
- 数据表示是否太臃肿
- 对象图
- 对象大小Integer 16字节 int 4字节
- 是否存在内存泄露
- 软、弱引用
- 第三方缓存实现
新生代调优
- 新生代的特点
- 所有的new操作的内存分配非常廉价
- TLAB thread-local allocation buffer
- 死亡对象的回收对象是零
- 大部分对象用过即死
- Minor GC的时间远低于Full GC(相差1-2个数量级)
- 所有的new操作的内存分配非常廉价
新生代越大越好吗:
如果内存大小确定,新生代太大导致老年代较小,触发fullGC概率大,建议占用堆内存的25%-50%
新生代能容量所有 {并发量*(请求-响应)} 的数据
幸存区大到能保留{当前活跃对象+需要晋升对象}
晋升阈值配饰得当,让长时间存活对象尽快晋升
老年代调优
CMS为例
- CMS的老年代内存越大越好(避免浮动垃圾导致并发失败)
- 先尝试不做调优,如果没有full GC那么已经...,否则先尝试调优新生代
- 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3
- -XX:CMSInitiatingOccupancyFraction=precent
案例
- 案例一:Full GC和Minor GC 频繁
新生代可能太小 - 案例二:请求高峰期发生Full GC ,并且暂停时间特别长(CMS)
重新标记耗费时间太长,可以在FullGC之前触发MinorGC - 案例三:老年代充裕情况下,发生Full GC(JDK1.7 CMS)
1.7之前,采用永久代作为方法区,永久代空间不足也会导致full GC