前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM02——JVM垃圾回收与性能调优

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

作者头像
用户10127530
发布2022-10-26 17:16:54
3660
发布2022-10-26 17:16:54
举报
文章被收录于专栏:半旧的技术栈半旧的技术栈
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%80%时。

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。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-09-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 6.垃圾回收
    • 6.1 判断垃圾
      • 6.1.1 引用计数法
      • 6.1.2 可达性分析算法
    • 6.2 五种引用
      • 6.2.1 强引用
      • 6.2.2 软引用
      • 6.2.3 弱引用
      • 6.2.4 虚引用
      • 6.2.5 终结器引用
    • 6.3 软引用应用
      • 6.4 弱引用
        • 6.5 回收算法
          • 6.5.1 标记清除算法
          • 6.5.2 标记整理算法
          • 6.5.3 复制算法
        • 6.6 分代回收机制
          • 6.7 GC分析
            • 6.8 垃圾回收器
              • 6.8.1 垃圾回收器分类
              • 6.8.2 串行垃圾回收器
              • 6.8.3 吞吐量优先
              • 6.8.4 响应时间优先
              • 6.8.5 G1垃圾回收器
            • 6.9 GC调优
              • 6.9.1 预备知识
              • 6.9.2 调优内容
              • 6.9.3 代码复核
              • 6.9.4 新生区内存调优
              • 6.9.5 幸存区调优
              • 6.9.6 老年代调优
              • 6.9.7 调优案例
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档