在讲垃圾回收机制之前,首先我们要明确什么是垃圾?怎么判定垃圾?判定垃圾之后又怎么回收?
1.1 什么是垃圾?
所谓垃圾就是内存中已经没有用的对象,那Java虚拟机中是怎么判定对对象是垃圾的呢?
1.2 如何判定是垃圾?
1.2.1 引用计数法
这个算法很简单,就是在引用一个对象的时候,计数器+1,在引用对象之后又“解绑”之后,计数器就-1,但是这种算法解决不了AB之间相互引用的问题。
1.2.2 可达性分析算法
可达性分析算法是从离散数学中的图论引入的,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 也是一组引用而并非对象。
1.3 GC Root 对象
在Java 中,有以下几种对象可以作为GC Root:
1.Java虚拟机栈 (局部变量表)中引用的对象
2.方法区中静态引用指向的对象
3.仍处于存活状态中的线程对象
4.Native方法中JNI引用的对象
1.4 GC Root 对象何时被回收?
不同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收
1.Allocation Failure:在堆内存中分配时,可用内存不足导致对象内存分配失败,系统会触发一次GC。
2.System.gc():在应用层,代码调用此API。
2.1 代码验证
2.1.1 验证虚拟机栈(栈帧中的局部变量)中引用的对象作为 GC Root
public class GCRootLocalVariable {
private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];//80M
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 241 M, total is 245 M,
第一次GC完成
free is 163 M, total is 245 M,
第二次GC完成
free is 242 M, total is 245 M,
当第一次 GC 时,g 作为局部变量,引用了 new 出的对象(80M),并且它作为 GC Roots,在 GC 后并不会被 GC 回收。
当第二次 GC:method() 方法执行完后,局部变量 g 跟随方法消失,不再有引用类型指向该 80M 对象,所以第二次 GC 后此 80M 也会被回收。
注意:上面日志包括后面的实例中,因为有中间变量,所以会有 1M 左右的误差,但不影响我们分析 GC 过程。
2.1.2 验证方法区中的静态变量引用的对象作为 GC Root
public class GCRootStaticVariable {
private static int _10MB = 10 * 1024 * 1024;
private byte[] memory;
private static GCRootStaticVariable staticVariable;
public GCRootStaticVariable(int size) {
memory = new byte[size];
}
public static void main(String[] args){
System.out.println("程序开始:");
printMemory();
GCRootStaticVariable g = new GCRootStaticVariable(4 * _10MB);
g.staticVariable = new GCRootStaticVariable(8 * _10MB);
// 将g置为null, 调用GC时可以回收此对象内存
g = null;
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 241 M, total is 245 M,
GC完成
free is 163 M, total is 245 M,
可以看出:
当程序刚开始运行时内存为 241M,并分别创建了 g 对象(40M),同时也初始化 g 对象内部的静态变量 staticVariable 对象(80M)。
当第一次 GC:调用 GC 时,只有 g 对象的 40M 被 GC 回收掉,而静态变量 staticVariable 作为 GC Root,它引用的 80M 并不会被回收。
2.1.3 验证活跃线程作为 GC Root
public class GCRootThread {
private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];
public static void main(String[] args) throws InterruptedException {
System.out.println("开始前内存情况:");
printMemory();
AsyncTask at = new AsyncTask(new GCRootThread());
Thread thread = new Thread();
thread.start();
System.gc();
System.out.println("main方法执行完毕,完成GC");
printMemory();
thread.join();
at=null;
System.gc();
System.out.println("线程代码执行完毕,完成GC");
printMemory();
}
private 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");
}
private static class AsyncTask implements Runnable {
private GCRootThread gcRootThread;
public AsyncTask(GCRootThread gcRootThread) {
this.gcRootThread = gcRootThread;
}
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
开始前内存情况:
free is241M,total is245M
main方法执行完毕,完成GC
free is163M,total is245M
线程代码执行完毕,完成GC
free is243M,total is245M
可以看出:
当程序刚开始时是 241M 内存,当调用第一次 GC 时线程并没有执行结束,并且它作为 GC Root,所以它所引用的 80M 内存并不会被 GC 回收掉。。
当第二次 GC 时:thread.join() 保证线程结束再调用后续代码,线程已经执行完毕并被置为 null,这时线程已经被销毁,所以之前它所引用的 80M 此时会被 GC 回收掉。
2.1.4 测试成员变量是否可作为 GC Root
public class GCRootClassVariable {
private static int _10MB = 10 * 1024 * 1024;
private byte[] memory;
public GCRootClassVariable(int size) {
this.memory = new byte[size];
}
private GCRootClassVariable classVariable;
public static void main(String[] args) {
System.out.println("程序开始:");
printMemory();
GCRootClassVariable g = new GCRootClassVariable(4 * _10MB);
g.classVariable = new GCRootClassVariable(8 * _10MB);
g = null;
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 241 M, total is 245 M,
GC完成
free is 243 M, total is 245 M,
- 从上面日志中可以看出当调用 GC 时,因为 g 已经置为 null,因此 g 中的全局变量 classVariable 此时也不再被 GC Root 所引用。所以最后 g(40M) 和 classVariable(80M) 都会被回收掉。这也表明全局变量同静态变量不同,它不会被当作 GC Root。
上面演示的这几种情况往往也是内存泄漏发生的场景,设想一下我们将各个 Test 类换成 Android 中的 Activity 的话将导致 Activity 无法被系统回收,而一个 Activity 中的数据往往是较大的,因此内存泄漏导致 Activity 无法回收还是比较致命的。