垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于 1960 年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了。 java 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。
如何判断一个对象是否还存活、是否需要被清理呢? 引用计数法是一个十分常见的算法,每当一个地方引用他,就将该对象的引用计数器值加1,一旦引用失效,则将引用计数器值减1,任何时候,只要对象引用计数为0,我们就可以毫无顾忌的清理掉他。 这是一个实现简单并且高效的算法,FlashPlayer、python、PHP 等等领域中都使用了引用计数法管理内存。 然而主流的实现中,java 都没有使用引用计数法,因为他的主要问题在于无法解决对象之间的相互循环引用问题。
在 Lisp、java、C# 等主流商用程序语言中,都是采用可达性分析来判断对象是否存活的。 如下图所示,当一个对象没有任何引用链相连,无法到达 GC Roots 对象时,则证明该对象是不可用的。
以下对象可作为 GC Roots 对象:
我们多次提到了“引用”这个词,java 中,对引用进行了区分,分为了下面的四类: 1. 强引用 — 代码中普遍存在的,Object obj = new Object() 所创建的引用,只要强引用存在,垃圾收集器就永远不会回收被引用对象 2. 软引用 — 有用但非必需的对象,只有在系统将要发生内存溢出异常前才会将这些对象列入回收范围 3. 弱引用 — 被弱引用关联的对象只能生存到下一次垃圾收集之前,jdk 提供了 WeakReference 类来实现弱引用 4. 虚引用 — 也称为“幽灵引用”或“幻影引用”,无法通过他获取一个对象的实例,因此虚引用是否存在对对象毫无影响,虚引用存在的目的在于当这个对象被收集器回收时可以收到一个系统通知,jdk 提供了 PhantomReference 类来实现虚引用
当经过可达性分析后,垃圾收集器对没有与 GC Roots 相连接的引用链进行第一次标记,然后进行一次筛选,筛选出没有 finalize 方法或者已经被执行了 finalize 方法的对象。 对于有必要执行 finalize 方法的对象,他们会被放置到 F-Queue 队列中,一个由虚拟机自动建立的、低优先级的 Finalizer 线程会去执行队列中的对象的 finalize 方法,但虚拟机只会触发 finalize 方法的执行,并不会承诺方法的完整运行。 对于筛选出的没有 finalize 方法或者已经被执行了 finalize 方法的对象,垃圾收集器进行第二次标记,标记出没有被引用的对象,接下来就可以对他进行回收了。 值得注意的是,任何一个对象的 finalize 方法都只会被自动调用一次,如果在 finalize 方法中,对象通过被其他类引用逃脱标记,在接下来的一次回收中,他的 finalize 方法将不会再次被执行。 finalize 方法通常并不被推荐使用。
java 虚拟机规范中并不要求方法区实现垃圾回收。 HotSpot 将方法区设置为永久代进行垃圾的收集,对于大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景,对类方法区的回收是避免永久代溢出的重要功能。 在堆中的垃圾清理通常可以回收 70% 到 95% 的空间,而在方法区垃圾收集的效率远低于此。
永久代需要收集的垃圾主要有两部分内容:废弃常量和无用的类。 废弃的常量与回收 java 堆中的对象非常类似,只要没有任何引用引用常量池中的常量,他就可以被回收了。 但是对于无用的类,必须同时满足下面三个条件才能认为他是无用的: 1. 类的所有实例都已经被回收 2. 该类的 ClassLoader 已经被回收 3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
HotSpot 虚拟机提供了-Xnoclassgc 控制是否开启对方法区的回收。 通过 -verbose:class 也可以控制对方法区的回收。 通过 -XX:+TraceClassLoding、-XX:+TraceClassUnLoading 可以查看类加载和卸载信息。