对象判定与 GC 算法
用途:理解“为什么这个对象没被回收”“GC 停顿从哪来”
存活判定
「存活判定」小节链接HotSpot 使用可达性分析:从 GC Roots 出发不可达的对象即可回收。GC Roots 包括:虚拟机栈中的引用、静态变量、常量、类元数据、JNI 引用、活跃线程、被锁持有的对象等。
| 概念 | 现状 |
|---|---|
| 引用计数 | 不作为主判定(无法处理循环引用),但部分实现用于辅助(如卡表/记忆集优化) |
| finalize | 已废弃(JDK 18 起被标记 for removal),不要依赖 |
| 弱/软/虚引用 | 缓存(软)、映射键(弱)、资源清理通知(虚);回收时机不确定,不能当业务清理机制 |
三种基本算法
「三种基本算法」小节链接| 算法 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 标记-清除 | 标记存活后回收空洞 | 实现简单 | 碎片化,分配需整理 |
| 标记-复制 | 存活对象整体拷到另一块 | 无碎片、分配指针碰撞快 | 空间利用率低,存活率高时代价大 |
| 标记-整理 | 标记后把存活对象向一端移动 | 无碎片、空间利用率高 | 移动成本高,需要更新引用 |
分代假说是这些算法的组织依据:大多数对象朝生夕死(弱分代假说),因此年轻代用复制(存活少、拷贝便宜),老年代用标记-整理或标记-清除(存活多、移动贵)。
停顿从哪里来
「停顿从哪里来」小节链接- STW 阶段:根扫描、引用处理、整理等需要暂停的时刻。
- 写屏障与卡表维护:跨代引用需要记录,屏障成本落在应用线程。
- 内存分配压力:Eden 太小导致 Young GC 频繁;晋升速率波动导致 Mixed/Full GC。
- 大对象与 humongous 分配:直接进老年代或触发整理。
- 元数据与类加载:Metaspace GC 阈值调整、卸载类。
- 堆外与引用队列:
Reference处理、Cleaner执行时机。
观测要点
「观测要点」小节链接开启统一 GC 日志:-Xlog:gc*,gc+heap=debug,safepoint:file=gc.log:time,uptime,level,tags。看四件事:GC 频率、每次停顿时长(P99 而非平均)、晋升速率、堆使用曲线的锯齿形态(回收后基线是否持续抬升=泄漏迹象)。
常见误区
「常见误区」小节链接| 误区 | 修正 |
|---|---|
| 内存没满就不会 Full GC | 晋升失败、元空间阈值、显式 System.gc()(RMI 等场景)都会触发 |
用 System.gc() 整理内存 |
触发全堆停顿,应禁用(-XX:+DisableExplicitGC)并改用正确参数 |
| 认为软引用能兜住缓存 | 回收时机不可控,缓存需要显式容量与淘汰策略 |
| 只看堆大小不看分配速率 | 分配速率才是 Young GC 频率的主因 |
| 停顿长就是 GC 问题 | 也可能是安全点等待、应用线程在分配路径被阻塞 |
延伸阅读
「延伸阅读」小节链接- 《深入理解 Java 虚拟机》第 3 章
- OpenJDK HotSpot GC Tuning 文档
- Memory Management Glossary(Richard Jones《The Garbage Collection Handbook》)