一、如何判断对象可以回收

1、引用计数法

  对每个对象的引用次数进行计数,当引用次数为 0 时,进行释放。弊端:循环引用时,两个对象的计数都为 1,导致两个对象都无法被释放。

LBYM4H549HTBVFXZ6.png

2、可达性分析算法

  可达性分析算法:首先确定一系列根对象,所谓根对象就是肯定不会当做垃圾回收的对象。在垃圾回收前,首先对每个对象进行扫描,判断每个对象是否被根对象所直接或间接引用。

  • JVM中的垃圾回收器通过可达性分析来探索所有存活的对象。
  • 扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收。
  • 可以作为 GC Root 的对象:
    • System Class(系统类,核心类):被启动类加载器加载的类,这些类不可能被当做垃圾回收。
    • Native Stack:java 虚拟机在执行一些方法时,需要调用操作系统的方法。操作系统方法在执行时,需要引用一些 java 对象。
    • Thread:活动线程所使用的对象。线程中方法调用的栈帧,栈帧内使用的变量引用的对象都可以作为根对象。List<Integer> list = new ArrayList<>(); list 是一个引用存在于栈帧中,它是一个局部变量;而new ArrayList<>() 是存储在堆里,根对象指的是后面的对象,而不是前面的引用。
    • Busy Monitor:java 中的同步锁 synchronized,被加锁的对象不能当做垃圾。
  • 可以使用 MAT 工具对 java 堆内存进行分析,首先使用 jmap 抓取堆快照jamp -dump:format=b,live,file=文件名 进程号
    • -dump 表示对堆进行生成快照。
    • format 表示转储文件的格式,b 表示二进制格式。
    • live 表示只抓取存活的对象,使用该参数抓取快照前,会先进行一次垃圾回收。

3、五种引用

7C12BETPYD8BKWQBQWJ9.png

(1)强引用

  只有 GC Root 都不通过强引用引用该对象时,该对象才能被垃圾回收。如上图,当 B、C 对象都不引用 A1 对象时,A1 对象才会被回收。

(2)软引用

  仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次发出垃圾回收,回收该对象。可以配合引用队列来释放软引用自身。如上图、当 B 对象不引用 A2 对象且垃圾回收后内存仍不足时,会再次发生垃圾回收回收 A2 对象。

  软引用的应用,加上该参数 -Xmx20m -XX:+PrintGCDetails -verbose:gc 观察控制台打印情况,可以发现经过两次的 Full GC 软引用引用的对象才被回收。

public class Demo {

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

    public static void main(String[] args) throws IOException {
        // list 和 SoftReference 是强引用,而 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());
        }
    }
}

  软引用引用的对象被回收后,软引用本身不会被清理。如果想要清理软引用,需要使用引用队列。

public class Demo {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        List<SoftReference<byte[]>> list = new ArrayList<>();

        // 使用引用队列,用于移除引用为空的软引用对象
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for (int i = 0; i < 5; i++) {
            // 关联了引用队列, 当软引用所关联的 byte[] 被回收时,软引用自己会加入到 queue 中去
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }

        // 从队列中获取无用的 软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while (poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }

        System.out.println("===========================");
        for (SoftReference<byte[]> reference : list) {
            System.out.println(reference.get());
        }

    }
}
(3)弱引用

  仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会可能回收该对象。可以配合引用队列来释放弱引用自身。使用引用队列的方法和软引用类似。

public class Demo {
    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());
    }
}
(4)虚引用

  必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存。

(5)终结器引用

  所有的类都继承自 Object 类,Object 类有一个 finalize 方法。当对象重写了 finalize 方法,并且没有强引用引用它时,就可以被垃圾回收。在垃圾回收时 jvm 会创建相应的终结器引用,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用他的 finalizer 方法,第二次 GC 时才能回收被引用对象。(Finalizer 线程的优先级很低,所以不建议使用)

4、引用队列

  软引用和弱引用可以配合引用队列:虽然软/弱引用对象所引用的对象被回收了,但它们还可能被强引用着,为了将它们快速释放,可以使用引用队列。在软引用和弱引用所引用的对象被回收以后,会将这些引用放入引用队列中,一起回收这些软/弱引用对象。

  虚引用和终结器引用必须配合引用队列:虚引用和终结器引用在使用时会关联一个引用队列,在引用对象被回收时,将该引用加入引用队列。

二、垃圾回收算法

1、标记清除

