【大白话说Java面试题 第69题】【JVM篇】第29题:GC Roots 有哪些?

📌 PDF :大白话说Java面试题 --- 02-JVM篇

第29题:GC Roots 有哪些

📚 回答:

  • 核心考点 :
    GC Roots 是可达性分析算法 的起点。大厂面试要求准确列出所有类型的GC Roots ,并能解释为什么某些对象会成为GC Root 以及常见的误解(如静态变量是否一定能阻止GC?)。

1. GC Roots 的完整定义

GC Roots 是指必须存活的对象集合 ,从它们出发可达的对象被标记为存活 ,不可达的则判定为可回收。


2. GC Roots 的5种类型(JDK 8及之前)
类型 说明 示例
虚拟机栈引用 每个栈帧中的局部变量表引用的对象 方法内的局部变量、参数
静态属性引用 方法区中类的静态变量引用的对象 private static User user = new User();
常量引用 方法区中运行时常量池引用的对象 字符串常量 "abc"、Class对象
JNI引用(Native Stack) 本地方法栈中JNI(全局/局部)引用的对象 JNIEnv->NewObject(...)
活跃线程 所有正在运行的Thread对象 Thread.currentThread()

3. 各类型的深度解释(面试加分项)

3.1 虚拟机栈中的引用(最常用)

  • 包括:局部变量、方法参数、临时变量

  • 示例:

    java 复制代码
    void foo() {
        Object o = new Object();  // o 是GC Root
        int i = 0;                // 基本类型不是引用,不算
    }
  • 注意:方法执行完出栈后,这些GC Root消失,对应对象变为不可达。

3.2 静态属性引用

  • 属于类级别,类未被卸载则静态变量一直存活。

  • 常见误解 :设置 static obj = null 后,原对象不再被GC Root引用,可能被回收。

  • 示例:

    java 复制代码
    class Cache {
        static Map<String, Object> map = new HashMap<>();  // map 是GC Root
    }
  • 内存泄漏高发地:静态集合类添加对象后忘记清理。

3.3 常量引用

  • 字符串常量池中的对象(如 "hello")
  • Class对象(如 String.class)
  • 基本类型包装类常量(如 Integer.valueOf(1) 缓存的 -128~127 对象)
  • 注意 :常量池中动态添加的字符串(intern())也是GC Root,直到JVM回收该常量。

3.4 JNI 引用

  • Global JNI Reference :显式 NewGlobalRef 创建,需手动 DeleteGlobalRef 否则泄漏
  • Local JNI Reference:本地方法栈帧内的引用,方法退出后自动释放
  • 场景:Android NDK / JNI 开发中,忘记删除GlobalRef导致内存泄漏。

3.5 活跃线程

  • 每个正在运行的线程本身就是GC Root(线程栈+程序计数器)
  • 线程内部的局部变量也是从该线程Root可达的

4. 其他隐藏的 GC Roots(JDK 8+)
类型 说明
系统类加载器 加载核心类(rt.jar)的BootClassLoader
JVM内部对象 SystemDictionary、JVMTI 标记的对象
同步监视器 被synchronized锁住的对象
StackMapTable JVM内部栈映射表引用的对象
Finalizer引用 尚未执行finalize()的对象会被Finalizer队列持有

5. 关键区别:哪些不是 GC Roots?
对象 是否是GC Root 原因
方法内的局部变量(未执行到) 否 栈帧未入栈,不存在引用
不可达的静态变量 否 类已被卸载(如自定义ClassLoader卸载时)
普通对象字段 否 需要通过GC Root链到达
软/弱/虚引用指向的对象 否 引用本身特殊处理,但引用的目标对象需链到Root才算存活

6. 大厂面试追问

Q1:静态变量引用的对象一定是 GC Root 吗?

A:是的,只要该类未被卸载。在自定义ClassLoader场景中,类可被卸载(如OSGi、热部署),卸载后静态变量不再作为GC Root。

Q2:字符串常量池中的对象永远不会被回收吗?

A:不会。JDK 7+字符串常量池在堆中,Full GC时若常量无引用,可以被回收。例如:String s = "a"; s = null; 触发Full GC后 "a" 可能被回收。

Q3:ThreadLocal 中的变量是 GC Root 吗?

A:ThreadLocalMap 的 key 是弱引用,不是直接GC Root ;但当前Thread对象是GC Root,从Thread可以访问到ThreadLocalMap,进而访问到key。所以只要线程活着,ThreadLocalMap的引用链就不断。

Q4:GC Roots 的数量一般是多少?

A:大应用中可达数万 甚至更多(每个栈帧的局部变量、每个静态变量、每个活跃线程等)。GC Roots 过多会导致可达性分析变慢。

Q5:可达性分析和引用计数法的区别?

A:引用计数无法解决循环引用(A->B, B->A),而可达性分析从GC Roots出发,不可达才回收,能正确处理循环。


7. 实战:如何查看一个对象是否被 GC Root 引用?

工具:MAT(Memory Analyzer Tool)

  1. 打开 heap dump
  2. 选择对象 → Path to GC Roots → 排除软/弱/虚引用
  3. 显示从该对象到某个GC Root的最短路径

示例结果:

复制代码
java.lang.Object @ 0x7f3a2c8
→  com.example.Cache.map (静态字段)
→  java.util.HashMap @ 0x7f3a3d0
→  ... (到GC Root)

8. 总结对比表(面试速记)
GC Root 类型 生命周期 常见泄漏风险
虚拟机栈引用 方法执行期间 无(自动释放)
静态属性引用 类生命周期(通常永久) 高(静态集合)
常量引用 JVM常量池回收时 中(intern字符串)
JNI Global引用 手动删除 高(忘记DeleteGlobalRef)
活跃线程 线程运行期间 中(线程池任务未释放Context)

💡 面试官想要的满分总结:

"GC Roots 是可达性分析的起点,主要包括5类:栈帧局部变量、静态变量、常量、JNI引用、活跃线程 。

特别注意:静态变量引用的对象只要类未卸载就一直是GC Root,容易引发内存泄漏;字符串常量池中的对象在JDK 7+中位于堆上,Full GC时可能被回收。

排查内存泄漏时,用MAT的 Path to GC Roots 功能,找到是哪种Root阻止了对象回收,就能定位问题根源。"


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
不是万能的豆学长9 分钟前
排查Go服务40天后的内存泄漏问题
开发语言·后端·golang·go
ShineWinsu9 分钟前
对于HTML:HTML常见标签的解析—上
前端·面试·html·浏览器·标签·图片·超链接
阿狗童鞋9 分钟前
SpringSecurity权限认证实战指南
java·后端·spring
码匠许师傅12 分钟前
【C++三方组件】libpqxx:PostgreSQL 的 C++ 客户端
开发语言·c++·postgresql
Wang's Blog13 分钟前
Java 项目部署之 Docker工具快速入门: 学习路径与命令速查导引
java·学习·docker
花间相见19 分钟前
【计算基础|网络13】UDP 与可靠传输
后端·面试
H.莓飛22 分钟前
【C++】内存管理
linux·开发语言·jvm·c++·visual studio
tachibana226 分钟前
Go 语言 Map 的底层实现原理
开发语言·后端·golang·make
考研保研资料分享31 分钟前
北大软微软件工程保研经验:哈工大面试追问与补录风险取舍
面试·职场和发展·软件工程
Wang's Blog35 分钟前
Java 项目部署之 Docker工具快速入门: Docker 镜像仓库
java·docker