一、为什么要理解 GC
Java 程序员不需要手动释放内存,但一旦出现Full GC 频繁、STW 时间过长的问题,如果不理解垃圾回收原理,就只能靠猜。
一次典型的故障:某订单服务在大促期间 RT 从 50ms 飙升到 3s,监控显示每分钟触发 2~3 次 Full GC。
二、分代假说
JVM 基于两个分代假说设计堆结构:- 弱分代假说:绝大多数对象都是朝生夕死的
- 强分代假说:熬过越多次 GC 的对象越难消亡
// 查看当前 JVM 使用的垃圾收集器
java -XX:+PrintCommandLineFlags -version
// JDK8 默认:Parallel Scavenge + Parallel Old
// JDK17 默认:G1
三、四种收集器对比
| 收集器 | 算法 | 线程 | STW | 适用场景 | | --- | --- | --- | --- | --- | | Serial | 复制/标记整理 | 单线程 | 长 | 客户端、小内存 | | Parallel | 复制/标记整理 | 多线程 | 长 | 吞吐量优先 | | CMS | 标记清除 | 并发 | 短 | 低延迟(已废弃)| | G1 | Region + 标记整理 | 并发 | 可控 | 大堆、低延迟 |四、判断对象存活
主流使用可达性分析而非引用计数,GC Roots 包括:- 虚拟机栈中引用的对象
- 方法区静态属性、常量引用的对象
- JNI 引用的对象
public class GCRootsDemo {
private static Object staticObj = new Object(); // 静态属性:GC Root
public void method() {
Object local = new Object(); // 局部变量:GC Root
// 方法结束后 local 不再可达
}
}
五、G1 调优实战
针对开篇的故障,最终参数如下:-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/data/logs/gc.log
关键改动:
- 固定堆大小:避免动态扩缩容带来的抖动
- IHOP 从 45 下调:提前启动并发标记,避免退化成 Full GC
- 暂停目标 200ms:G1 会自动调整年轻代大小来满足目标
六、效果
| 指标 | 调优前 | 调优后 | | --- | --- | --- | | Full GC 次数/小时 | 150 | 0 | | 平均 RT | 320ms | 48ms | | P99 RT | 3.1s | 210ms |小结
GC 调优不是玄学,核心是减少对象晋升到老年代的速度并给并发标记留出足够时间。先通过jstat 和 GC 日志定位问题,再针对性调参,切忌直接抄网上的"万能参数"。
评论(3)