JVM02——JVM垃圾回收与性能调优

2022-10-26 17:16:54 浏览数 (2)

6.垃圾回收

6.1 判断垃圾
6.1.1 引用计数法

当一个对象被引用一次则计数 1,失去引用计数-1,当计数为0则判断为垃圾。但当对象间存在循环引用时(如下图)会无法被回收。

6.1.2 可达性分析算法

Java中使用可达性分析算法来判断垃圾。肯定不会被垃圾回收的对象为根对象,可以经由根对象直接或间接引用的对象不会被垃圾回收,反则反之。打个比喻:连在串上的葡萄就是不可以被回收的对象,散在盘中的葡萄就是可以被垃圾回收的。

哪些对象可以作为根对象呢?使用eclipse的MAT(memory analyzer)可以进行分析。这个工具比jvisual更加专业,可以找到内存泄漏。

运行如下代码。

代码语言:javascript复制
/**
 * 演示GC Roots
 */
public class Demo2_2 {

    public static void main(String[] args) throws InterruptedException, IOException {
        List<Object> list1 = new ArrayList<>();
        list1.add("a");
        list1.add("b");
        System.out.println(1);
        System.in.read();

        list1 = null;
        System.out.println(2);
        System.in.read();
        System.out.println("end...");
    }
} 

使用命令jps查看当前运行代码的进程为17332。

代码语言:javascript复制
PS F:资料 解密JVM代码jvm> jps
13472 Launcher
3296 Jps
12868 RemoteMavenServer36
17332 Demo2_2
11180

在list回收前、后分别使用jamp抓取目标进程内存的快照,转储为二进制文件,并设置live参数在抓取快照前主动触发垃圾回收。回收前的操作命令如下。

代码语言:javascript复制
jmap -dump:format=b,live,file=gcRootDemo.bin 17332

使用elipse的MAT工具可以来分析内存泄漏问题,官网下载安装MAT,https://www.eclipse.org/mat/downloads.php。

使用MAT工具,菜单栏file->open dump file打开刚才抓取的快照文件。打开文件时报错Invalid HPROF file header,其中一个原因为人工改变了文件的编码格式,重新抓取并不要改变编码格式。将两个文件都打开以方便对照。如下图,查看文件的GC Roots。

可以看到GC Root的具体情况,被分为了4类。

System Class是程序运行所必须的核心类。

第二类是执行本地方法时操作系统所引用的Java对象的类。

Busy Monitor是指正在加锁的对象,如果这对象被回收了,则锁无法被释放,故不会被回收。

最后是活动着的线程中,局部变量所引用的对象不能被当成垃圾回收。比如下图中的ArrayList其实就是对应代码中list1被垃圾回收前所指对象,在list对象回收后的抓取的内存快照gcRootDemo2.bin中该对象不存在了。

6.2 五种引用

java中有五种引用类型。

6.2.1 强引用

只要沿着GC Root可以找到该对象,则不会被垃圾回收。如图中A1对象,只有当B,C对象对A1的引用都断开时,才会被垃圾回收。

6.2.2 软引用

当发生垃圾回收且内存不够时,则会对其进行进行回收。如图中A2对象,当B对象的引用断开,那么进行垃圾回收且内存不够时,A2对象将会被回收。

6.2.3 弱引用

当发生垃圾回收时,就会对其进行回收。如图中A3对象,当B对象的引用断开,那么进行垃圾回收,A3对象将会被回收。

特别的,软引用和弱引用本身也属于对象,可以配合引用队列进行使用。

6.2.4 虚引用

虚引用与终结器引用必须配合引用对象进行使用。如前文中提到的ByteBuffer对象,会创建一个虚引用Cleaner,并且会将ByteBufer所分配的直接内存传递给Cleaner引用。当ByteBufferber对象被垃圾回收后,Cleaner会进入引用队列。ReferenceHandler会定期扫描引用队列中新入队的对象,当Cleaner被扫描到就会执行其clean()方法,调用Unsafe对象的freememory()将直接内存释放。

6.2.5 终结器引用

所有对象都继承自Object,而Object中有一个finalize()方法,对象可以重写finalize()方法,在对象进行垃圾回收时该方法将被调用。但是对象已经没有强引用了,finalize()方法怎么被调用呢?其实就是通过终结器引用实现的。在B对象断开A4的强引用后,终结器引用会被加入引用队列,由一个优先级很低的finalizeHandler进行扫描,当扫描到引用队列中的终结器引用后,会执行其所引用的A4对象的finalize()方法。由于finalize()方法不会被立刻执行,而是先进行入队,并且负责扫描的finalizeHandler优先级低,可能导致finalize()迟迟得不到执行,因此不推荐使用它进行资源回收.

