阅读 52

JVM 阅读GC log,以及各种VM参数的使用

前言

  • 要会阅读GC log,来理解各种垃圾集器。
  • 理解内存区域的作用以及调整JVM 内存大小。
  • 要会操作创建对象时使用哪个区域。

使用VM参数,阅读GC log

代码及VM参数,触发Minor GC

  • VM参数
-Xms20M  // 堆的最大值
-Xmx20M  // 堆的最小值
-Xmn10M  // 年轻代大小   即 Eden 和 两个Servivor的总大小,那么20-10=10M就是老年代的大小
-XX:+PrintGCDetails // 打印GC 日志
-XX:SurvivorRatio=8  //  设置Eden所占年轻代的比率
复制代码

image.png

  • 回顾一下内存划分

image.png

  • 代码
public class GCLogTest {
    //什么都不写,年轻代耗费2032k内存,不同机器应该会不一样
    public static void main(String[] args) throws ClassNotFoundException {
        int size = 1024*1024;//M
        byte[] bytes = new byte[4*size];//4M
        byte[] bytes1 = new byte[3*size];//3M
        byte[] bytes2 = new byte[3*size];//3M
        System.out.println("GC log");
        //内存中不止有创建的byte[],还有运行Java程序所必须使用的对象。
    }
}
复制代码

image.png

  • 解释日志含义

image.png

前面明明设置了年轻代为 10M 为什么日志却是9216k?

  • 年轻代有一个Eden区和两个Serviver,即s0和s1。
  • 前面设置Eden区占8 ,那么s0和s1各占1。
  • 在使用时只会用s0和s1中的其中一个。另外一个一定是不使用的。所以就少了1024k
  • 即9216+1024 = 10240k = 10M,就是堆的设置大小和实际大小会差1024k。被其中一个Serviver区浪费了。

image.png

观察到老年代使用了4104k,如何得来?

  • 可以通过GC前后的内存变化来计算。
  • 5976 - 840 = 5136k,表示年轻代释放了5136k容量。
  • 年轻代释放的内容可能去了老年代或者已经被抹除了。
  • 5976 - 4944 = 1032k,表示整个堆释放的空间为1032k。
  • 5136 - 1032 = 4104k,表示年轻代释放的内容中抹除了1032k,剩下的4104k去了老年代。

代码,触发Full GC

  • 代码
public class GCLogTest {
    //什么都不写年轻代,耗费2032k内存,不同机器应该会不一样
    public static void main(String[] args) throws ClassNotFoundException {
        int size = 1024*1024;//M
        byte[] bytes = new byte[4*size];//4M
        byte[] bytes1 = new byte[4*size];//4M
        byte[] bytes2 = new byte[3*size];//3M
        System.out.println("GC log");
    }
}
复制代码

image.png

  • 解释一下Full GC

image.png

创建对象比上面触发时Full GC 容量大,却没有触发Full GC

上面的代码是11M byte[],触发了Full GC

  • 创建13M byte[]
public class GCLogTest {
    //什么都不写年轻代,耗费2032k内存,不同机器应该会不一样
    public static void main(String[] args) throws ClassNotFoundException {
        int size = 1024*1024;//M
        byte[] bytes = new byte[4*size];//4M
        byte[] bytes1 = new byte[3*size];//3M
        byte[] bytes2 = new byte[size];//1M
        byte[] bytes3 = new byte[5*size];//5M
        System.out.println("GC log");
    }
}
复制代码

image.png

  • 明明创建的内容比较大,为什么没有触发Full GC
    • 对象无法在新生代创建时,就直接在老年代创建。
      • 不要想着将对象拆分了,一个部分在新生代,另一部分在老年代,这是不可能的。
  • 如下的情况也是会直接在老年代创建
public class GCLogTest {
    //什么都不写年轻代,耗费2032k内存,不同机器应该会不一样
    public static void main(String[] args) throws ClassNotFoundException {
        int size = 1024*1024;//M
        byte[] bytes = new byte[8*size];//8M ,加上其它的,会超过新生代大小
        System.out.println("GC log");
    }
}
复制代码

image.png

public class GCLogTest {
    //什么都不写年轻代,耗费2032k内存,不同机器应该会不一样
    public static void main(String[] args) throws ClassNotFoundException {
        int size = 1024*1024;//M
        byte[] bytes = new byte[11*size];//11M ,对象超过了新生代和老年代的大小
        System.out.println("GC log");
    }
}
复制代码

image.png

阈值

在终端使用java -XX:+PrintCommandLineFlags,查看JVM启动参数。
简单聊一聊UseCompressedOops 和 UseCompressedClassPointers这两个JVM参数。

通过一个VM 参数设置会在老年代创建对象的阈值

  • 就是设置一个阈值,当创建的对象大于这个阈值时,不管新生代的空间够不够都去老年代创建
  • 以下为本次VM参数
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=4194304  //阈值为4M
复制代码
  • 代码,创建的字节数组为5M
public class ThresholdTest {
    public static void main(String[] args) {
         int size = 1024*1024;
         byte[] bytes = new byte[5*size];
    }
}
复制代码

image.png

要想阈值起作用,垃圾收集器得是单线程的

  • 在原来的VM 参基础上启动单线程垃圾收集器