EXP40FSC05WP938.png

  标记清除算法:虚拟机执行垃圾回收时,先采用标记算法确定可回收的对象,然后垃圾收集器根据标记清除相应的内容。清除后的空间并不是将内存空间的字节设为 0,而是记录下这段内存的起始结束地址,下次分配内存时,可以覆盖这段内存。

  特点:速度快;容易产生大量的内存碎片,可能导致大对象无法分配。无法分配后会导致 gc,从而影响效率。

2、标记整理

6CH55RX8OKHRVTB.png

  标记整理算法:虚拟机执行垃圾回收时,先采用标记算法确定可回收的对象。然后整理剩余的对象,可以避免产生内存碎片。因为整理需要耗费大量时间,所以效率低。

  特点:速度慢;没有内存碎片。

3、复制

4HW3VN40YD7OH3FZXHGG.png

  复制算法:将内存分为等大小的两个区域,FROM 和 TO(TO 中为空)。在进行垃圾回收时,先将被 GC Root 引用的对象从 FROM 放入 TO 中,再回收不被 GC Root 引用的对象。然后交换 FROM 和 TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。

  特点:不会有内存碎片;需要占用双倍内存空间。

三、分代垃圾回收

1、Minor GC

XOB581DGUPR679AXIWGM.png

  • 新生代存放都是朝生夕死的对象。
  • 老年代存放都是长时间存活的对象。

  新创建的对象都会放在新生代的伊甸园中,当伊甸园的内存不足时,就会进行一次垃圾回收,这个回收叫 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作)。

ZTGXS1SX2LARIU6VG2DO.png

  Minor GC 会将伊甸园和幸存区From中的存活对象复制到幸存区TO中,并将其寿命加1,再交换两个幸存区。

  再次创建对象时,若新生代的伊甸园又满了,则会再次触发 Minor GC。这时不仅会回收伊甸园中的垃圾,还会回收幸存区From中的垃圾。再将存活对象复制到幸存区TO中。回收后交换两个幸存区,并将幸存区中的对象寿命加1。

2、Full GC

PPLDZ0HSGJY27BZFY.png

  如果幸存区中对象的寿命超过某个阈值(最大为 15,4bit),就会被放入老年代中。如果新生代老年代中的内存都满了,就会先触发 Minor GC,再触发 Full GC,扫描新生代和老年代中所有不再使用的对象并回收。

3、GC 分析

(1)jvm 相关参数

| 含义 | 参数 |
| - | - |
| 堆初始大小 | -Xms |
| 堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
| 新生代初始和最大大小 | -Xmn 或(-XX:NewSize=size 和 -XX:MaxNewSize=size) |
| 幸存区比例 | -XX:SurvivorRatio=ratio(默认为 8,伊甸园 8,两个幸存区各 1) |
| 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
| 晋升阈值 | -XX:MaxTenuringThreshold=threshold |
| 晋升详细 | -XX:+PrintTenuringDistribution |
| GC详细 | -XX:+PrintGCDetails -verbose:gc |
| FullGC 前进行 MinorFC | -XX:+ScavengeBeforeFullGC(默认打开) |

(2)默认堆情况

  使用以下参数运行 java 程序。

-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC

  堆的默认情况。

Heap
// 新生代的空间,因为幸存区to无法直接使用,所以 total 是9M,used 是已使用的空间,后面的是内存地址。
 def new generation   total 9216K, used 1805K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
// 伊甸园
  eden space 8192K,  22% used [0x00000000fec00000, 0x00000000fedc3490, 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 3086K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 336K, capacity 388K, committed 512K, reserved 1048576K
(3)分配 7 MB

  当我们分配 7MB 的空间。

public class Demo {
    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;

    public static void main(String[] args) throws InterruptedException {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
    }
}

  因为伊甸园装不下 7MB 所以发生了一次 GC(Minor GC),可以发现伊甸园和幸存区的空间都发生了变化。

// GC 类型		新生代:回收前的内存占用->回收后的(总大小),垃圾回收的耗时] 整个堆的回收
[GC (Allocation Failure) [DefNew: 1640K->587K(9216K), 0.0010678 secs] 1640K->587K(19456K), 0.0010989 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 8165K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  92% used [0x00000000fec00000, 0x00000000ff366830, 0x00000000ff400000)
  from space 1024K,  57% used [0x00000000ff500000, 0x00000000ff592c08, 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)
 Metaspace       used 3112K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 338K, capacity 388K, committed 512K, reserved 1048576K
(4)分配 7 + 1 MB

  当我们先分配一个 7MB 再分配一个 1MB 的空间时。

public static void main(String[] args) throws InterruptedException {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_1MB]);
    }

  发生了两次 GC,第二 GC 将新生代的对象放入了老年代,虽然也没达到阈值,因为内存紧张,所以提前晋升了。MB

