当前位置: 首页 > news >正文

GC Roots与可达性分析——对象是如何被标记存活的?

GC Roots与可达性分析——对象是如何被标记存活的?

前言

“对象不再被引用时就会被回收”——这句话我们听了无数遍,但JVM到底怎么判断“不再被引用”?

是数一数有多少引用指向它(引用计数法),还是从根出发找一找还能不能找到它(可达性分析法)?Java选择了后者。

但更微妙的问题是:一个对象明明代码里已经不使用了,为什么有时候还是没被回收?这就涉及局部变量表与内存泄漏的经典陷阱。

本文将从GC Roots出发,带你彻底理解可达性分析法,以及如何避免“意外”的内存泄漏。


一、可达性分析法的本质

1.1 核心原理

可达性分析法:JVM从一个根集合(GC Roots)出发,通过引用关系遍历对象图。所有能被遍历到的对象,就被标记为“存活”;遍历结束后,未被标记的对象就是“可回收”的垃圾。

这个过程可以想象成一张有向图

  • 对象是图中的节点
  • 引用关系是图中的边
  • GC Roots是图的入口节点

1.2 一个生动的例子:链表

classNode{Nodenext;}publicvoidtest(){Nodehead=newNode();// 头节点head.next=newNode();// 第二个节点head.next.next=newNode();// 第三个节点// 方法执行中...}

可达性分析的过程

  1. 从GC Roothead(局部变量)出发
  2. 沿着head.next找到第二个节点
  3. 沿着head.next.next找到第三个节点
  4. 三个节点全部标记为存活

如果中间断了:

head.next=null;// 中间引用断开

结果:第二个和第三个节点从GC Roots无法到达,被标记为垃圾。


二、GC Roots到底是什么?

你可以把GC Roots想象成程序运行的**“起点”“全局抓手”**。只有从这些点出发能找到的对象,才是有用的。

2.1 五种GC Roots

publicclassGCRootsExample{// 1. 静态变量(在方法区中)// 只要类GCRootsExample未被卸载,t1指向的对象就永远可达privatestaticTestt1=newTest();// 2. 常量池中的引用(也是方法区)privatestaticfinalTestt2=newTest();publicvoidmethod(){// 3. 局部变量表(在虚拟机栈帧中)// 当method()正在执行时,这两个变量就是GC RootsObjectobj=newObject();Stringstr="hello";// 4. 活跃的Java线程// 线程启动后,它自己就是“活的”Threadthread=newThread(()->{while(true){// 这个线程内部的对象通过线程本身可达}});thread.start();// obj 和 str 在方法执行完出栈后,就不再是GC Roots了}publicstaticvoidmain(String[]args){// 5. 方法参数(也是局部变量)// main方法执行期间,args是GC RootGCRootsExampleexample=newGCRootsExample();example.method();// example对象被main方法的局部变量引用,所以存活}// 6. JNI引用(本地方法栈中的对象)publicnativevoidnativeMethod();// 7. 同步监视器(synchronized持有的对象)publicvoidsyncMethod(){synchronized(this){// this就是GC Root// ...}}}

2.2 关键理解

GC Root类型所在区域生命周期
局部变量虚拟机栈方法执行期间
静态变量方法区类卸载前
常量方法区类卸载前
活跃线程堆(Thread对象)线程结束前
JNI引用本地方法栈方法执行期间
同步监视器锁释放前

三、避免内存浪费小技巧

下面是一个优化性能的小技巧,避免内存浪费。

3.1 一个极端的例子

publicvoidtest(){// 第一阶段:分配一个大数组并使用byte[]bigBuffer=newbyte[100*1024*1024];// 100MBprocessData(bigBuffer);// 处理数据// 第二阶段:执行耗时操作,不再需要bigBuffertry{Thread.sleep(10000);// 模拟耗时10秒的其他操作}catch(InterruptedExceptione){e.printStackTrace();}// 第三阶段:可能还有其他操作...computeOtherThings();// 方法结束}

3.2 发生了什么?

Thread.sleep(10000)执行的这10秒内:

  1. test()方法的栈帧仍然存在(方法还没结束)
  2. 局部变量bigBuffer还在局部变量表中
  3. bigBuffer仍然指向堆中的100MB数组
  4. 从GC Roots可达性分析看:这个数组是可达的!

结果:整整10秒,100MB内存被白白占用,无法回收。

方法执行的后期,这个对象已经不用了却仍然占用内存,造成了临时的内存浪费

在长耗时的方法中,这种浪费可能很严重。

3.3 如何避免?

publicvoidtest(){// 第一阶段byte[]bigBuffer=newbyte[100*1024*1024];processData(bigBuffer);// 关键:主动切断引用bigBuffer=null;// 局部变量表中的引用被清空// 第二阶段Thread.sleep(10000);// 此时bigBuffer已不可达,可以被GC回收computeOtherThings();}

手动置null的本质:把局部变量表中的引用值清空,让对象提前变成不可达。


四、可达性分析 vs 引用计数法

4.1 引用计数法的缺陷(循环引用)

classNode{Nodenext;}publicvoidtest(){Nodea=newNode();Nodeb=newNode();a.next=b;// b的计数+1b.next=a;// a的计数+1// 方法结束,a和b出栈// 但a还被b.next引用,b还被a.next引用// 计数永远不为0,无法回收!}

关键分析

  • 栈帧销毁只销毁了引用变量ab(存储地址的内存)
  • 但堆中a对象的next字段还在指向b,b对象的next字段还在指向a
  • 引用计数法统计所有引用,包括堆内的引用
  • 两个对象的计数都是1,永远不会变成0

4.2 可达性分析法如何解决?

// 还是这个循环引用Nodea=newNode();Nodeb=newNode();a.next=b;b.next=a;// 方法结束

可达性分析的过程

  1. 从GC Roots开始找(a和b出栈后,没有根指向它们)
  2. 遍历引用链:从任何根出发都找不到a和b
  3. 结论:a和b都是不可达的,全部标记为垃圾
  4. 一次性全部回收

4.3 两种方法的本质区别

对比项引用计数法可达性分析法
关注点有多少引用指向我从根出发能否找到我
统计范围所有引用(栈+堆)只关心从根出发的路径
循环引用❌ 无法处理✅ 轻松处理
并发开销大(每次赋值要原子操作)小(只在GC时扫描)

一句话总结:Java的GC不是在看“谁指着我”,而是在看“我能从根找到谁”。


五、一个帮你通透的类比

引用计数法:就像一个小区的保安,每个人进出都要登记(引用变化就要计数)。如果两个人互相说“我在他家”(循环引用),保安就糊涂了,以为两家都有人,永远不清理。

可达性分析法:就像警察查户口,从街道办(GC Roots)开始,挨家挨户查:你们家是谁管的?查到谁就登记,没查到的就是空房。不管你们邻里之间怎么互相说“我在他家”,只要街道办找不到你,你就是空房。

局部变量未清空:就像你搬家了(不再需要某个对象),但还在街道办的登记册上(局部变量还在引用),警察就会认为你家还有人。


六、总结

  1. 可达性分析的本质:从GC Roots出发,遍历对象图,能到达的存活,不能到达的回收
  2. GC Roots的五种类型:局部变量、静态变量、常量、活跃线程、JNI引用、同步监视器
  3. 局部变量表陷阱:方法未结束,即使不再使用的对象,只要还在局部变量表中,就仍然存活
  4. 手动置null的意义:提前切断引用,让对象提前变得不可达
  5. 引用计数法的死穴:循环引用导致计数永远不为0
  6. Java的选择:用全局扫描的可达性分析,换取正确性和并发性能

未完待续

理解了对象怎么被标记存活,下期我们来看对象存在哪里——内存区域的演进与直接内存,从永久代到元空间,从传统IO到零拷贝,带你理解JVM性能优化的权衡艺术。


参考资料

  • 《深入理解Java虚拟机》周志明
  • Oracle官方文档:Java Garbage Collection Basics
  • HotSpot VM源码

如果你觉得本文有帮助,欢迎点赞、评论、转发!

http://www.jsqmd.com/news/495134/

相关文章:

  • 每日一题:Span<T>和Memory<T>
  • 万爱通礼品卡闲置不用?教你选择最靠谱的线上回收渠道 - 团团收购物卡回收
  • 网页编辑器如何优化WangEditor的Word粘贴功能?
  • 从“安全孤岛”到“信任基石”:ibbot智体机灵如何重新定义AI智能体的安全范式
  • TCP/IP转EtherNet/IP 协议转换 罗克韦尔PLC与视觉设备交互
  • Simulink十四自由度整车模型
  • 【重磅】优质的朋友圈广告排名前十 - 服务品牌热点
  • 【从零入门23种设计模式24】行为型之访问者模式
  • 给AI老板植入幻觉:让它自认是饮水机
  • OpenAI 新模型 GPT - 5.4 系列:小身材能否撬动大市场?
  • 总结GRG石膏制品选购要点,天津好用的品牌有哪些 - mypinpai
  • 探索 FDTD 算法仿真超透镜:从参数调整到聚焦实现
  • 黑马点评-用JMeter测试缓存重建时,HTTP请求的响应结果的data为空
  • 二分题目集
  • 2026年GRG石膏制品优质供应商推荐,费用怎么算 - 工业设备
  • 项目实训(一):项目基础框架与 FastAPI 后端创建
  • 深度解析 `utf8mb4` 和 `utf8mb4_unicode_ci`:从原理到实战,避坑指南全解析
  • SSR驱动220V需TVS/MOV而非RCD
  • 2026年黑龙江高性价比二手房翻新企业排名,值得选的品牌 - 工业推荐榜
  • Claude国内镜像站实测:可扩展监督与宪法AI,推理架构的范式革命
  • 关于防抖和节流
  • 操作步骤分享:DeepSeek转Word文档的正确步骤
  • 探寻2026正极材料废气焚烧炉推荐厂商,选购要点有哪些 - myqiye
  • OpenClaw 高效配置与集成指南:从模型选择到 API 对接
  • Meta羊驼LLaMA的崛起与争议:开源AI的史诗级故事
  • 讲讲靠谱的轻集料混凝土LC5.0源头工厂,京津冀地区有哪些推荐? - 工业品牌热点
  • 英语六级作文历年真题及范文模版汇总PDF电子版(2015-2025年6月)
  • 风爆远征英雄年代怀旧服:初心不改热血依旧,英雄年代怀旧服必玩国战经典
  • HomeAssistant——MQTT设备实体创建
  • 【深度学习实战】巧用“噪声”画出心脏:扩散模型(Diffusion Model)在超声影像合成中的破局