6.3 软引用应用

配置运行下列代码,显然会报OutOfMemoryError。

代码语言:javascript复制
/**
 * 演示软引用
 * -Xmx20m 
 */
public class Demo2_3 {

    private static final int _4MB = 4 * 1024 * 1024;



    public static void main(String[] args) throws IOException {
       List<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 5; i  ) {
            list.add(new byte[_4MB]);
        }


    }
 }

而以上常见在实际编程中其实是常见的,比如在读取图片内容时。软引用可以解决这种内存占用的问题。

代码语言:javascript复制
 public static void soft() {
        // list --> SoftReference --> byte[]

        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i  ) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());

        }
        System.out.println("循环结束:"   list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }

以上代码中,list和SoftReference是强引用,但是SoftReference和byte[]是软引用。打印结果如下。

代码语言:javascript复制
[B@7f31245a
1
[B@6d6f6e28
2
[B@135fbaa4
3
[B@45ee12a7
4
[B@330bedb4
5
循环结束:5
null
null
null
null
[B@330bedb4

观察到在循环结束前可以调用到bye[]数组,但是循环结束前四个数组已经变成了null。

添加虚拟机参数:-XX: PrintGCDetails -verbose:gc,打印垃圾回收的细节与详细参数,然后再次运行查看垃圾回收完整过程。

代码语言:javascript复制
[B@7f31245a
1
[B@6d6f6e28
2
[B@135fbaa4
3
[GC (Allocation Failure) [PSYoungGen: 2209K->488K(6144K)] 14497K->13130K(19968K), 0.0024761 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@45ee12a7
4
[GC (Allocation Failure) --[PSYoungGen: 4696K->4696K(6144K)] 17338K->17502K(19968K), 0.0020330 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 4696K->4466K(6144K)] [ParOldGen: 12806K->12675K(13824K)] 17502K->17142K(19968K), [Metaspace: 3331K->3331K(1056768K)], 0.0100720 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) --[PSYoungGen: 4466K->4466K(6144K)] 17142K->17158K(19968K), 0.0024079 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 4466K->0K(6144K)] [ParOldGen: 12691K->740K(8704K)] 17158K->740K(14848K), [Metaspace: 3331K->3331K(1056768K)], 0.0118198 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[B@330bedb4
5
循环结束:5
null
null
null
null
[B@330bedb4
Heap
 PSYoungGen      total 6144K, used 4377K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
  eden space 5632K, 77% used [0x00000000ff980000,0x00000000ffdc64f0,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 8704K, used 740K [0x00000000fec00000, 0x00000000ff480000, 0x00000000ff980000)
  object space 8704K, 8% used [0x00000000fec00000,0x00000000fecb90f0,0x00000000ff480000)
 Metaspace       used 3352K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 359K, capacity 388K, committed 512K, reserved 1048576K

观察到在第三个循环结束后,内存已经快不足,进行了新生代回收。在第四个循环结束后,又进行了一次新生代的回收,但是效果不理想(4696K->4696K),于是触发了一次Full GC。由于进行垃圾回收且内存仍然不足,又触发了一次新的垃圾回收,将软引用所引用的对象释放。像缓存的图片等不重要的对象,可以通过软引用来引用,当内存空间不足时就会回收它们。

同时我们也注意到,前四个软引用所指的对象已经是null了,没有必要再把这四个软引用保留在list集合中。可以配合引用队列来完成软引用的回收。

代码语言:javascript复制
  public static void soft() {
        // list --> SoftReference --> byte[]
        List<SoftReference<byte[]>> list = new ArrayList<>();
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
        for (int i = 0; i < 5; i  ) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], referenceQueue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());

        }

        Reference<? extends byte[]> poll = referenceQueue.poll();
        while (poll != null) {
            list.remove(poll);
            poll = referenceQueue.poll();
        }
        System.out.println("循环结束:"   list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }

结果如下。

代码语言:javascript复制
[B@7f31245a
1
[B@6d6f6e28
2
[B@135fbaa4
3
[B@45ee12a7
4
[B@330bedb4
5
循环结束:1
[B@330bedb4

Process finished with exit code 0
6.4 弱引用

与软引用十分类似。

代码语言:javascript复制
/**
 * 演示弱引用
 * -Xmx20m -XX: PrintGCDetails 
 */
public class Demo2_5 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        //  list --> WeakReference --> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 10; i  ) {
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            for (WeakReference<byte[]> w : list) {
                System.out.print(w.get() " ");
            }
            System.out.println();

        }
        System.out.println("循环结束:"   list.size());
    }
}

打印的结果如下。其中第10 次循环时,由于弱引用本身也占有一定的内存,触发Full GC。

代码语言:javascript复制
[B@7f31245a 
[B@7f31245a [B@6d6f6e28 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 
[GC (Allocation Failure) [PSYoungGen: 2209K->504K(6144K)] 14497K->13139K(19968K), 0.0023913 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 [B@45ee12a7 
[GC (Allocation Failure) [PSYoungGen: 4712K->496K(6144K)] 17347K->13326K(19968K), 0.0025155 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null [B@330bedb4 
[GC (Allocation Failure) [PSYoungGen: 4704K->504K(6144K)] 17534K->13350K(19968K), 0.0020861 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null [B@2503dbd3 
[GC (Allocation Failure) [PSYoungGen: 4711K->504K(6144K)] 17557K->13382K(19968K), 0.0017767 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null null [B@4b67cf4d 
[GC (Allocation Failure) [PSYoungGen: 4710K->456K(6144K)] 17588K->13334K(19968K), 0.0017985 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null null null [B@7ea987ac 
[GC (Allocation Failure) [PSYoungGen: 4775K->504K(5120K)] 17653K->13398K(18944K), 0.0011480 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@7f31245a [B@6d6f6e28 [B@135fbaa4 null null null null null [B@12a3a380 
[GC (Allocation Failure) [PSYoungGen: 4735K->256K(5632K)] 17629K->13531K(19456K), 0.0016537 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 256K->0K(5632K)] [ParOldGen: 13275K->782K(8192K)] 13531K->782K(13824K), [Metaspace: 3345K->3345K(1056768K)], 0.0108212 secs] [Times: user=0.17 sys=0.00, real=0.01 secs] 
null null null null null null null null null [B@29453f44 
循环结束:10

当然,弱引用与软引用的区别是,只要触发垃圾回收,无论内存是否充足都会回收其引用对象。

代码语言:javascript复制
public class WeakReferenceDemo {
    public  static  void  main(String[] args) {

        WeakReference<String> sr =  new  WeakReference<String>( new  String( "hello" ));

        System.out.println(sr.get());
        System.gc();                 //通知JVM的gc进行垃圾回收
        System.out.println(sr.get());
    }
}

输出结果。

代码语言:javascript复制
hello
null
6.5 回收算法
6.5.1 标记清除算法

先标记(将不可被GC Root直接或者间接访问的内存标记),再清除(并不是做清零操作,而是被空闲的内存起始地址放入空闲内存表,下次分配内存时就可以使用)。这种方式的优点是速度快,缺点是容易产生内存碎片,比如存储一个数组对象,总的内存空间足够,但是内存不连续,依然会导致内存溢出问题。

6.5.2 标记整理算法

先标记,再整理(移动对象)。优点是内存连续,缺点是消耗一定的时间,(对象移动,同时对象的地址发生变化,如果对象有引用,那么引用中保存的地址也需要随之发生改变。)

6.5.3 复制算法

先标记,后复制。将对象从from移动到to区域,在移动过程中就完成了内存整理工作。同时交换from和to区。优点是空间连续,缺点是需要使用双倍的内存空间。

6.6 分代回收机制

JVM同时综合使用了三种垃圾回收算法。这就是分代回收机制。

内存空间可以分为新生代和老年代,新生代又可以分为伊甸园和幸存者from,幸存者to。之所以采用分代回收机制,是为了使不同的垃圾回收策略。新生代用于存放朝生夕死的对象,会频繁的进行垃圾清理。

一个对象被创建后,首先会放入新生代的伊甸园中。

当新生区内存无法放入新的对象时,会触发一次Minor GC,将根据根可达算法判断伊甸园和幸存区From中哪些对象可以被回收,对于没有被垃圾回收的对象,根据复制算法将其复制到幸存区to中,交换幸存区From和幸存区To,并将未被回收对象寿命增1。Minor GC会引发STW(Stop the world),即进行垃圾回收时其他用户线程会被暂停。之所以要触发STW是因为垃圾回收的过程中会改变对象的地址,如果不暂停其他线程,当其他线程找不到对象会发生混乱。因为大部分对象都会被垃圾回收,需要通过复制算法改变内存地址的对象并不多,Minor GC的SWT较短。

当幸存区中的对象寿命到了阈值(最大为15{4bit}),说明这些对象的生命周期较长,这些对象将会被移到老年代中,当内存资源较为紧张,新生代存放不下更多对象,也可能将对象移到老年代中。老年代的垃圾回收频率较低。

如果堆中新生代快满了,放不进新的对象,同时老年代也快满了,会先尝试触发Minor GC,空间仍然不足就会触发Full GC。对整个堆进行垃圾回收。因为Full GC时老年代的回收算法耗时,同时要回收的对象数量较多,Full GC的SWT时间较长。如果Full GC后内存仍然不足就会触发Out of Memory。

6.7 GC分析

下面我们通过实例对GC的过程分析。开始GC分析之前,先了解一些GC常用的一些参数。其中上表中的晋升是指新生代晋升到老年代。

参考下面代码,设置参数并运行。其中参数-XX: UserSerialGC是将垃圾回收器设置为UserSerialGC,这种垃圾回收器的幸存区不会进行自动调整,有助于我们观察现象。

代码语言:javascript复制
/**
 *  演示内存的分配策略
 */
public class Demo2_1 {
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20M -Xmx20M -Xmn10M -XX: UseSerialGC -XX: PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
    public static void main(String[] args) throws InterruptedException {

    }
}

打印信息如下。

代码语言:javascript复制
Heap
 def new generation   total 9216K, used 2311K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  28% used [0x00000000fec00000, 0x00000000fee41d50, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3268K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 347K, capacity 388K, committed 512K, reserved 1048576K

观察到我们分配的新生代内存是10M,但是打印的只有9M,这是因为伊甸园占用8M,幸存区From和To各占用1M,JVM认为幸存区中的内存始终有一块空间是需要空着的,不能存放内容,所以这部分空间没有被计算进来。

新生代的伊甸园只有8M内存,其中28%还已经被占用了,新增以下代码。

代码语言:javascript复制
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);

果然触发了Minor GC。垃圾回收前新生代占用2147k,垃圾回收后占用749k,新生代总大小9216K。堆空间回收前占用2147K,垃圾回收后占用749K,总大小19456K。由于数组被放入了list集合中,而list集合被根GC Root所访问,不会被垃圾回收,所以byte[]数组被移到了幸存区中。垃圾回收后放入了7M的对象。伊甸园占用率93%。

代码语言:javascript复制
[GC (Allocation Failure) [DefNew: 2147K->749K(9216K), 0.0128891 secs] 2147K->749K(19456K), 0.0129487 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
Heap
 def new generation   total 9216K, used 8327K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  92% used [0x00000000fec00000, 0x00000000ff366830, 0x00000000ff400000)
  from space 1024K,  73% used [0x00000000ff500000, 0x00000000ff5bb4d8, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)

再新增以下代码,创建一个1M大小的数组。

代码语言:javascript复制
list.add(new byte[_1MB]);

打印信息如下。触发了两次GC操作,在第二次GC操作时,幸存区已经无法容纳这个1M的byte[]对象了,因此部分对象从幸存区晋升到了老年代中。

代码语言:javascript复制
[GC (Allocation Failure) [DefNew: 2147K->748K(9216K), 0.0039741 secs] 2147K->748K(19456K), 0.0040840 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 8244K->26K(9216K), 0.0096121 secs] 8244K->7932K(19456K), 0.0096617 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 1216K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  14% used [0x00000000fec00000, 0x00000000fed29758, 0x00000000ff400000)
  from space 1024K,   2% used [0x00000000ff400000, 0x00000000ff406bb8, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 7905K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  77% used [0x00000000ff600000, 0x00000000ffdb8508, 0x00000000ffdb8600, 0x0000000100000000)
 Metaspace       used 3314K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 353K, capacity 388K, committed 512K, reserved 1048576K

下面介绍一种大对象直接晋升老年代的情况。将之前的代码注释,直接在list集合中添加8M的byte[]数组。

代码语言:javascript复制
list.add(new byte[_8MB]);

这种情况伊甸园肯定放不下这个数组,幸存区也放不下,JVM经过计算,发现即使触发了垃圾回收也无法在新生代存放这个对象,这种情况不会触发垃圾回收,如果老年代空间足够这个大对象就会直接晋升老年代。

代码语言:javascript复制
Heap
 def new generation   total 9216K, used 2478K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  30% used [0x00000000fec00000, 0x00000000fee6bbe8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
 Metaspace       used 3333K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 357K, capacity 388K, committed 512K, reserved 1048576K

如果新生代,老年代都不足以存放了,就会Out of Memory。

思考一个问题。如果一个非主线程的其他线程发生内存溢出,会导致整个java进程退出吗?实验下。

代码语言:javascript复制
 public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
            list.add(new byte[_8MB]);
        }).start();

        System.out.println("sleep....");
        Thread.sleep(1000L);
        System.out.println("I'm alive,Haha");
}

结果如下。一个非主线程的其他线程发生内存溢出,不会导致整个java进程退出。

代码语言:javascript复制
sleep....
[GC (Allocation Failure) [DefNew: 4796K->990K(9216K), 0.0038712 secs][Tenured: 8192K->9179K(10240K), 0.0052058 secs] 12988K->9179K(19456K), [Metaspace: 4269K->4269K(1056768K)], 0.0094779 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [Tenured: 9179K->9124K(10240K), 0.0038569 secs] 9179K->9124K(19456K), [Metaspace: 4269K->4269K(1056768K)], 0.0039093 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
	at cn.itcast.jvm.t2.Demo2_1.lambda$main$0(Demo2_1.java:20)
	at cn.itcast.jvm.t2.Demo2_1$$Lambda$1/1023892928.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)
I'm alive,Haha
Heap
 def new generation   total 9216K, used 349K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   4% used [0x00000000fec00000, 0x00000000fec57530, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 9124K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  89% used [0x00000000ff600000, 0x00000000ffee9060, 0x00000000ffee9200, 0x0000000100000000)
 Metaspace       used 4294K, capacity 4708K, committed 4992K, reserved 1056768K
  class space    used 467K, capacity 528K, committed 640K, reserved 1048576K
6.8 垃圾回收器
6.8.1 垃圾回收器分类

有三类垃圾回收器。

1、串行垃圾回收器

  • 单线程
  • 适合堆内存较小场景,适合个人电脑。

2、吞吐量优先

  • 多线程
  • 适合堆内存较大场景,需要多核CPU支持
  • 让单位时间内,SWT时间最短

3、响应时间优先

  • 多线程
  • 适合堆内存较大场景,需要多核CPU支持
  • 尽可能使单次响应SWT时间最少

注:吞吐量优先追求的是单位时间的STW时间最短,响应时间优先是追求每次响应的速度最快。举例如下,算法1:0.2s/次 * 2次 =0.4s,算法2:0.1s/次 * 5次 =0.5s,算法1进行垃圾回收的总时间最短,吞吐量更大。算法2的单次垃圾回收时间更短,响应速度更快。

6.8.2 串行垃圾回收器

使用-XX: UseSerialGC = Serial SerialOld可以开启串行垃圾回收器。其中新生代采用的算法是复制算法,老年代采用的是标记整理算法。在垃圾回收线程运行前,会先阻塞其他线程。

6.8.3 吞吐量优先

开启-XX: UseParrallelGC或者-XX: UseParrallelGC(开启一个另外一个会自动开启)使用吞吐量优先的垃圾回收器,其新生代算法仍为复制算法,老年代算法仍为标记整理算法。不过其特别之处在于:在垃圾回收前,用户线程会暂停,但是垃圾回收时会开启多个线程同时执行垃圾回收操作,开启的线程数量与cpu核数相同。当然,我们也可以使用-XX:ParallelGCThreads=n来指定进行垃圾回收的线程数量。参数-XX: UseAdaptiveSizePolicy可以使用自适应的策略来调整堆的大小,这里主要是新生代空间的调整。XX:GCTimeRatio=n用于设置除垃圾回收时间外的时间占比,假设-XX:GCTimeRatio=19 ,则垃圾收集时间为1/(1 19),默认值为99,即1%时间用于垃圾收集。-XX:ParrallelGCMills=ms用于调整每一次垃圾回收的暂停时间。但是XX:GCTimeRatio=n

-XX:ParrallelGCMills=ms这两个参数其实是有冲突的。当GCTimeRatio设置的更大,就要调整堆使堆更大,以增加吞吐量,而堆更大则每次垃圾回收的暂停时间就会更长。两者要进行合理取舍。(注:JVM的堆大小有起始值和最大值,堆在这个范围内进行大小调节)

6.8.4 响应时间优先

ConcMarkSweepGC是工作在老年代的垃圾回收器。望文生义,响应时间优先垃圾回收器采取的垃圾回收策略是标记清除法(快,无需内存移动)。其中Con是concurrent的缩写,这表示响应时间优先的垃圾回收器在某些阶段采用的是并发策略(在某些阶段仍需STW):垃圾回收线程和其他用户线程并行执行,这样显然有利于提高程序的响应性能,但是也会牺牲吞吐量,与CMS垃圾回收器配合的垃圾回收器为ParNewGC。不过,CMS垃圾回收器有时会并发失败,这时会采取补救措施,将CMS退化为SerialOld。

在老年代快满时,将会阻塞其他线程,然后由垃圾回收线程对于GC Root进行快速的标记,由于只标记GC Root,这个过程很短。然后其他线程就可以恢复执行了,同时垃圾回收与其他用户线程并发执行,垃圾回收并发标记除根对象外的其他要被回收的对象。在并发标记结束后再次STW,然后重新标记,防止由于用户线程的活动导致对象的地址发生变化。重新标记结束后用户线程又可以执行了,垃圾回收线程进行并发清理。

可以设置并行线程数和并发线程数,并行线程数一般与cpu的核数相同,一般建议将并发线程数设置为并发线程数的1/4,即垃圾回收线程与用户线程按照1:3来抢占cpu,并发执行。

在进行并发清理的过程中,不能把这个过程新产生的垃圾清理掉,这些垃圾需要下一次垃圾回收时进行清理,称为浮动垃圾。因为清理是并发的,可能还没有清理出足够的空间存放这部分浮动垃圾,因此不能够像其它垃圾回收器一样,等到堆内存不足了再进行垃圾回收,必须为他们预留空间,参数-XX:CMSInitiatingOccupancyFraction=percent可以用来设置执行垃圾回收的时机:当内存的占比达到设置值就执行垃圾回收。

有可能新生代的对象引用老年代的对象,在进行重新标记时,要对整个堆的对象进行扫描,包括新生代的对象,然后根据这个新生代的对象扫描整个老年区的对象,做可达性的分析。这样无疑会消耗时间。使用参数-XX:CMSScanvengeBeforeRemark会先使用ParNewGC对新生代进行扫描,将其中可以回收的对象进行回收。

标记清除可能会导致垃圾碎片过多,导致并发失败,CMS会退化为SerialOld,进行一次全面的垃圾整理。这无疑会造成很多的时间消耗,这也时CMS存在的一个问题。

6.8.5 G1垃圾回收器

JDK9的默认垃圾回收器,取代了之前的CMS垃圾回收器。

(1)垃圾回收阶段

首先是进行Young Collection,当老年代到达阈值时,进行Young Collection ConcurrentMark,最后进行Mixed Colletion。

(2)Young Collection

新创建的对象会被放入伊甸园。

当伊甸园满后(会分配总的大小)会进行Young Collection。通过复制算法将未被回收的对象移至幸存区。

当再次触发垃圾回收时,会将一部分没有被回收的幸存区对象移到老年代(达到年龄阈值),另一部分没有被回收的幸存区对象移到其他幸存区(未达到年龄阈值)。

(2)Young GC CM(Concurrent marking)

  • 在Young GC(阶段1)的同时进行GC Root初始标记
  • 在老年代内存占用达到阈值时,会触发并发标记(无SWT)

(3)混合收集

混合收集阶段会全面收集垃圾,但是值得注意的是可以通过参数设置最大暂停时间,为了达到最大暂停时间的设置目标,老年代的内存可能不会全部进行拷贝整理,而是优先整理垃圾最多的内存。这也称其为G1的原因(Garbage First).

(4)Full GC

对于SerialGC和ParrallelGC而言,当老年代内存不足发生的垃圾回收就是full gc,但是对于CMS和G1垃圾回收器,老年代进行回收时是并发操作的,并不会造成太长的SWT,并不是full gc,G1、CMS只有当垃圾回收的速度比垃圾产生的速度要慢时,导致老年也满了,退化为SerialGC,才会触发full gc。

(5) Young Collection跨代使用

在进行young collection时,我们要查找GC Root,有一部分根对象可能在老年代中存活,新生代的对象被老年代引用了,如果我们对于整个老年代的对象进行扫描,效率肯定很低。G1垃圾回收器采取卡牌策略,把老年代的块状空间进一步划分为卡牌,当某个卡牌中的对象引用了新生代中的对象,就被标记为脏卡。在新生代内存中使用RememberSet来记录所有的引用,在查找GC Root时就可以通过RememberSet直接关注到脏卡区域,无需扫描全部老年代内存空间。当引用关系发生变化时,post-writter barrier和dirty card queue会配合更新变化的引用关系,最后通过异步线程Concurent reinforce Threads更新RememberSet。

(6) Remark

通过post-writter barrier satb_mark_queue实现。具体过程如下。

在remark阶段,我们采用黑色表示已经完成mark处理的对象,用灰色表示正在进行remark操作的对象,用白色表示还未进行remark操作的对象。如下图,A已经完成mark,并且其被强引用,故不会被垃圾回收,B正在remark,C尚未开始mark。

如果B完成Remark,在对C进行mark时恰B与C的引用断开(标记阶段垃圾回收线程与用户线程是并发的),那么C就会被标记成为白色。如果在后面A又引用了C,那么垃圾回收时,C对象会被回收吗?答案当然是不会。这是因为当C对象被引用时,会执行post-writter barrier,并将C对象放入satb_mark_queue,置为灰色。在进行remark时,会对satb_mark_queue中的对象进行扫描,如扫描到被引用,则会将其置为黑色。

(7)字符串去重

在jdk8中,string字符串是存放在char数组中,如果通过new String的方式创建可能会导致重复创建。除了通过intern()方法来避免重复创建的发生,G1垃圾回收器会在新生代回收时并发检查是否存在重复创建的字符串,如果有则让他们指向同一个char数组。使用-XX: UseStringDeduplication可以开启字符串去重(默认打开)。

(8)类卸载

在jdk8u40后,所有对象在经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器所有的类都不再被使用后(主要是框架、自定义类加载器),就会尝试对这些类进行类卸载。使用-XX: ClassUnloadConcurrentWithMark可以开启类卸载(默认开启)。

(9)巨型对象

巨型对象指大小大于region的一半的对象,在jdk8u60后,可以回收巨型对象。

巨型对象有如下特别之处。

  • G1不会复制巨型对象
  • 垃圾回收时优先回收
  • 当某个巨型对象被老年代的incoming引用为0时,将会在新生代垃圾回收时被回收(参考下图)。

(10)动态调整阈值

并发标记必须在堆占满前完成,否则将退化为full gc(注:在新版本的jvm中,full gc已经不是前文所提到的单线程,但是仍然有很长的STW时间,需要避免)。在jdk9之前我们采用-XX:InitiatingHeapOccupancyPercent来设置开始并发标记的阈值,但是阈值如果设置过低则频繁GC,如果设置过高则易Full GC。jdk9中可以对于阈值进行动态调整。

jdk中还有很多对于垃圾回收器的改进。建议多读官网文档

6.9 GC调优
6.9.1 预备知识

(1) 常用命令

jvm调优需要对于一些常用的内存设置参数熟悉,可以查阅官网。或使用命令java -XX: PrintFlagsFinal -version | findstr "GC"查看jvm中与GC相关的虚拟机参数。

查看的结果示例如下。

代码语言:javascript复制
java version "16.0.2" 2021-07-20
Java(TM) SE Runtime Environment (build 16.0.2 7-67)
Java HotSpot(TM) 64-Bit Server VM (build 16.0.2 7-67, mixed mode, sharing)
    uintx AdaptiveSizeMajorGCDecayTimeScale        = 10                                        {product} {default}
     uint ConcGCThreads                            = 3                                         {product} {ergonomic}
     bool DisableExplicitGC                        = false                                     {product} {default}
     bool ExplicitGCInvokesConcurrent              = false                                     {product} {default}
    uintx G1MixedGCCountTarget                     = 8                                         {product} {default}
    uintx G1PeriodicGCInterval                     = 0                                      {manageable} {default}
     bool G1PeriodicGCInvokesConcurrent            = true                                      {product} {default}
   double G1PeriodicGCSystemLoadThreshold          = 0.000000                               {manageable} {default}
    uintx GCDrainStackTargetSize                   = 64                                        {product} {ergonomic}
    uintx GCHeapFreeLimit                          = 2                                         {product} {default}
    uintx GCLockerEdenExpansionPercent             = 5                                         {product} {default}
    uintx GCPauseIntervalMillis                    = 201                                       {product} {default}
    uintx GCTimeLimit                              = 98                                        {product} {default}
    uintx GCTimeRatio                              = 12                                        {product} {default}
     bool HeapDumpAfterFullGC                      = false                                  {manageable} {default}
     bool HeapDumpBeforeFullGC                     = false                                  {manageable} {default}
   size_t HeapSizePerGCThread                      = 43620760                                  {product} {default}
    uintx MaxGCMinorPauseMillis                    = 18446744073709551615                      {product} {default}
    uintx MaxGCPauseMillis                         = 200                                       {product} {default}
      int ParGCArrayScanChunk                      = 50                                        {product} {default}
    uintx ParallelGCBufferWastePct                 = 10                                        {product} {default}
     uint ParallelGCThreads                        = 13                                        {product} {default}
     bool PrintGC                                  = false                                     {product} {default}
     bool PrintGCDetails                           = false                                     {product} {default}
     bool ScavengeBeforeFullGC                     = false                                     {product} {default}
     bool UseAdaptiveSizeDecayMajorGCCost          = true                                      {product} {default}
     bool UseAdaptiveSizePolicyWithSystemGC        = false                                     {product} {default}
     bool UseDynamicNumberOfGCThreads              = true                                      {product} {default}
     bool UseG1GC                                  = true                                      {product} {ergonomic}
     bool UseGCOverheadLimit                       = true                                      {product} {default}
     bool UseMaximumCompactionOnSystemGC           = true                                      {product} {default}
     bool UseParallelGC                            = false                                     {product} {default}
     bool UseSerialGC                              = false                                     {product} {default}
     bool UseShenandoahGC                          = false                                     {product} {default}
     bool UseZGC                                   = false                                     {product} {default}

(2) 掌握常用工具

(3) 调优与代码、平台相关,无万能范式。

6.9.2 调优内容

(1)调优领域

  • 内存
  • 锁竞争
  • cpu占用
  • io

(2) 调优目标

  • 高吞吐量(科学运算):ParrellelGC
  • 还是低延迟(互联网项目):CMS、G1、ZGC
  • hotspot外的虚拟机:zing…
6.9.3 代码复核

查看full gc前后的内存占用,考虑以下几个问题。

  • 数据量是不是太多?下面代码就会加载大量数据到堆内存中。
代码语言:javascript复制
resultSet = statement.executeQuery("select * from xxx");

应该改为:

代码语言:javascript复制
resultSet = statement.executeQuery("select * from xxx limit n");
  • 数据表示太臃肿?
    • 对象用到什么数据项查什么数据项
    • 对象大小 Integer24byte ,int24byte
  • 是否存在内存泄漏?如:
代码语言:javascript复制
static Map map = new HashMap();

对长期存活的对象建议使用弱引用、软引用。

对于缓存类型的数据建议使用第三方缓存实现,如redis。

6.9.4 新生区内存调优

(1) 新生代的特点

  • 对象分配极其廉价:使用TLAB,即Thread Local allocation buffer(参考:浅析java中的TLAB - 简书 (jianshu.com)),避免了线程竞争,提高了内存分配的效率。
  • 对象的销毁代价小:采用复制算法整理内存,对于垃圾对象销毁代价小。
  • 大部分对象朝生夕死,minor gc时间远低于full gc

由于新生代具有以上特点,对于新生代进行内存调优效果更明显,往往进行内存调优时先考虑新生代的内存调优。

(2) 设置新生代内存大小

参数-Xmn可以设置新生代大小。如果新生代设置过小,将会导致minor gc频繁发生,耗费stw时间。如果新生代设置过大,或将导致只发生Full GC,占用的时间同样会很高,官方推荐设置为25%-50%。根据经验,我们一般将新生代的内存空间设置为:所容纳的最大并发量 * 一次请求响应的数据量。这样一次请求响应完成后大部分的内存将可以被释放,可以有效的减少GC的触发次数。

6.9.5 幸存区调优

(1) 设置幸存区大小

幸存区要至少能够存放当前活跃(将被回收)的对象 即将晋升(不被回收)的对象,如果幸存区的对象容纳不下,当前活跃的对象可能会被晋升到老年代,这就使一个本来拥有较短生命周期的对象在Full GC时才会被垃圾回收。

(2)设置合理晋升阈值

通过参数``-XX: PrintTenuringDistribution可以打印各个年龄的对象在内存中的占用,-XX: MaxTenuringThreshold=threshold`调整晋升阈值。如果幸存区的晋升阈值设置过大,则需要晋升到老年代的对象可能不会被及时晋升。而新生代进行Minor GC时耗费时间主要发生在复制对象上,这就会导致STW时间变长。

6.9.6 老年代调优

以CMS为例。

CMS的老年代的内存要尽可能大,避免浮动垃圾又导致内存溢出,使老年代退化。一般先进行新生代调优,有必要再考虑老年代调优。如果没有发生Full GC,一般无需对老年代进行调优,如果发生了Full GC,可以观察发生Full GC时老年代的内存占用超过的阈值,将老年代内存大小调大1/41/3.另外也可以用`-XX: CMSInitiatingOccupyFraction=percent`设置老年代垃圾回收的时机,一般推荐设置为内存占用75�%时。

6.9.7 调优案例
  • Full GC和Minor GC特别频繁

考虑新生代内存设置过小,通过增加新生代内存大小,避免频繁触发Minor GC,以及将生命周期较短的对象带入老年代进而引发Full GC。

  • 请求高峰期发生Full GC,单次占用时间特别长(CMS)

查看GC日志,查看到底时CMS各阶段耗费时间。CMS再重新标记时耗时最多,根据日志发现有1s。由于在重新标记阶段,不仅会扫描老年代的对象,还会扫描新生代的对象,并根据根可达算法进行扫描,考虑在业务高峰器,新生代对象较多。可以将CMS重新标记前先将新生代内存进行一次整理。

在这里插入图片描述

  • 老年代充足情况发生Full GC(CMS,jdk1.7)

查看输出提示信息并无并发失败、晋升失败等,说明老年代空间充裕,确认jdk版本为1.7,非jdk1.8。在jdk1.7及以前,方法区由元空间来管理,考虑元空间不足导致Full GC。

0 人点赞