本文转载自微信公众号「码上Java」,作者码上Java。转载本文请联系码上Java公众号。
在开始之前,我们先回顾一下堆是个什么玩意,大家可能都知道,我们每天创建的Java对象几乎都存放在堆上面,所以说堆是一个巨大的对象池一点都不过分,在这个对象池里面管理者数据巨大的对象实例。
在对象池中对象的引用层次,有的是很深的。比如一个调用非常频繁的接口,生产对象的速度是非常可观的。对象之间的关系,可以形容成一张网。虽然Java总是给人一种有使不完的内存的感觉,但是对象也不能一直增加不减少啊,所以就必须有垃圾回收这个操作。
那么JVM如何发现垃圾的呢?
"垃圾回收"本文中简称 GC
你还记得电视剧中的“诛九族""?
比如小憨批打了皇帝老儿一巴掌,把皇帝老儿打的鼻青脸肿滴,皇帝老儿非常生气,他要下令诛小憨批的九族,以平心头只恨。
哈哈哈嗝~ 小憨批完了~
那么我们看看在古代这个诛九族是具体操作的呢?首先需要追溯到共同的祖先(也就是小憨批家族的大哥大),再往下逐一细数和小憨批有关系的(小憨批真坑啊)。
其实发生在堆上的垃圾回收和这个“诛九族“的是相同思路,那么我们下面具体分析一下JVM是如何进行GC的呢?
关于JVM的GC是不受程序控制的,当满足一定条件的时候就会主动触发。
当发生GC的时候,对于一个对象来说,JVM总能够找到引用它的祖先,当找到最后的时候,JVM发现这家伙的有些祖先已经玩完了,那么它们就会被JVM给干掉。
为什么还有没有被干掉的祖先呢?因为这些躲过GC的祖先们,它们是GC Roots ,长得比较特殊嘛(下面介绍它们的样子)。
当从GC Roots 向下追溯、搜索,就会产生一个引用链。当碰到有对象没有任何一个GC Roots 产生关系的话,这个对象就会被无情的干掉。(一根绳上的蚂蚱嘛)
来,我们画个图瞅瞅咋回事,如下图所示,Object5、Object6、Object7,由于不能和 GC Root 产生关联,发生 GC 时,就会被摧毁。
其实所谓的垃圾回收就是围绕着GC Roots 来的,但是同时,GC Roots 也存在着很多内存泄漏的根源,因为其他引用小弟压根没有这个权利。
你可能会产生疑问,那么什么样的对象才会是GC Roots 呢?
这个不在于它是什么样的对象,关键是它所处的位置(仔细品~)。
GC Roots 是什么
首先,GC Roots必须是一组必须活跃的引用。简单的讲,就是程序接下来通过直接引用或间接引用,能够被访问到的潜在被使用的对象(咋感觉还是有点绕呢)。
GC Roots 是这样子滴:
- Java线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等等。也就是与我们栈帧相关的各种引用。
- 所有当前被加载的Java类。
- Java类的引用类型静态变量。
- 运行时常量池里的引用类型常量。
- JVM内部数据结构的一些引用,比如sun.jvm.hotspot.memory.Univers类。
- 用于同步的监控对象。比如调用了对象的wait()方法。
- JNI handles,包括global handles 和 local handles。
以上GC Roots大致可以分为一下三大类。
- 活动线程相关的各种引用。
- 类的静态变量的引用。
- JNI引用。
最后我们需要注意的是,我们这里说的是活跃的引用,而不是对象,对象是不能作为GC Roots的。
整个GC过程中是找到那些活对象,并把剩余的空间都认得为“无用”。而不是找到所有死掉的对象,并回收它们占用的空间。所有说,哪怕JVM的堆非常大,基于tracing的GC方式,回收速度也是跟快的。
总结
GC Roots 就是可达性分析法。还有一种叫作引用计数法的方式。下面我们简单介绍一下。
引用计数法:在Java中如果要操作对象,就必须先获取该对象的引用,因此可以通过引用计数法来判断一个对象是否可以被回收。在为一个对象添加一个引用时,引用计数器就加1;为对象删除一个引用时,引用计数器就减1;如果一个对象的引用计数为0,则说明该对象没有被引用,可以回收。
优点是垃圾回收比较及时,实时性比较高,只要对象计数器为 0,则可以直接进行回收操作;而缺点是无法解决循环引用的问题。
因为存在循环引用这个致命的硬伤,没有一个主流JVM是采用引用计数法来实现 GC 的,所以你现在完全忘记引用计数这种方式了。