[GC (Allocation Failure) [DefNew: 1640K->589K(9216K), 0.0011331 secs] 1640K->589K(19456K), 0.0011619 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8084K->27K(9216K), 0.0047422 secs] 8084K->7780K(19456K), 0.0047669 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 1217K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  14% used [0x00000000fec00000, 0x00000000fed297b8, 0x00000000ff400000)
  from space 1024K,   2% used [0x00000000ff400000, 0x00000000ff406c58, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 7753K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  75% used [0x00000000ff600000, 0x00000000ffd92650, 0x00000000ffd92800, 0x0000000100000000)
 Metaspace       used 3143K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K
(5)分配 8 MB

  大对象直接晋升老年代,当直接添加一个 8MB 的对象时,虚拟机判断新生代的空间肯定不足时,会直接将其放入老年代。

public static void main(String[] args) throws InterruptedException {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
    }

  没有发生 GC。

Heap
 def new generation   total 9216K, used 1972K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  24% used [0x00000000fec00000, 0x00000000feded2e8, 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 3143K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K
(6)分配 8 + 8 MB

  分配 8 + 8 MB 的对象时,会抛出一个堆内存不足的溢出错误。在报错之前会先做一次 Full GC 的垃圾回收。注意:如果某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行。这是因为当一个线程抛出 OOM 异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常。

[GC (Allocation Failure) [DefNew: 1808K->604K(9216K), 0.0020061 secs][Tenured: 8192K->8795K(10240K), 0.0051692 secs] 10000K->8795K(19456K), [Metaspace: 3129K->3129K(1056768K)], 0.0082997 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [Tenured: 8795K->8777K(10240K), 0.0021452 secs] 8795K->8777K(19456K), [Metaspace: 3129K->3129K(1056768K)], 0.0021935 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 573K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   7% used [0x00000000fec00000, 0x00000000fec8f768, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 8777K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  85% used [0x00000000ff600000, 0x00000000ffe92500, 0x00000000ffe92600, 0x0000000100000000)
 Metaspace       used 3214K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 347K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at 力扣.A.jvm.Demo.main(Demo.java:25)

四、垃圾回收器

并行收集:指多条垃圾收集线程并行工作,但此时用户线程处于等待状态

并发收集:指用户线程与垃圾收集线程并发工作(不一定是并行的,可能会交替执行)。

吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是99%

1、串行

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

2XZQK33SUG799YXK4.png

  安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。

  开启串行垃圾回收器:-XX:+UseSerialGC=Serial + SerialOld。新生代使用 Serial(复制算法),老年代使用 SerialOld(标记整理算法)。因为是串行的,所以只有一个垃圾回收线程运行。且在该线程执行回收工作时,其他线程进入阻塞状态。

(1)Serial 收集器

  Serial 收集器是最基本的、发展历史最悠久的收集器。特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。

(2)ParNew 收集器

  ParNew 收集器其实就是 Serial 收集器的多线程版本。特点:多线程、ParNew 收集器默认开启的收集线程数与 CPU 的数量相同。在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题。

(3)Serial Old 收集器

  Serial Old是 Serial 收集器的老年代版本。特点:同样是单线程收集器,采用标记整理算法

2、吞吐量优先

  • 多线程;
  • 堆内存较大,多核 cpu(如果是单个 cpu,多个垃圾回收线程争抢一个 cpu,效率不如串行);
  • 让单位时间内,STW 的时间最短 0.2+0.2=0.4;
  • JDK1.8默认使用的垃圾回收器。

OUS4Z8TRZL6R7ULB.png

  开启吞吐量优先垃圾回收器:-XX:+UseParallelGC~ -XX:+UseParallelOldGC。新生代使用复制算法,老年代使用标记整理算法。

(1)Parallel Scavenge 收集器

  Parallel Scavenge 收集器与吞吐量关系密切,故也称为吞吐量优先收集器。特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)。可以通过 -XX: ParallelGCThreads=n 控制垃圾回收的线程数。

GC 自适应调节策略

  Parallel Scavenge 收集器可设置-XX:+UseAdaptivesizePolicy 参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、晋升老年代的阈值(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。

两个目标

  Parallel Scavenge 收集器可以根据设置的参数来调整堆的大小,来达到设置的期望模板。

  • -XX:GCTimeRatio=ratio:设置吞吐量的目标,调整垃圾回收的时间跟总时间的一个占比。即 1/(1+ratio),默认ratio 为 99(一般设置为 19)。也就是说默认情况下垃圾回收时间不能超过总时间的 1%,如果达不到则调整堆的大小,一般是增大堆的大小,使得垃圾回收不频繁。
  • -XX:MaxGCPauseMillis=ms:设置垃圾回收最大执行时间的目标(默认 200 毫秒)。
(2)Parallel Old 收集器

  Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用标记整理算法。

3、响应时间优先

  • 多线程;
  • 堆内存较大,多核 CPU;
  • 尽可能让单次 STW 的时间最短 0.1+0.1+0.1+0.1+0.1=0.5;
  • 尽量不影响其他线程运行。

9GT2NW9L5HA7S9XCG.png

(1)CMS 收集器

  Concurrent Mark Sweep:一种以获取最短回收停顿时间为目标的老年代收集器。特点:基于标记清除算法实现,并发收集、低停顿,但是会产生内存碎片,发生并发失效。应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务。

工作流程

  • 初始标记(initial-mark):只标记 GC Roots 能直接关联到的对象,速度很快。但是仍存在 Stop The World 问题。
  • 并发标记(concurrent-mark):进行 GC Roots Tracing 的过程(从 GC Roots 开始找到它能引用的所有其它对象的过程),可以与用户线程并发执行。
  • 重新标记(remark):为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题。
  • 并发清理(concurrent-sweep):对标记的垃圾进行清理回收,之后再产生的垃圾称为浮动垃圾,只能等到下次清理时,才能清掉。

相关参数

  • -XX :+UseConcMarkSweepGC~ -XX:+UseParNewGC ~ SerialOld:开启响应时间优先的参数。新生代使用复制算法;老年代使用并发标记清除算法,它可以和用户线程并发执行,并发失效和晋升失败后会退化到 SerialOld(单线程)。
  • -XX : ParallelGCThreads=n ~ -XX:ConcGCThreads=threads:前者并行执行时垃圾回收线程数(默认 4 );后者并发执行时垃圾回收线程数,一般设置为并行的 1/4。
  • -XX:CMSInitiatingOccupancyFraction=percent:执行垃圾回收的内存占比。比如:设置为 80,只要老年代的内存到 80% 时就进行垃圾回收。
  • -XX:+CNSScavengeBeforeRemark:在重新标记时,有一个特殊的场景,有可能新生代的对象引用老年代的对象,所以在重新标记时,需要扫描整个堆。判断老年代的对象是否存活,这样非常耗时会影响性能。因为新生代的对象比较多,而且可能很多对象是需要作为垃圾回收的。所以为了提升性能,可以使用这个参数,在重新标记之前先对新生代进行一次垃圾回收。这样的话就可以减少新生代中的对象,减少扫描的对象,减少重新标记的压力。

退化到 Serial GC

  • 并发失效:主要原因垃圾产生速度快于垃圾回收速度,导致老年代空间不足(可能是CMS 触发太晚导致的)。在新生代发生垃圾回收时,达到晋升年龄的对象会被移动到老年代中。如果老年代没有足够的空间容纳这个晋升对象,CMS 为了腾出老年代空间,就会从本来的 MinorGC 退化成 FullGC。FullGC 回收的同时,所有进程必须 Stop The World,并用单线程(SerialGC)开始垃圾回收。导致本来可以并发的 MinorGC 变得缓慢无比。
  • 晋升失败:主要原因内存碎片过多,CMS 开启新生代垃圾收集的时候,判断老年代似乎有足够空间容纳所有晋升对象。然而晋升的时候才发现老年代的空间竟然都是碎片化的,根本容纳不了一个完整的晋升对象。只能进行内存整理,所有应用运行的线程停止,CMS 开始对老年代进行整理和压缩。空间压缩要通过移动里面的对象,令这些对象排列好,所以晋升失败更加浪费时间。完成清理的堆空间变得规整和空余,继续运行应用。

4、G1(Garbage First)

IZS46ADCLXR1Q92KO.png

适用场景:

  • 同时注重吞吐量和低延迟。
  • 超大堆内存,会将堆划分为多个大小相等的 Region。
  • 整体上是标记整理算法,两个区域之间是复制算法。

相关 JVM 参数

  • -XX:+UseG1GC:开启 G1 收集器。JDK 9 默认开启,而且替代了 CMS 收集器。
  • -XX:G1HeapRegionSize=size:设置每个 Region 的大小,每个 Region 都有新生代,老年代等。
  • -XX:MaxGCPauseMillis=time:设置暂停时间目标,默认是 200 ms。
(1)G1 垃圾回收的三个阶段

RCP852N7AC31PLC729.png

Young Collection

  分区算法 Region:分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间。(E:伊甸园 S:幸存区 O:老年代)

  Young Collection 是对新生代的收集器,会 STW。当伊甸园被占满时会触发该收集器,采用复制算法将其复制到幸存区或老年代。

30A9OX7L46OWDNRBBOYC.png

Young Collection + CM

  • 在 Young GC 时,就会对 GC Root 进行初始标记,即找到根对象;
  • CM 指的是并发标记(从根对象出发找到其他存活的对象);
  • 在老年代占用堆内存的比例达到阈值时,在新生代收集器运行的同时,进行并发标记(不会 STW),阈值可以使用-XX:InitiatingHeapOccupancyPercent=percent 来设置,默认为 45%。

MJYQIDHQIIPUVPD8.png

Mixed Collection

  Mixed Collection 指混合收集器,会对 E、S、O 进行全面垃圾回收。

  • 最终标记(Remark)会 STW;
  • 拷贝存活(Evacuation)会 STW;
  • -XX:MaxGCPauseMillis=ms:用于指定最长的停顿时间。

:为什么有的老年代被拷贝了,有的没拷贝?

  因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)。

GGFX23IQV0BHD6HHFTDZN.png

(2)Full GC

SerialGC 和 ParallelGC

  • 新生代内存不足发生的垃圾收集 - minor gc;
  • 老年代内存不足发生的垃圾收集 - full gc。

CMS

  • 新生代内存不足发生的垃圾收集 - minor gc;
  • 老年代内存不足(老年代所占内存超过阈值);
    • 如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理;
    • 并发失效和晋升失败都会退化为串行收集,便会触发 Full GC。

G1

  • 新生代内存不足发生的垃圾收集 - minor gc;
  • 老年代内存不足(老年代所占内存超过阈值);
    • 并发标记 + 混合收集:如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理;
    • 如果垃圾产生速度快于垃圾回收速度,则会发生并发失败,退化为其他收集器,便会触发 Full GC。
(3)Young Collection 跨代引用

9QV759KY8KLEQ3J.png

  新生代垃圾回收的跨代引用问题(老年代中的对象引用新生代中的对象):新生代垃圾回收的过程,首先要找到根对象,然后进行可达性分析找到存活对象,接着进行复制操作等等。因为一些根对象来自于老年代,老年代的对象比较多,如果直接遍历老年代非常耗时,因此使用卡表技术

  卡表:对老年代进行细分,分为一个个卡(card,大约 512k),如果老年代中的对象引用了新生代中的对象,则将这个卡标记为脏卡。那么在查找对象时,就不需要遍历整个老年代,只需遍历脏卡即可。减少搜索范围,从而提升效率。

  • Remembered Set:它存在于 E 中,用于保存新生代对象对应的脏卡。
  • post-write barried + dirty card queue:在对象引用发生变更时,由写屏障去更新脏卡。它是一个异步操作,将需要更新的脏卡放入队列,由一个线程进行更新。
  • concurrent refinement threads 更新 Remembered Set。

W1VM941G4S59LP2D6RWP.png

(4)Remark(重新标记)

  在并发标记阶段,收集器处理对象的过程中。黑色:已被处理,需要保留的;灰色:正在处理中的;白色:还未处理的。在垃圾回收时,会根据对象的状态,将黑色的保留,白色的回收。

TDSPR3RLCKHM5YCKL.png

  在并发标记过程中,因为用户线程也在并发执行,有可能 A 被处理后并没有引用 C。但在并发标记结束之前 A 又引用了 C,这时就会用到 Remark(pre-write barrier + satb_mark_queue)。过程如下:

  • 当对象引用发生改变时,就会给该对象加一个写屏障,写屏障的代码将会执行。写屏障将该对象加到一个队列当中,并将其变为灰色(处理中)。
  • 在并发标记阶段结束后,进入重新标记阶段,会 STW。重新标记阶段会对队列中的对象进行重新处理。

6AUQRKYGPKSB7WBT0ED.png

(5)G1 的优化

JDK 8u20 字符串去重:-XX:+UseStringDeduplication(默认开启)

  • 将所有新分配的字符串(底层是 char[])放入一个队列;
  • 当新生代回收时,G1 并发检查是否有重复的字符串;
  • 如果它们的值一样,就让他们引用同一个 char[]。虽然它们是两个字符串对象,但它们底层引用的 char[] 数组是同一个;
  • 注意,与 String.intern() 的区别:
    • intern 关注的是字符串对象;
    • 字符串去重关注的是char[];
    • 在 jvm 内部,使用了不同的字符串表。

优点与缺点

  • 节省了大量内存
  • 新生代回收时间略微增加,导致略微多占用CPU

JDK 8u40 并发标记类卸载:-XX:+ClassUnloadingWithConcurrentMark(默认开启)

  所有对象都经过并发标记后,就能知道哪些类不再被使用。当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。

JDK 8u60 回收巨型对象

  • 一个对象大于 region 的一半时,就称为巨型对象;
  • G1 不会对巨型对象进行拷贝;
  • 回收时被优先考虑;
  • G1 会跟踪老年代所有 incoming 引用,如果老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉。

HEE145DL87RCB9JM35S.png

JDK 9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC。 可以让并发清理提前开始,来防止 FullGC。
  • JDK9 之前需要使用 -XX:InitiatingHeapOccupancyPercent 来设置并发清理开始的阈值。
  • JDK9 可以动态调整:
    • -XX:InitiatingHeapOccupancyPercent:用来设置初始值;
    • 虚拟机会进行数据采样并动态调整阈值;
    • 总会添加一个安全的空档空间,让堆的空间足够大。

JDK 9 更高效的回收,详见官方文档

五、垃圾回收调优

  使用该命令可以查看虚拟机的参数(路径为本电脑的 jdk 路径):Java 官方文档

"C:\software\Java\jdk1.8.0_211\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"
  • 调优的领域:内存、锁竞争、CPU 占用、IO。
  • 确定目标:低延迟/高吞吐量?从而选择合适的 GC:
    • 低延迟:CMS,G1,ZGC(JDK 12)
    • 高吞吐量:ParallelGC
    • 第三方:Zing
  • 查看 Full GC 前后的内存占用,考虑以下几个问题:
    • 数据量是不是太大?
      • 查询数据表时,没必要取出全部数据,可以使用分页,取出需要的数据量。
    • 数据表示是否太臃肿?
      • 对象图:尽量不要 select *,需要哪些属性取哪些属性。
      • 对象大小:引用类型最小 16字节,Integer 24字节,int 4 字节。
    • 是否存在内存泄露?
      • 软/弱引用,缓存使用第三方。

1、新生代调优

新生代的特点

  • 所有的 new 操作的内存分配非常廉价;
    • TLAB thread-local allocation buffer:线程局部内存分配缓冲区,防止并发内存分配冲突。
  • 死亡对象的回收代价是零,大部分对象用过即死;
  • Minor GC 的时间远远低于Full GC。

新生代内存越大越好么?

  • 新生代小,容易发生 Minor GC,造成 STW;
  • 新生代大,老年代的内存就变小了。老年代的内存不足,触发的就是 Full GC;
  • 新生代能容纳所有 (并发量 * 请求响应)的数据,Oracle 建议新生代占堆的 25%~50%。

幸存区调优

  • 幸存区大到能存储,当前活跃对象 + 需要晋升对象;
  • 晋升阈值配置得当,让长时间存活的对象尽快晋升;
    • 晋升阈值:-XX:MaxTenuringThreshold=threshold
    • 晋升详细:-XX:+PrintTenuringDistribution

2、老年代调优

以 CMS 为例

  • CMS 的老年代内存越大越好,防止并发失效;
  • 先不对老年代做做调优,先尝试调优新生代;
  • 观察发生 Full GC 时,老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3;
    • -XX:CMSInitiatingOccupancyFraction=percent:执行垃圾回收的内存占比。

标题:JVM 垃圾回收
作者:Yi-Xing
地址:http://zyxwmj.top/articles/2021/01/24/1611502585821.html
博客中若有不恰当的地方,请您一定要告诉我。前路崎岖,望我们可以互相帮助,并肩前行!