垃圾回收机制(上)你真的懂了吗?

在讲垃圾回收机制之前,首先我们要明确什么是垃圾?怎么判定垃圾?判定垃圾之后又怎么回收?

1.1 什么是垃圾?

所谓垃圾就是内存中已经没有用的对象,那Java虚拟机中是怎么判定对对象是垃圾的呢?

1.2 如何判定是垃圾?

1.2.1 引用计数法

这个算法很简单,就是在引用一个对象的时候,计数器+1,在引用对象之后又“解绑”之后,计数器就-1,但是这种算法解决不了AB之间相互引用的问题。

1.2.2 可达性分析算法

可达性分析算法是从离散数学中的图论引入的,JVM 把内存中所有的对象之间的引用关系看作一张图,通过一组名为”GC Root"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。

image

比如上图中,对象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 无法回收还是比较致命的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容