并发标记是主流垃圾回收器提升标记效率的核心机制,标记过程中业务线程仍在运行,会不断修改对象的引用关系,此时读写屏障会记录引用变更保证标记正确性,但高频的引用修改会让屏障产生大量额外开销,拖慢整体性能。
读写屏障与并发标记的基础原理
并发标记阶段,GC线程和业务线程同时工作,业务线程修改对象引用时,可能会让已经标记过的对象被遗漏,或者把不该回收的对象错误回收。读写屏障就是用来解决这个问题的机制:
- 读屏障:在业务线程读取对象引用时触发,通常用于SATB(快照 at the beginning)标记算法,记录读取时的引用快照
- 写屏障:在业务线程修改对象引用时触发,通常用于增量更新标记算法,记录引用变更的新旧值
不管是读屏障还是写屏障,每次触发都需要执行额外的逻辑,比如将变更记录到SATB队列或者卡表中,高频的引用修改会让这部分开销成倍增长。
问题特征识别
首先可以通过GC日志和监控指标判断是否存在这类问题:
GC日志特征
开启GC详细日志后,重点关注并发标记阶段的相关指标:
- 并发标记阶段的耗时远高于同配置下的基准值
- 日志中出现大量
Update_Remap、Scan_SATB_Buffer之类的屏障相关日志 - SATB缓冲区或者卡表缓冲区频繁被填满,触发额外的处理线程
监控指标特征
应用层面的监控会出现以下表现:
- 业务接口的响应时间波动增大,尤其是大对象操作相关的接口
- GC相关的CPU占用率升高,且大部分消耗在屏障处理逻辑上
- 老年代对象的引用变更频率远高于新生代
分步排查流程
第一步:统计屏障触发频率
可以通过JVM自带的参数或者arthas等工具统计屏障触发次数,以G1垃圾回收器为例,开启以下参数可以输出屏障相关的统计信息:
# 开启G1的屏障统计日志 -XX:+G1PrintSATBBufferReuseStats -XX:+PrintGCDetails # 输出屏障触发的详细次数 -XX:+UnlockDiagnosticVMOptions -XX:+PrintBarrierStats
拿到日志后统计单位时间内读屏障和写屏障的触发次数,对比正常场景下的数值,如果高出5倍以上就可以确定存在高频引用修改问题。
第二步:定位高频修改的业务线程
可以通过线程栈采样的方式找到频繁执行引用修改操作的线程,使用async-profiler工具进行采样:
# 采样30秒的线程栈,重点关注屏障相关的栈帧 ./profiler.sh -d 30 -e wall -f barrier_profile.html 进程ID
在生成的火焰图中,搜索write_barrier或者read_barrier相关的栈帧,向上追溯业务代码,就能找到触发屏障的业务逻辑。
第三步:分析引用修改的具体逻辑
找到对应的业务代码后,分析对象引用修改的场景,常见的高频修改场景有:
- 全局缓存中频繁更新大对象的引用,比如本地缓存的put、remove操作
- 消息队列消费时频繁修改共享对象的引用关系
- 批量处理数据时,循环内不断修改对象间的引用
可以通过以下代码示例模拟高频引用修改的场景:
import java.util.ArrayList;
import java.util.List;
public class ReferenceModifyDemo {
// 模拟全局共享的缓存对象
private static final List<Object> GLOBAL_CACHE = new ArrayList<>();
public static void main(String[] args) {
// 高频修改引用关系,触发大量写屏障
for (int i = 0; i < 1000000; i++) {
Object newObj = new Object();
// 修改全局缓存的引用
GLOBAL_CACHE.add(newObj);
if (GLOBAL_CACHE.size() > 100) {
GLOBAL_CACHE.remove(0);
}
}
}
}
优化方案
定位到问题后可以根据场景选择对应的优化方式:
- 如果是缓存场景,可以改用弱引用或者软引用的缓存实现,减少强引用修改的频率
- 如果是批量处理场景,可以将引用修改的操作合并,减少单次循环中修改引用的次数
- 如果是共享对象场景,可以对修改操作加锁,或者改用线程本地对象,避免多线程同时修改共享引用
- 如果业务允许,可以调整GC参数,比如增大SATB缓冲区的大小,减少缓冲区填满的次数:
# 增大G1的SATB缓冲区大小 -XX:G1SATBBufferSize=1024
验证优化效果
优化后重新运行应用,对比优化前后的指标:
- 并发标记阶段的耗时是否下降
- 屏障触发频率是否降低到合理范围
- 业务接口的响应时间是否恢复正常
如果指标都符合预期,说明优化生效,否则需要重新回到排查流程,检查是否有遗漏的高频修改场景。