java GC实战

之前的文章讲了java GC的理论(java内存垃圾回收),本篇文章我们来进行一次实战,稳固下知识。
为了清晰地看到java的GC过程,我写了个线程来帮忙,共2个类文件。

首先是Main函数所在的类:

package com.xingshulin;

public class GcTest {
    public static void main(String[] args) {
        // 为了能够看完整日志,等20s
        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 测试对象在新生代N岁时进入老年代
        String[] id = {"Test_MaxTenuringThreshold"};
        // 测试minorGC时候的GC情况
//        String[] id = {"Test_NewSize"};
        // 测试minorGC时候的GC情况
//        String[] id = {"Test_OldSize"};

        // 开始测试
        if(id[0].equals("Test_MaxTenuringThreshold")) {
            // 观察MaxTenuringThreshold=3时候的GC情况
            ThreadTest threadTest = new ThreadTest(0);
            new Thread(threadTest).start();
            while (true) {
                threadTest = new ThreadTest(1);
                new Thread(threadTest).start();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } else if(id[0].equals("Test_NewSize")) {
            // 观察minorGC时候的GC情况
            while (true) {
                ThreadTest threadTest = new ThreadTest(1);
                new Thread(threadTest).start();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } else if(id[0].equals("Test_OldSize")) {
            // 观察majorGC时候的GC情况
            while (true) {
                ThreadTest threadTest = new ThreadTest(2);
                new Thread(threadTest).start();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

然后是线程类:

package com.xingshulin;

public class ThreadTest implements Runnable {

    int num;

    public ThreadTest(int num) {
        this.num = num;
    }

    @Override
    public void run() {
        System.out.println("This is Thread " + num);

        // num=0的时候测试对象在新生代3岁时进入老年代
        if(num == 0) {
            // 新生代128M(-Xmn128m,eden=102.4M,To_Survivor=12.8M,From_Survivor=12.8M)
            // ~7M(8*1000000+16+8+padding)
            // 在64位的系统中, 数组占用内存为: 型别占用内存 * 数组长度 + 16(object overhead占用16bytes)+ 8(数组长度)+ padding
            long[] a = new long[1000000];
            while (true) {
                try {
                    System.out.println("This is Thread " + num + ",Thread is " + this.toString() + ",Array length is " + a.length);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } else if(num == 1) {
            // 新生代128M(-Xmn128m,eden=102.4M,To_Survivor=12.8M,From_Survivor=12.8M)
            // ~15M(8*2000000+16+8+padding)
            // 在64位的系统中, 数组占用内存为: 型别占用内存 * 数组长度 + 16(object overhead占用16bytes)+ 8(数组长度)+ padding
            long[] a = new long[2000000];
            System.out.println("This is Thread " + num + ",Thread is " + this.toString() + ",Array length is " + a.length);
        } else if(num == 2) {
            // 老年代384M(-Xms512m -Xmx512m,OldSize=512-128)
            // ~153M(8*20000000+16+8+padding)
            // 在64位的系统中, 数组占用内存为: 型别占用内存 * 数组长度 + 16(object overhead占用16bytes)+ 8(数组长度)+ padding
            long[] a = new long[20000000];
            System.out.println("This is Thread " + num + ",Thread is " + this.toString() + ",Array length is " + a.length);
        }
    }
}

通过代码,我们可以测试3种情况,当然,需要使用者把相应的行启用(将注解去掉即可),如下:

        // 测试对象在新生代N岁时进入老年代
        String[] id = {"Test_MaxTenuringThreshold"};
        // 测试minor GC时候的GC情况
//        String[] id = {"Test_NewSize"};
        // 测试full GC时候的GC情况
//        String[] id = {"Test_OldSize"};

在测试之前,我们需要固定好年轻代、老年代、MaxTenuringThreshold、log等参数,方便我们观察。

-Xmn128m -Xms512m -Xmx512m -XX:MaxTenuringThreshold=3 -XX:+PrintGCDetails -Xloggc:/Users/apple/Tom/gc.log

上面的参数代表新生代大小固定为128M、堆内存固定为512M、老年代为384M(512M-128M)、MaxTenuringThreshold为3、打印详细的GC日志及地址等。顺带说一句,新生代Eden区和Survivor区默认的比例是8:2,也就是Eden区是102.4M,两个Survivor区分别为12.8M。

1. 测试对象在新生代3岁时进入老年代

为了验证这个条件,代码中我启动了1个永久线程(num=0时)来验证对象在新生代3岁时进入老年代,另外的线程都是每秒启动但是很快会完结来达到触发minor GC的目的。


java GC实战_第1张图片
jstat

从jstat的结果我们可以看到,在第20s的时候,线程启动,Eden区到达了35.84%(程序启动时的8% + 永久线程对象比例~7.6M/102.4M=7.4% + 临时线程对象比例~15M/102.4M=14.6% + 其他对象)。在运行到24s时,由于Eden区满触发了第一次Minor GC,永久线程对象被转入一个Survivor区,达到了54.84%(~7.6M/12.8M)。接下来的29s、34s分别又触发了两次Minor GC,永久线程对象年龄到达了3岁。在第40s时,第四次Minor GC触发,永久线程对象被转入了老年代,占2.2%(~7.6M/384M)。

相应的,GC日志如下:

java GC实战_第2张图片
GC log

在这里需要留个坑,虽然我们实验的MaxTenuringThreshold=3时的GC行为是正常的,但是如果不设置MaxTenuringThreshold,也就是默认为15的时候,对象却不是在15岁的时候被进入老年代的,大约是在7岁时就被移入了。之前认为有可能是触发了 java内存垃圾回收中提到的“动态对象年龄判定”机制,但是经过试验也不是,还需要继续研究。

2. 测试minor GC时候的GC情况

相比于上面的情况,这个就简单了。我们只需要验证下Eden区满了后会触发Minor GC即可。将代码行 String[] id = {"Test_NewSize"} 注释打开,运行:


java GC实战_第3张图片
jstat

可以看到,从第20s到第46s,一共触发了4次Minor GC,我们的long数组对象没有被移入老年代,因为我们在这里只开启了临时线程,对象随时可以被回收。


java GC实战_第4张图片
GC log

3. 测试full GC时候的GC情况

将代码行 String[] id = {"Test_OldSize"} 注释打开,运行:


java GC实战_第5张图片
jstat

可以很明显的看到,这次增长的时老年代中的比例。原因是我们写入的long数组对象的大小达到了~153M,超过了新生代Eden区的大小,这种对象会直接进入老年代。和新生代一样,老年代满了后就会触发GC,但这个GC是full GC,而每次full GC时至少又会触发一次的minor GC,这个我们从结果上就可以看得很清楚了,比如第23s的时候。
只是有一点我还没有搞明白,我做实验的时候这个full GC每次都会跟随两次minor GC,很有规律,不知是不是jvm的要求。


java GC实战_第6张图片
GC log

你可能感兴趣的:(java GC实战)