垃圾回收器如何处理循环引用
垃圾回收是一门编程语言中必不可少的一部分,不论是手动释放内存的C和C++,还是自动回收垃圾的Java和C#等语言。对于Java这样的语言,一般的开发者不强求关心对象回收和内存释放,但是理解垃圾回收对开发工作还是大有裨益的。
在编程语言中,普遍存在着循环引用这样的问题,垃圾回收器是如何处理循环引用呢,常用的垃圾回收有引用计数和引用对象遍历两种实现,它们各自又是如何处理循环引用呢?本文讲以JVM中的GC为例逐一回答这些问题。
何为循环引用
如果有两个或者以上的对象,它们彼此引用,就会造成循环引用。如下面的例子
class Node {
Node next;
}
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
代码中,a对象引用了b对象,b对象也引用了a对象,这种情况下a对象和b对象就形成了循环引用。
引用计数GC处理
什么是引用计数
引用计数是一种垃圾回收的形式,每一个对象都会有一个计数来记录有多少指向它的引用。其引用计数会变换如下面的场景
- 当对象增加一个引用,比如赋值给变量,属性或者传入一个方法,引用计数执行加1运算。
- 当对象减少一个引用,比如变量离开作用域,属性被赋值为另一个对象引用,属性所在的对象被回收或者之前传入参数的方法返回,引用计数执行减1操作。
- 当引用计数变为0,代表该对象不被引用,可以标记成垃圾进行回收。
如何处理
实际上单纯的基于引用计数实现的计数器无法处理循环引用带来的问题。
CPython的垃圾回收就是采用引用计数,采用引用计数的主垃圾回收器会清理垃圾,对于那些因为循环引用无法清理的对象,CPython会不时启动一个辅助的基于引用遍历的垃圾回收器来清理它们。
引用遍历GC处理
什么是引用对象遍历
垃圾回收器从被称为GC Roots的点开始遍历遍历对象,凡是可以达到的点都会标记为存活,堆中不可到达的对象都会标记成垃圾,然后被清理掉。 GC Roots有哪些
- 类,由系统类加载器加载的类。这些类从不会被卸载,它们可以通过静态属性的方式持有对象的引用。注意,一般情况下由自定义的类加载器加载的类不能成为GC Roots
- 线程,存活的线程
- Java方法 栈 中的局部变量或者参数
- JNI方法栈 中的局部变量或者参数
- JNI全局引用
- 用做同步监控的对象
- 被JVM持有的对象,这些对象由于特殊的目的不被GC回收。这些对象可能是系统的类加载器,一些重要的异常处理类,一些为处理异常预留的对象,以及一些正在执行类加载的自定义的类加载器。但是具体有哪些前面提到的对象依赖于具体的JVM实现。
如何处理
基于引用对象遍历的垃圾回收器可以处理循环引用,只要是涉及到的对象不能从GC Roots 强引用 可到达,垃圾回收器都会进行清理来释放内存。
总结
基于引用计数的垃圾回收器无法处理循环引用导致的内存泄露问题,但是其在主流的JVM中很少,几乎所有的JVM都是采用引用对象遍历的方法,垃圾回收器都会处理循环引用潜在的问题。
一本书
- 深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)
- http://ju.outofmemory.cn/entry/180113