深入理解G1垃圾回收器:GC Roots与标记机制的"人性化"探秘
G1(Garbage-First)垃圾回收器是Java世界的"清道夫",而GC Roots和标记机制则是它的"灵魂"。想知道JVM如何判断哪些对象该留、哪些该扔?跟着我一起走进这场垃圾回收的"侦探游戏",从GC Roots的"根源"到标记的"追踪术",带你轻松get核心!
一、GC Roots的本质:垃圾回收的"生命线"
GC Roots是垃圾回收的"根集合",就像一棵参天大树的根,所有的"活对象"都得靠它们抓住生命线。理解GC Roots的精髓,就是搞清楚哪些引用是从"堆外"伸进来的"救命稻草"。
先从反面入手:什么不是GC Roots?
有时候,排除法比直接定义更直观。我们先把"伪装者"揪出来:
- 堆里的普通对象 :比如
new String()
,它得有人"罩着"才能活。 - 没人管的孤儿:没被任何根引用的对象,注定被回收。
- 非静态成员变量:这些是对象的"跟班",自己没资格当根。
- 挂掉的线程:线程一结束,它手里的东西也"随风而去"。
GC Roots的"四大金刚"
筛选掉杂牌军后,真正的"根"现身了,它们是:
-
线程栈上的引用
- 包括:方法里的局部变量、参数、临时变量。
- 特点:像临时工,方法调用栈一变,它们就"下岗"。
- 例子 :
void foo() { User user = new User(); }
------user
是个根,但方法一结束,它就"下班"了。
-
静态变量与常量
- 包括 :
static
字段、final
常量。 - 特点:像老员工,和类"同生共死"。
- 例子 :
static Config globalConfig;
------ 只要类在,它就在。
- 包括 :
-
JNI引用
- 包括:Native代码(比如C++)里抓着的Java对象。
- 特点:跨语言的"外交大使",地位特殊。
- 例子 :
JNIEnv->NewObject()
------ 从C世界伸来的"援手"。
-
系统保留对象
- 包括 :类对象(
Class
)、活跃线程、系统类加载器。 - 特点:JVM的"核心资产",少了谁都不行。
- 例子 :
Object.class
、Thread.currentThread()
------ 这些是大佬级别。
- 包括 :类对象(
这些"根"就像城市的"水源地",所有的对象都得通过它们"喝到水"才能存活。
二、初始标记:快如闪电的"起跑枪"
初始标记是G1的第一个"暂停"(STW),就像赛跑前的发令枪,目标是迅速标记GC Roots的"直系亲属"。
它有多"快刀斩乱麻"?
- 只扫GC Roots直接抓着的对象,至于"七大姑八大姨",留给后面处理。
- 不追踪完整的引用链,效率优先。
- 经常搭Young GC的"顺风车",省时又省力。
举个小例子
java
class Graph {
static Node root = new Node(); // GC Root
static class Node {
Node next;
Node() { next = new Node(); }
}
}
初始标记只管root
,至于root.next
或者更远的"亲戚",它才不费那个心思呢。
为什么这么快?
- 暂停时间:通常1ms以内,快得像眨眼间的事。
- 并行加速:多线程一起上,像"扫地僧"集体出动。
- 位图妙招:用Bitmap记下标记状态,效率高得飞起。
三、并发标记:织出一张"存活大网"
初始标记只是开了个头,接下来是并发标记------它像个"名侦探",从GC Roots出发,顺着线索追踪,画出完整的"存活地图"。
它的"侦探术"有哪些?
-
顺藤摸瓜
- 从GC Roots开始,深度优先搜索,把所有能找到的"活物"标记出来。
-
SATB算法(时间快照)
- 在标记开始时拍个"全家福",保证画面不乱。
- 用写屏障(Write Barrier)记下中途的"捣乱者",确保不漏线索。
-
脏卡记事本
- 引用变了?记在"脏卡表"(Card Table)里,回头再查。
幕后工具箱
工具名 | 干啥用的? | 咋实现的? |
---|---|---|
标记位图 | 标记谁还活着 | 每个Region一张"名单" |
标记栈 | 存着待查的线索 | 像个"待办清单" |
脏卡表 | 记下中途的"变动" | 字节数组当"记事本" |
这阶段不打扰程序,像个默默工作的"助手",一边标记一边让应用继续跑。
四、重新标记与清理:查漏补缺的"收尾大师"
重新标记:再来一次"体检"
并发标记难免有"漏网之鱼",重新标记就暂停一下(STW),处理脏卡里的变化,补上漏标、修好错标,确保万无一失。
清理:收拾"战场"
- 摸底存活率:看看每个Region还有多少"活口"。
- 组回收队:挑出要清理的区域(CSet)。
- 更新通讯录:刷新跨区域引用的"记账本"(Remembered Set)。
- 大扫除:完全没用的Region直接"清空"。
五、实战小妙招:轻松记住GC Roots
口诀助攻
"栈静JNI系统"(读起来像"站静JNI系统"):
- 栈:线程栈引用
- 静:静态变量
- JNI:本地方法引用
- 系统 :系统保留对象
念几遍,保证过目不忘!
避坑小贴士
- 误区 :所有线程都是GC Roots?
真相:只有活线程算,挂了的线程不算数。 - 误区 :静态变量引用的对象是GC Root?
真相:静态变量是根,它指向的对象只是"被罩着"而已。
六、总结:G1标记的"精髓提炼"
- GC Roots是命根子:四大类型,从堆外来的"定海神针"。
- 标记分步走:初始标记快如风,并发标记织大网,重新标记补漏洞。
- 技术亮点:SATB锁住一致性,写屏障撑起并发。
- 效率秘诀:用最短的暂停换最大的成果。
搞懂这些,你就能"摸透"G1的脾气,下次调优时胸有成竹。垃圾回收原来也可以这么有趣,对吧?下回我们再聊聊G1的回收阶段,继续这场"探秘之旅"!