在 JVM 运行时数据区域中,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。
一、判断对象状态
JVM 在回收一个对象时,首先要判断这个对象的状态,如果判断对象为无效的(没有被任何对象或变量引用),则需要被 JVM 垃圾回收器回收。
1.1 引用计数算法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数不为 0 的对象仍然存活。
1 | public class ReferenceCountingGC { |
testGC()
方法执行后,objA
和 objB
不会被垃圾回收器回收,因为两个对象出现了循环引用,引用计数器永远不为 0,导致无法对它们进行回收。
虽然引用计数算法简单、高效,但是因为存在循环引用的问题,所以 JVM 并没有使用引用计数算法标记对象状态。
1.2 可达性分析算法
可达性分析(Tracing GC)通过 GC Roots 作为起始节点向下进行搜索,GC Roots 搜索的经过的路径称为引用链(Reference Chain),能够到达到的对象都是存活的(也就是引用链上的对象),不可达的对象被标记为无效的。
如图 Object5、Object6 和 Object7 虽然相互存在引用关系,但是 GC Roots 不可达,形成不了引用链,所以会被标记为无效的对象。
JVM 使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
只有引用类型的变量才有可能被认为是 GC Roots,值类型的变量永远不被认为是 GC Roots。而且 GC Roots 并不包括堆中对象所引用的对象,这样就不会出现循环引用。
可作为 GC Roots 的节点主要在全局性的引用与执行上下文中,GC Roots 必须是当前存活的引用类型对象。GC 管理的区域是 Java 堆,而虚拟机栈、方法区和本地方法栈不被 GC 所管理,因此选用这些区域内引用的对象作为 GC Roots,是不会被 GC 所回收的。其中虚拟机栈和本地方法栈都是线程私有的内存区域,只要线程没有终止,就能确保它们中引用的对象的存活。而方法区中类静态属性引用的对象是显然存活的。常量引用的对象在当前可能存活,因此,也可能是 GC Roots 的一部分。
二、垃圾回收过程
即使在可达性分析算法中不可达的对象,也不是一定会死亡的,它们暂时都处于 “缓刑” 阶段,要真正宣告一个对象 “死亡”,至少要经历两次标记过程。
2.1 第一次标记
如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是该对象是否覆盖了 finalize()
方法:
- 若已覆盖该方法,并该对象的
finalize()
方法还没有被执行过,那么就会将finalize()
扔到 F-Queue 队列中。 - 若未覆盖该方法,或者该对象的
finalize()
方法已经被执行过,则直接回收释放对象内存。
2.2 第二次标记
JVM 会自动建立一个低优先级的 Finalizer 线程去执行执行 F-Queue 队列中的 finalize()
方法。为了防止 F-Queue 队列中的其它对象长时间处于等待状态,而导致整个内存回收系统崩溃,一个对象在 finalize()
方法中执行缓慢,或者发生了死循环(更极端的情况),JVM 就直接停止其执行,将该对象清除回收。所以 JVM 不会让 F-Queue 队列等待所有的 finalize()
方法都执行结束。
2.3 对象重生或死亡
如果某个对象的 finalize()
方法时被执行时,与引用链上的任何一个对象建立了关联(例如:把自己(this 关键字)赋值给某个类变量或者对象的成员变量),那么该对象就在第二次标记时被移出 “即将回收” 的集合;如果没有,那么就会被垃圾收集器清除回收。
任何一个对象的 finalize()
方法都只会被 JVM 调用一次,所以自救也只能进行一次,如果回收的对象之前调用了 finalize()
方法,后面回收时就不会调用 finalize()
方法了。
使用 finalize()
方法来 “拯救” 对象是不值得提倡的,因为它不是 C/C++ 中的析构函数,而是 Java 刚诞生时为了使 C/C++ 程序员更容易接受它所做的一个妥协。它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。finalize()
能做的工作,使用 try-finally
或者其它方法都更适合、及时。
三、方法区的回收
因为在 JDK 8 之前方法区主要存放永久代对象,而永久代对象的回收率比新生代差很多,因此在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
四、引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 具有四种强度不同的引用类型。
4.1 强引用
被强引用关联的对象不会被垃圾收集器回收。
使用 new 一个新对象的方式来创建强引用。
1 | Object obj = new Object(); |
4.2 软引用
被软引用关联的对象,只有在内存不够的情况下才会被回收。
使用 SoftReference 类来创建软引用。
1 | Object obj = new Object(); |
4.3 弱引用
被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集发生之前。
使用 WeakReference 类来实现弱引用。
1 | Object obj = new Object(); |
WeakHashMap 的 Entry 继承自 WeakReference,主要用来实现缓存。
1 | private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> |
Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。ConcurrentCache 采取的是分代缓存,经常使用的对象放入 eden 中,而不常用的对象放入 longterm。eden 使用 ConcurrentHashMap 实现,longterm 使用 WeakHashMap,保证了不常使用的对象容易被回收。
1 | public final class ConcurrentCache<K, V> { |
4.4 虚引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
使用 PhantomReference 来实现虚引用。
1 | Object obj = new Object(); |