序言
Java 堆和方法区这两个区域有着显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法执行的不同条件分支所需要的内存也可能不一样,只有处于运行期,才知道程序会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理。接下来所讨论中“内存”分配与回收也仅仅特指这一部分内存。
1· 对象已死
在堆里面存放着几乎所有的对象实例,垃圾收集器在对堆进行回收之前,第一件事情就是要确定这些对象中哪些对象存活,哪些对象死亡。
1.1 引用计数算法
何为引用计数算法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
客观来说,引用计数算法虽然占用1一些额外的内存空间来进行计数,但它的原理简单,判定效率也高,在大多数情况下它都是一个不错的算法。但是,在 Java 领域,至少主流的 Java 虚拟机里面都有使用引用计数算法来进行内存管理,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确的工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
举个例子,看如下代码中:对象 objA 和 objB 都有字段 instance,赋值令 objA.instance = objB 及 objB.instance = objA,除此之外这两个对象再无任何引用,实际上这个对象已经不可能再被访问,但是它们互相引用着对象,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
public class Main {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 这个成员属性的作用是占点内存,以便在GC日志中看清楚是否被回收过
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) {
Main objA = new Main();
Main objB = new Main();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假设在这行发生GC,objA和objB是否能被回收?
System.gc();
}
}
GC日志信息如下:
[0.016s][info][gc] Using G1
[0.019s][info][gc,init] Version: 17.0.2+8-LTS-86 (release)
[0.019s][info][gc,init] CPUs: 12 total, 12 available
[0.019s][info][gc,init] Memory: 15741M
[0.019s][info][gc,init] Large Page Support: Disabled
[0.019s][info][gc,init] NUMA Support: Disabled
[0.019s][info][gc,init] Compressed Oops: Enabled (Zero based)
[0.019s][info][gc,init] Heap Region Size: 2M
[0.019s][info][gc,init] Heap Min Capacity: 8M
[0.019s][info][gc,init] Heap Initial Capacity: 246M
[0.019s][info][gc,init] Heap Max Capacity: 3936M
[0.019s][info][gc,init] Pre-touch: Disabled
[0.019s][info][gc,init] Parallel Workers: 10
[0.019s][info][gc,init] Concurrent Workers: 3
[0.019s][info][gc,init] Concurrent Refinement Workers: 10
[0.019s][info][gc,init] Periodic GC: Disabled
[0.020s][info][gc,metaspace] CDS archive(s) mapped at: [0x0000000800000000-0x0000000800bc0000-0x0000000800bc0000), size 12320768, SharedBaseAddress: 0x0000000800000000, ArchiveRelocationMode: 0.
[0.020s][info][gc,metaspace] Compressed class space mapped at: 0x0000000800c00000-0x0000000840c00000, reserved size: 1073741824
[0.020s][info][gc,metaspace] Narrow klass base: 0x0000000800000000, Narrow klass shift: 0, Narrow klass range: 0x100000000
[0.120s][info][gc,task ] GC(0) Using 5 workers of 10 for full compaction
[0.120s][info][gc,start ] GC(0) Pause Full (System.gc())
[0.120s][info][gc,phases,start] GC(0) Phase 1: Mark live objects
[0.121s][info][gc,phases ] GC(0) Phase 1: Mark live objects 0.683ms
[0.121s][info][gc,phases,start] GC(0) Phase 2: Prepare for compaction
[0.121s][info][gc,phases ] GC(0) Phase 2: Prepare for compaction 0.372ms
[0.121s][info][gc,phases,start] GC(0) Phase 3: Adjust pointers
[0.122s][info][gc,phases ] GC(0) Phase 3: Adjust pointers 0.409ms
[0.122s][info][gc,phases,start] GC(0) Phase 4: Compact heap
[0.122s][info][gc,phases ] GC(0) Phase 4: Compact heap 0.262ms
[0.123s][info][gc,heap ] GC(0) Eden regions: 2->0(1)
[0.123s][info][gc,heap ] GC(0) Survivor regions: 0->0(0)
[0.123s][info][gc,heap ] GC(0) Old regions: 0->2
[0.123s][info][gc,heap ] GC(0) Archive regions: 0->0
[0.123s][info][gc,heap ] GC(0) Humongous regions: 4->0
[0.123s][info][gc,metaspace ] GC(0) Metaspace: 433K(640K)->433K(640K) NonClass: 406K(512K)->406K(512K) Class: 26K(128K)->26K(128K)
[0.123s][info][gc ] GC(0) Pause Full (System.gc()) 10M->0M(14M) 2.949ms
[0.123s][info][gc,cpu ] GC(0) User=0.00s Sys=0.00s Real=0.00s
[0.124s][info][gc,heap,exit ] Heap
[0.124s][info][gc,heap,exit ] garbage-first heap total 14336K, used 945K [0x000000070a000000, 0x0000000800000000)
[0.124s][info][gc,heap,exit ] region size 2048K, 1 young (2048K), 0 survivors (0K)
[0.124s][info][gc,heap,exit ] Metaspace used 447K, committed 640K, reserved 1056768K
[0.124s][info][gc,heap,exit ] class space used 28K, committed 128K, reserved 1048576K
我们看第36行,[0.123s][info][gc ] GC(0) Pause Full (System.gc()) 10M->0M(14M) 2.949ms,表示这次FULL GC的结果,堆内存使用量从10M减少到0M,这意味着虚拟机并没有因为这两个对象互相引用就放弃回收它们,这也侧面说明了 Java 虚拟机并不是通过引用计数算法来判断对象是否存活的。
1.2 可达性分析算法
当前主流的商用程序语言的内存管理子系统都是通过可达性分析算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为 “GC Roots” 的根对象作为起始节点,从这些节点开始根据引用关系向下搜索,搜索过程中所走过的路径称为“引用链”,如果某个对象到 GC Roots 间没有任何引用链相连,则证明此对象就是不可能再被使用的对象。如下图所示,对象 object5、object6、object7 虽然互有关联,但是它们带 GC Roots 是不可达的,因此它们将会被判定为可回收的对象。
在 Java 技术体系里面,固定可作为 GC Roots 的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量表、临时变量等。
- 在方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池里的引用。
- 在本地方法栈中 JNI(即 Native 方法)所引用的对象。
- Java 虚拟机内部的引用,如基本数据类型的 Class 对象,一些常驻的异常对象,还有系统类加载器。
- 所以被同步锁持有的对象。
2. 再谈引用
无论通过引用计数算法判定对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判断对象是否存都和“引用”离不开关系。在 JDK 1.2 版后,对引用的概念进行扩充,将引用分为强引用、软引用、弱引用、虚引用 4 种,这 4 种引用强度逐渐减弱。
- 强引用就是最传统的引用的定义,它是指在程序代码之中普遍存在的引用赋值,即类似于
Object obj = new Object()这种引用关系。无论在任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 - 软引用时用来描述一些还有用,但非必要的对象。只被软引用关联着的对象,在系统将要发生内存溢出之前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,那么将会抛出内存溢出异常。在 JDK 1.2 版后提供了
SoftReference类来实现软引用。 - 弱引用也是用来描述那些非必要对象,但是它的强度比软引用更弱一些,被弱引用关联的对象,只能生存到下一个垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用所关联的对象。在 JDK 1.2 版后提供了
WeakReference类来实现弱引用。 - 虚引用也被称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用方式。一个对象是否有虚引用存在,完全不会对其生存时间造成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是为了能在这个对象被收集器回收时受到一个系统通知。在 JDK 1.2 版后提供了
PhantomReference类来实现虚引用。
3. 生存还是死亡?
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”,这时候它还暂处于“缓刑”阶段,要真正宣告一个对象死亡,至少还要经历两次标记过程:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为没有必要执行。
如果这个对象被判定为有必要执行finalize()方法,那么该对象就被会加入到一个F-Queue队列中,随后,虚拟机会自动创建一条低优先级的Finalizer线程去执行队列当中对象的finalize()方法。这里执行只是指虚拟机会触发这个额方法开始运行,但并不承诺一定会等待它运行完毕。这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者极端地出现死循环,将会导致整个F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统崩溃。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()方法成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将移除“即将回收”的集合;如果对象这时候还没有逃脱,那基本上就真的要被回收了。从如下代码可以看到一个对象的finalize()方法被执行,但是它依然可以存活。
/**
* ClassName: FinalzeEscapeGC
* Package: PACKAGE_NAME
* Description:
*
* @Author ms
* @Create 2024/11/22 20:53
* @Version 1.0
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC saveHook = null;
public void isAlive(){
System.out.println("yes , i am still alive:");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.saveHook = this;
}
public static void main(String[] args) throws InterruptedException {
saveHook = new FinalizeEscapeGC();
// 对象第一次拯救自己
saveHook = null;
System.gc();
// 因为Finalizer的优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (saveHook != null){
saveHook.isAlive();
}else {
System.out.println("no, i am dead:");
}
// 下面这段代码和上面的完全相同,不过这次自救切失败了
saveHook = null;
System.gc();
// 因为Finalizer的优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (saveHook != null){
saveHook.isAlive();
}else {
System.out.println("no, i am dead:");
}
}
}
执行结果如下:
finalize method executed!
yes , i am still alive:
no, i am dead:
从运行结果可以看到,saveHook 对象的finalize()方法确实被垃圾收集器触发过,并且在被收集前成功逃脱了。另外一个需要注意的地方是,代码中有两段完全一样的代码片段,执行结果确实一次成功逃脱,一次失败了。这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
4. 回收方法区
相比于堆内存的回收来说,方法区的回收条件非常苛刻,性价比也比较低。
方法区的垃圾收集主要回收两部分内容:废产量常量和不再使用的类型。
- 回收废弃的常量和回收对象非常类型,举个例子,常量池中存在字符串 “java”,但是没有任何对象引用这个字符串的话,那么发生垃圾回收时,“java”常量就会被清除常量池
- 回收类型的条件相当苛刻,需要同时满足下面三个条件:
- 该类的所有实例都已经被回收,java 堆中不存在该类及其任何派生类的对象。
- 加载该类的类加载器已经被回收,这个条件极难达成。
- 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
在大量使用反射、动态代理、CGLIB等字节码框架,动态生成 JSP 以及 OSGI 这类频繁自定义类加载器的场景中,是需要具备类卸载的能力,这样才能避免方法区的内存压力过大。