垃圾回收(Garbage Collection,简写为 GC)可能是虚拟机众多知识点中最为大众所熟知的一个了,也是Java开发者最关注的一块知识点。Java 语言开发者比 C 语言开发者幸福的地方就在于,我们不需要手动释放对象的内存,JVM 中的垃圾回收器(Garbage Collector)会为我们自动回收。但是这种幸福是有代价的:一旦这种自动化机制出错,我们又不得不去深入理解 GC 回收机制,甚至需要对这些“自动化”的技术实施必要的监控和调节。
上一节课我介绍了 Java 内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作,这几个区域内不需要过多考虑回收的问题。
而堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。
什么是垃圾
所谓垃圾就是内存中已经没有用的对象。 既然是”垃圾回收",那就必须知道哪些对象是垃圾。Java 虚拟机中使用一种叫作"**可达性分析”**的算法来决定对象是否可以被回收。
可达性分析
可达性分析算法是从离散数学中的图论引入的,JVM 把内存中所有的对象之间的引用关系看作一张图,通过一组名为”GC Root"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。如下图所示:
比如上图中,对象A/B/C/D/E 与 GC Root 之间都存在一条直接或者间接的引用链,这也代表它们与 GC Root 之间是可达的,因此它们是不能被 GC 回收掉的。而对象M和K虽然被对J 引用到,但是并不存在一条引用链连接它们与 GC Root,所以当 GC 进行垃圾回收时,只要遍历到 J/K/M 这 3 个对象,就会将它们回收。
注意:上图中圆形图标虽然标记的是对象,但实际上代表的是此对象在内存中的引用。包括 GC Root 也是一组引用而并非对象。
GC Root 对象
在 Java 中,有以下几种对象可以作为 GC Root:
Java 虚拟机栈(局部变量表)中的引用的对象。
方法区中静态引用指向的对象。
仍处于存活状态中的线程对象。
Native 方法中 JNI 引用的对象。
什么时候回收
不同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收。
Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
System.gc():在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。
代码验证 GC Root 的几种情况
现在我们了解了 Java 中的 GC Root,以及何时触发 GC,接下来就通过几个案例来验证 GC Root 的情况。在看具体代码之前,我们先了解一个执行 Java 命令时的参数。
-Xms 初始分配 JVM 运行时的内存大小,如果不指定默认为物理内存的 1/64。
比如我们运行如下命令执行 HelloWorld 程序,从物理内存中分配出 200M 空间分配给 JVM 内存。
复制java -Xms200m HelloWorld
验证虚拟机栈(栈帧中的局部变量)中引用的对象作为 GC Root
我们运行如下代码:
复制public class GCRootLocalVariable {
private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];
public static void main(String[] args){
System.out.println("开始时:");
printMemory();
method();
System.gc();
System.out.println("第二次GC完成");
printMemory();
}
public static void method() {
GCRootLocalVariable g = new GCRootLocalVariable();
System.gc();
System.out.println("第一次GC完成");
printMemory();
}
/**
* 打印出当前JVM剩余空间和总的空间大小
*/
public static void printMemory() {
System.out.print("free is " + Runtime.getRuntime().freeMemory()/1024/1024 + " M, ");
System.out.println("total is " + Runtime.getRuntime().totalMemory()/1024/1024 + " M, ");
}
}
打印日志:
复制开始时:
free is 242 M, total is 245 M,
第一次GC完成
free is 163 M, total is 245 M,
第二次GC完成
free is 243 M, total is 245 M,
可以看出:
当第一次 GC 时,g 作为局部变量,引用了 new 出的对象(80M),并且它作为 GC Roots,在 GC 后并不会被 GC 回收。
当第二次 GC:method() 方法执行完后,局部变量 g 跟随方法消失,不再有引用类型指向该 80M 对象