-XX:+UseSerialGC  
复制代码

image.png

设置对象在Servivor区年龄的阈值

  • Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。
  • 新生代几乎是所有 Java 对象出生的地方,新生代是 GC 收集垃圾的频繁区域。
  • 当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些 仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1。
  • 当年龄到达一定值时对象如果还在Servivor区复制来复制去的,就会被移到老年代。
  • -XX:MaxTenuringThreshold:表示当年龄到达设置的最大值时会被移到老年代。
    • 该参数的默认值为15,CMS中默认值为6,G1中默认为15(在Jvw中,该数值是由4个bit来表示的,所以最大值 1111,即15)。
    • 是设置的最大值,对象有可能在年龄到达最大值时被移到老年代,但是年龄到达设置的最大阈值时一定会被移到老年代。
    • 使用 -XX:TargetSurvivorRatio=value JVM 会根据情况动态设置它的大小,但不会超过手动给它设置的值。
  • 经历了多次GC后,存活的对象会在From Survivor与To Survivor之间来回存放,而这里面的一个前提则是这两个空间有足够的大小来存放这些数据
  • 在GC算法中,会计算每个对象年龄的大小,如果达到某个年龄后发现总大小已经大于了Survivor(其中一个)空间的50%,那么这时就需要调整阔值,不能再继续等到默认的15次GC后才完成普升(移到老年代),因为这样会导致Survivor空间不足,所以需要调整阈值,让这些存活对象尽快完成晋升。

演示一下,对象从年轻代以到老年代的过程

  • 以下为本次代码 VM 参数
-Xmx200M // 堆大小 200M
-Xmn50M  // 新生代大小 
-XX:TargetSurvivorRatio=60 // 当一个Servivor区中的对象大于 Servivor总大小的 60 % 时,
                            // 重新估计TenuringThreshold,即更新年龄为多少会进入老年代
-XX:+PrintTenuringDistribution  // 打印对象年龄
-XX:+PrintGCDetails  //  打印GC 日志
-XX:+PrintGCDateStamps  //  打印 GC 时的时间
-XX:+UseConcMarkSweepGC  //   老年代使用 CMS 垃圾收集器
-XX:+UseParNewGC   // 新生代使用 ParNew收集器
-XX:MaxTenuringThreshold=3 // 年龄阈值,最大的
复制代码

本次并没有指定Serivivor占多少

  • 代码
public class TenuringTest {
    public static void main(String[] args) throws InterruptedException {
        byte[] bytes = new byte[512 * 1024];
        byte[] bytes1 = new byte[512 * 1024];//main方法没有结束,就不会被抹除
        for (int i = 1; i < 7; i++) {
            willGC();
            Thread.sleep(1000);
            System.out.println("loop: 00" + i);
        }
        byte[] bytes2 = new byte[1024 * 1024];
        byte[] bytes3 = new byte[1024 * 1024];
        byte[] bytes4 = new byte[1024 * 1024];
        byte[] bytes5 = new byte[1024 * 1024];
        willGC();
        System.out.println("loop: 007");
        Thread.sleep(1000);
        willGC();
        System.out.println("loop: 008");
        Thread.sleep(1000);
        System.out.println("end...");
    }

    private static void willGC() {//调用完,创建的对象就可以消除了
        for (int i = 0; i < 40; i++) {
            byte[] bytes = new byte[1024 * 1024];// 1 M
        }
    }
}
复制代码

image.png

(max 3) 表示动态判断确定的阈值最大值为3,由-XX:MaxTenuringThreshold=3决定。
Desired survivor size 3145728 bytes 怎么来的?

  • 因为没有指定Servivor的比例 所以默认8:1:1,-Xmn50M指定新生代大小为50M ,所以 40:5:5
  • -XX:TargetSurvivorRatio=60,表示其中一个Serivior区超过的60%重新判断 阈值(threshold), 即 5 * 60% = 3M = 3145728 bytes
  • 也说在其中一个Serivior区对象大小到3145728 bytes 时,重新判断阈值。

小结

-Xms20M  // 堆的最大值
-Xmx20M  // 堆的最小值
-Xmn10M  // 年轻代大小   即 Eden 和 两个Servivor的总大小,那么20-10=10M就是老年代的大小
-XX:+PrintGCDetails // 打印GC 日志
-XX:SurvivorRatio=8  //  设置Eden所占年轻代的比率
-XX:+UseSerialGC  // 使用单线程垃圾收集器,包括新生代与老年代
-XX:PretenureSizeThreshold=4194304  //创建对象的大小阈值为4M,超过就直接在老年代创建
-XX:TargetSurvivorRatio=60 // 当一个Servivor区中的对象大于 Servivor总大小的 60 % 时,
                            // 重新估计TenuringThreshold,即更新年龄为多少会进入老年代,
-XX:+PrintTenuringDistribution  // 打印各年龄对象的大小
-XX:+PrintGCDetails  //  打印GC 日志
-XX:+PrintGCDateStamps  //  打印 GC 时的时间
-XX:+UseConcMarkSweepGC  //   老年代使用 CMS 垃圾收集器
-XX:+UseParNewGC   // 新生代使用 ParNew收集器
-XX:MaxTenuringThreshold=3 // 年龄阈值,最大的
复制代码
文章分类
后端
文章标签