要说 Java 和 C++ 最显著的差别之一,应该就是内存动态分配和回收机制了吧。Java 的面试必问 JVM 的垃圾回收,C++ 的面试则必问内存泄漏,我好想逃~~ 却逃不掉~~
对象死亡判断
在垃圾回收之前,一定要判断清楚哪些对象仍然存活,哪些对象已经死去,要是不小心把仍然存活的对象埋葬了,那可就成为 "sha人凶手" 了。
引用计数法
引用计数法是一个简单高效的算法,具体原理是在对象中添加一个计数器 count
,每当一个地方引用它,count
值加一;当引用失效时,count
值减一;count
为 0
的对象就认为是死亡对象。
这种方法在很多领域都有应用,如 C++ 中的智能指针或是 Python 中的 GC 设计,它最大的好处就是回收及时 :一个对象的引用计数归零的那一刻即是它成为垃圾的那一刻,同时也是它被回收的那一刻。相较而言, Java 中死亡对象就得等到下一次 GC 时才被清理。但是该方法会遇到很多 "例外情况",其中最重要的就是无法解决循环引用的问题。
在下面的例子中,objA 和 objB 形成了循环引用,但是 Java 并没有放弃回收它们,这也侧面证明了 Java 没有使用引用计数算法。
java
// VM 参数:-XX:+PrintGC
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
// 占内存,方便查看是否发生了 GC
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
可达性分析法
Java 中真正使用的判断对象是否存活的算法其实是可达性分析法 。这个算法通过一系列 "GC Roots" 作为起始节点集合,并根据引用关系进行 BFS 搜索 ,得到的路径被称为 "引用链"。如果某个对象到任意 GC Roots 间都没有引用链,则认为该对象死亡。如下图所示, object5、6、7 便是死亡对象。
可达性分析本身的思想是很简单的,那到底有哪些对象可以作为 GC Roots 呢?事实上在 Java 技术体系中,能够固定作为 GC Roots 的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的局部变量、临时变量等。
- 方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。
- 方法区中常量引用的对象,譬如字符串常量池里的引用。
- 本地方法栈中 JNI(即一般说的Native方法)引用的对象。
- JVM 内部的引用,如基本数据类型对应的 Class 对象,系统类加载器等等。
- 所有被同步锁(synchronized 关键字)持有的对象。
再谈引用
无论是哪种算法,都不可避免地涉及到了 "引用" 这个概念。在 JDK 1.2 以前,reference 类型指的就是传统意义上的引用,而为了能让程序自己决定 对象的生命周期,JDK 1.2 引入了强引用、软引用、弱引用、虚引用四种引用类型。
-
强引用
强引用就是指最传统的引用,是在程序中普遍存在的引用赋值,即类似
Object obj = new Object()
这类的引用关系。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。下面代码中,尽管 o1 已经被回收,但是 o2 的强引用一直存在,所以对象不会被 GC 回收:
javapublic class StrongReferenceDemo { public static void main(String[] args) { Object o1 = new Object(); Object o2 = o1; o1 = null; System.gc(); System.out.println(o1); System.out.println(o2); } }
运行结果:
-
软引用
软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
下面的代码模拟了内存充足和内存不足两种情况下,软引用对象的回收情况:
javapublic class SoftReferenceDemo { public static void main(String[] args) { System.out.println("------内存足够的情况------"); softRefMemoryEnough(); System.out.println("------内存不够用的情况------"); softRefMemoryNotEnough(); } private static void softRefMemoryEnough() { Object o1 = new Object(); SoftReference<Object> s1 = new SoftReference<Object>(o1); System.out.println(o1); System.out.println(s1.get()); o1 = null; System.gc(); System.out.println(o1); System.out.println(s1.get()); } private static void softRefMemoryNotEnough() { Object o1 = new Object(); SoftReference<Object> s1 = new SoftReference<Object>(o1); System.out.println(o1); System.out.println(s1.get()); o1 = null; byte[] bytes = new byte[10 * 1024 * 1024]; System.out.println(o1); System.out.println(s1.get()); } }
运行结果:
-
弱引用
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。弱引用可以和引用队列结合使用,一旦对象只有弱引用,GC thread 就会把弱引用直接插入到引用队列,与将 finalizable 对象插入 finalization queue 是同一时机。
下面代码中,由于强引用 o1 不存在了,只剩下弱引用 w1,因此无论内存是否充足,对象都会被回收:
javapublic class WeakReferenceDemo { public static void main(String[] args) { Object o1 = new Object(); WeakReference<Object> w1 = new WeakReference<Object>(o1); System.out.println("强引用存在时,o1 = " + o1); System.out.println("强引用存在时,w1 = " + w1.get()); o1 = null; System.gc(); System.out.println("强引用不存在时,o1 = " + o1); System.out.println("强引用不存在时,w1 = " + w1.get()); } }
运行结果:
-
虚引用
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例(即 get() 方法永远返回 null)。虚引用必须和引用队列结合使用,当某个被引用的对象被回收后,JVM 会将指向它的引用加入到引用队列的队列末尾 ,这相当于是一种通知机制 ,由 ReferenceHandler 守护线程完成。虚引用最常用的地方是配合 Cleaner 完成堆外内存的释放。
下面的代码对虚引用和引用队列进行了简单的使用:
javapublic class PhantomReferenceDemo { public static void main(String[] args) throws InterruptedException { Object o1 = new Object(); ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>(); PhantomReference<Object> phantomReference = new PhantomReference<Object>(o1,referenceQueue); System.out.println("------强引用存在时------"); System.out.println(o1); System.out.println(referenceQueue.poll()); System.out.println(phantomReference.get()); o1 = null; System.gc(); Thread.sleep(3000); System.out.println("------强引用消失后------"); System.out.println(o1); System.out.println(referenceQueue.poll()); //引用队列中 System.out.println(phantomReference.get()); } }
运行结果:
生存还是死亡?
一个对象在确定回收之前,仍然有机会通过 finalize() 方法 "拯救" 自己,具体过程如下:
- 重写了 finalize() 的类实例化时,JVM 会标记该对象为 finalizable;
- GC thread 检测到对象不可达时,如果对象是 finalizable,会将对象添加到 finalization queue,对象被 finalizer daemon thread 的 Finalizer class 引用,重新可达,推迟 GC;
- finalizer daemon thread 在一段时间之后(某个不确定时间) ,将会从 finalization queue 出队对象,调用对象的 finalize(),随后标记对象为 finalized ,并断开 Finalizer class 的强引用;
- GC thread 重新检测到对象不可达时,才会回收对象。
- 对于虚引用,在对象被销毁了之后会被加入到引用队列(注意弱引用和虚引用加入队列的不同时机)。
注一:任何一个对象的 finalize() 方法都只会被系统自动调用一次。
注二:finalize() 方法运行代价高昂,不确定性大,不建议使用。(可以用 try-finally 代替)
回收方法区
Java 虚拟机规范中说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的 "性价比" 一般比较低。
方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类:
-
回收废弃常量与回收 Java 堆中的对象非常类似。以常量池中字符串对象 "java" 为例,如果没有任何地方引用了这个字面量,那么在发生内存回收,且垃圾回收器判断确有必要的话, "java" 常量就会被系统清理出常量池。
-
无用的类需要同时满足以下三个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
在类同时满足这三个条件时,将被允许回收(非必须)。在大量使用反射、动态代理等的场景下,通常会需要进行类的卸载以保证不会对方法区造成过大的内存压力。