外观
Java 的栈、堆与 GC Roots 是什么?
标准回答
Java 的栈、堆和 GC Roots 分别回答三个问题:方法怎样执行、对象通常放在哪里、哪些对象仍然可达。线程栈保存方法调用帧、局部变量等执行状态;堆主要存放对象和数组;GC Roots 是垃圾回收进行可达性分析的一组起点。
例如方法里写 Object value = new Object(),可以先理解为局部变量 value 保存一个引用,新建对象本体通常在堆里。方法返回使这条局部引用不再有效,但如果其他对象、缓存或回调还持有它,对象仍可能存活。具体运行时还有优化,不能把这个基础模型当作所有优化后的物理内存布局。
GC Roots 常见来源包括活跃线程栈中的引用、JNI 强全局引用以及运行时持有的类等;类的静态字段引用也可能让对象从这些根保持可达。GC 从根沿强引用查找,能到达的对象通常不能作为垃圾回收,已无强可达路径的对象才可能在后续回收阶段释放。Root 可以理解成引用图的出发点,而不是一种你需要继承的 Java 类。
所以内存泄漏常见原因是“业务不用了,但引用链没断”。例如长生命周期单例保存 Activity,这个 Activity 及它引用的 View 仍可能从根到达。排查应找保留它的引用链,而不是只看有没有调用 System.gc;GC 也不会替你注销监听、结束线程或正确关闭所有外部资源。
关键机制
- 常见根来源包括活跃线程栈引用、已加载类的静态引用,以及 JNI 等持有的引用;具体 GC 实现还有其他根。
- 强引用保持可达,弱引用不阻止相应回收;软引用行为受内存压力和实现影响,不适合代替可控的缓存策略。
- Android 运行时还涉及 native、图形缓冲等内存,Java 堆健康不代表整个进程没有内存压力。
通俗例子
局部变量指向一个 Bitmap,方法返回只让这条局部引用消失。如果单例缓存仍持有 Bitmap,它依然不能按“已不用”回收。
追问与回答
两个对象互相引用,会导致 Java GC 回收不了吗?
不一定。Java GC 主要依据从根出发的可达性,而不是简单统计引用数量。如果两个对象只互相引用,整个环都不能从 GC Roots 到达,仍可以被回收。真正需要关注的是是否存在从活跃线程、长期容器等根来源到这个环的保留路径。
静态变量为什么容易造成泄漏?
静态字段往往依附于生命周期很长的类,在这些类仍然存活时,会让它引用的对象继续保持可达。把 Activity 放进去,就容易让短生命周期页面跟着长期存活。并不是所有 static 都会泄漏,而是字段持有的对象是否超过它应有的生命周期。
易混淆点
GC 不会自动结束线程、关闭连接或解绑回调;资源释放仍需明确的生命周期管理。
参考资料
内容来源
- Android 技术札记 · 原条目
java-memory