Java 语言开发者比 C/C++ 语言开发者不需要手动释放对象的内存,JVM 中的垃圾回收器(Garbage Collector)会为我们自动回收。自动完成必然有要其他价值付出,比如这种自动化机制出错,程序员就不得不去深入理解 GC 机制,避免不能写出GC不能够完成对象回收的代码逻辑,甚至需要对这些“自动”技术实施必要的监控和优化。
GC 机制
GC(Garbage Collection)回收的内容就是对内存中已经没有用的对象清除动作。 既然是”垃圾回收",那就必须知道哪些对象是不再用的垃圾。Java 虚拟机中使用一种叫作 可达性分析 的算法来决定对象是否可以被回收。
可达性分析
可达性分析算法是从离散数学中的图论引入。JVM 以”GC Root"的对象作为起始点,把内存中所有的对象间的引用关系看作一张图,从起点开始开始搜索,所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。如下图所示:
GC Root 对象
在 Java 语言中,有以下几种对象可以作为 GC Root:
1、Java 虚拟机栈(栈帧中的局部变量表)中的引用的对象。
2、方法区中类静态属性常量引用的对象。
3、仍处于存活状态中的线程对象。
4、Native 方法中 JNI 引用的对象。
用代码实操验证 GC Root
验证Java虚拟机栈(栈帧中的局部变量)中引用的对象作为 GC Root:
public class GCRootFrameLocalVar {
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() {
GCRootFrameLocalVar g = new GCRootFrameLocalVar();
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 ");
}
}
打印LOG:
解析:
-
第一次 GC:局部变量g,引用了 new 出的对象GCRootFrameLocalVar,大小为80M,并且它作为 GC Roots,在 GC 后并不会被 GC 回收。
-
第二次 GC:method() 方法执行完后,局部变量 g 跟随方法消失,不再有引用类型指向GCRootFrameLocalVar对象,所以第二次 GC 后,对象GCRootFrameLocalVar也会被回收,释放80M空间。
验证方法区中的类静态属性静态变量引用的对象作为 GC Root
public class GCRootStaticVar {
private static int _10MB = 10 * 1024 * 1024;
private byte[] memory;
private static GCRootStaticVar staticVar;
public GCRootStaticVar(int size) {
memory = new byte[size];
}
public static void main(String[] args){
System.out.println("程序开始:");
printMemory();
GCRootStaticVar g = new GCRootStaticVar(4 * _10MB);
g.staticVar = new GCRootStaticVar(8 * _10MB);
g = null; //将g置为null告诉GC可以回收该对象占用内存空间
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 ");
}}
打印LOG:
小结:
程序刚开始运行时内存为 242M,创建了GCRootStaticVar对象g,大小为40M;同时也初始化 g对象内部的静态变量 staticVar 对象,大小为80M。调用 GC 时,局部对象g的 40M空间被 GC 回收掉,而类静态变量 staticVar 作为 GC Root 引用的 80M空间就不会被回收。
验证活跃线程作为 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 Exception {
System.out.println("开始前内存情况:");
printMemory();
AsyncTask at = new AsyncTask(new GCRootThread());
Thread thread = new Thread(at);
thread.start();
System.gc();
System.out.println("main方法执行完毕,完成GC");
printMemory();
thread.join(); // thread.join() 保证线程结束再调用后续代码,
at = 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 ");
}
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(Exception e){}
}
}
}
打印LOG:
小结:
程序刚开始时内存是 242M ,第一次屌用 GC 线程仍在执行,并且以它作为 GC Root,所以它所引用的 80M 内存并不会被 GC 回收掉。当调用第二次 GC 时,线程已经执行完毕并AsyncTask对象被置为null,此刻线程已经被销毁,它所引用的 80M空间会被 GC 回收掉。
验证成员变量作为 GC Root
这一块就不贴代码了,有兴趣对同学可以亲自尝试一下,不过这里需要提示注意,全局变量同静态变量不同,它不会被当作 GC Root。
总结
对于从事 Android 开发的工程师来说,有时候GC回收会很大程度上影响 UI 线程,并造成界面卡顿现象。既然以上情况会作为GCRoot 被引用从而也就是内存泄漏发生的场景,如果将以上各个类换成 Android 中的 Activity 将导致 Activity 无法被系统回收,而 Activity 占用内存空间往往是较大的,因此导致 Activity 无法回收出现内存泄漏是比较致命的。