03 JVM中垃圾回收算法以及典型的垃圾回收器

1 垃圾回收算法

1-1 标记清除算法

算法概述

优点:回收速度快

缺点:造成内存碎片,无法分配大的连续空间。

算法思想

step1: 第一次扫描,通过GC root对象判断堆内存中哪些对象可以进行垃圾回收,进行标记

step2: 第二次扫描, 将那些标记的GC root对象进行垃圾回收,只需要将起始内存地址与终止内存地址放入空闲内存区就行。

1-2 标记整理算法

第一个依旧是标记,第二步会进行一个空间整理,从而不产生碎片。

优点:避免了内存碎片

缺点:对空间的整理使得效率比较低下。

1-3 复制算法

特点

  • 将管理的内存分为2块区域,from区域与to区域,将那些不需要回收的对象从from区域拷贝到to区域。复制的过程中完成内存区域的整理。
  • 之后交换from和to的指向。

优点:不会产生内存碎片

缺点:需要双倍的内存空间,内存利用率不高,而且拷贝也需要时间。



1-4 三种垃圾回收算法总结

垃圾回收算法 优点 缺点
标记清除算法(Mark Sweep) 速度较快 产生内存碎片
标记整理算法(Mark Compact) 没有内存碎片 速度慢
复制算法(Copy) 没有内存碎片 需要占用双倍内存空间

注意:实际的JVM垃圾回收算法中上面的三种算法是综合使用的

2 JVM分代回收算法

2-1 概述

Garden of Eden:伊甸园 garbage:垃圾

  • 新生代主要由三部分内容组成,分别是Eden区,幸存区from,幸存区to。 通常情况下只有Eden区与幸存区from会存放数目,幸存区to只有垃圾回收时,复制对象会用到。堆内存的新生代进行一次垃圾回收(Minor GC),大部分对象都会都会被回收。
  • 老年代通常存放一些经常被使用的对象,一个对象如果经历多次垃圾回收仍然幸存,那么该对象会从新生代放入老年代。只有新生代内存不足并且老年代内存也不足的时候才会触发full GC对老年代的对象进行垃圾回收

为什么需要进行划分?

实际环境中,对象的生命周期是不同的,老年代的对象生命周期比较长,可能很长时间才进行一次垃圾回收。新生代的对象生命周期比较短,垃圾回收比较频繁。这种分区法方便采用不同的垃圾回收算法更加有效的进行垃圾回收

2-2 分代垃圾回收示例

step1:程序刚刚开始运行,产生的对象先放入Eden区,当Eden区放不下的时候。

step2:对Eden区进行Minor GC,并将没有被垃圾回收的对象复制的幸存区To,然后交换幸存区To和幸存区From,第一次垃圾回收的最终的效果如下图所示

step3: 第一次Minor GC, Eden区又有空间可以分配给新的对象使用,经过一段时间Eden又不够用了,触发第二次Minor GC, 这次垃圾会检查Eden区以及幸存区From哪些对象可以存活,并将这些对象复制到幸存区To,然后交换幸存区To和幸存区From,这个时候Eden区又空了出来,可以放置新的对象

  • 实际垃圾回收过程中,JVM会对每个对象经过垃圾回收幸存下来的次数进行记录,比如上图中,幸存区的2个对象经过垃圾回收的次数分别是1和2。

step4: 当一些对象经过垃圾回收的次数仍然幸存的次数达到一个阈值(说明这个对象价值比较高),那么这个对象会被移动到老年代。

极端情况考虑:Eden区,from区,老年区都已经满了?

此时会触发Full GC(优先Minor GC,Minor GC依旧内存不够)

2-3 分代垃圾回收的总结

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的
    对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
    • 暂停时间较短,由于新生代大部分对象都是垃圾,复制的对象很少,所以效率较高
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit,对象头存储)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时
    间更长
    • Full GC 的stop the world的时间要比MInor GC时间长,老年代存活对象较多加上空间整理时间,所以停止时间会较长。
    • 如果Full GC后,空间仍然不足会触发内存不足的异常

2-4 垃圾回收相关的虚拟机参数

参数含义 参数 备注
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) NewSize是初始大小,MaxNewSize是最大大小。
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy 幸存区的比例,默认是8,假设新生代10M内存,8M划分给Eden区,剩下的二等分,一份from,一份to。
幸存区比例 -XX:SurvivorRatio=ratio 动态调整幸存区比例
晋升阈值 -XX:MaxTenuringThreshold=threshold 用于动态调整幸存区比例
晋升详情 -XX:+PrintTenuringDistribution 用于动态调整幸存区比例
GC详情 -XX:+PrintGCDetails -verbose:gc 打印详情信息
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC 默认在Full GC前进行一次Minor GC

2-5 垃圾回收案例分析

情况1:什么都不放的情况

new generation:新生代 tenured generation:老年代

package cn.itcast.jvm.t2;
import java.util.ArrayList;
/**
 *  演示内存的分配策略
 */
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 : 堆初始与最大大小都是20M,新生代的大小为10M.
    // -XX:+UseSerialGC : 为了学习方便,采用这个垃圾回收器,默认的垃圾回收器并不是这个。
    // -XX:+PrintGCDetails -verbose:gc :打印详细信息
    // -XX:-ScavengeBeforeFullGC :在Full GC 前进行 Minor GC.
    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);
    }
}

情况1执行结果

可以看到即使用户没有创建对象,系统对象也要占据一部分堆内存空间。

Heap
 def new generation   total 9216K, used 2341K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
// 新生代的空间总的大小为9216K,这里没有把To空间给计算进去,系统任务To的空间是分配是不可用的,所以不是10M,已经使用了2341K,[]内部则是内存地址范围。                              
  eden space 8192K,  28% used [0x00000000fec00000, 0x00000000fee49420, 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)
// 老年代大小为10M,可以看到没有任何空间使用                                             
                                             
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
                                
 Metaspace       used 3394K, capacity 4496K, committed 4864K, reserved 1056768K
                                
  class space    used 378K, capacity 388K, committed 512K, reserved 1048576K

Java的内存对象都是分配在堆上吗


情况2:新生代堆空间放满,触发GC
    public static void main(String[] args) throws InterruptedException {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);      // 系统类占用2341K,加上new的7MB触发垃圾回收
    }

情况2执行结果

[GC (Allocation Failure) [DefNew: 2342K->696K(9216K), 0.0029193 secs] 2342K->696K(19456K), 0.0029867 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
// GC:minor GC(新生代垃圾回收)    FUll GC  (老年代垃圾回收)
// [Times: user=0.00 sys=0.00, real=0.00 secs] 垃圾回收执行时间
Heap
 def new generation   total 9216K, used 8110K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  90% used [0x00000000fec00000, 0x00000000ff33d8c0, 0x00000000ff400000)
  from space 1024K,  67% used [0x00000000ff500000, 0x00000000ff5ae100, 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 3539K, capacity 4536K, committed 4864K, reserved 1056768K
  class space    used 395K, capacity 428K, committed 512K, reserved 1048576K
情况三: 新生代内存随着对象的增多放不下了
    public static void main(String[] args) throws InterruptedException {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_512KB]);
    }

执行结果

  • 新生代放不下,将新生代的对象放置到老年代
[GC (Allocation Failure) [DefNew: 2342K->670K(9216K), 0.0022591 secs] 2342K->670K(19456K), 0.0023131 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8678K->538K(9216K), 0.0061246 secs] 8678K->8354K(19456K), 0.0061637 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 1132K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   7% used [0x00000000fec00000, 0x00000000fec94930, 0x00000000ff400000)
  from space 1024K,  52% used [0x00000000ff400000, 0x00000000ff486a00, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 7815K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  76% used [0x00000000ff600000, 0x00000000ffda1f80, 0x00000000ffda2000, 0x0000000100000000)
 Metaspace       used 3539K, capacity 4536K, committed 4864K, reserved 1056768K
  class space    used 395K, capacity 428K, committed 512K, reserveed 1048576K
情况四:一开始直接分配大于新生代的内存,如果老年代放的下,则直接放到老年代
    public static void main(String[] args) throws InterruptedException {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
    }

执行结果

Heap
 def new generation   total 9216K, used 2507K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  30% used [0x00000000fec00000, 0x00000000fee72ca8, 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 3539K, capacity 4536K, committed 4864K, reserved 1056768K
  class space    used 395K, capacity 428K, committed 512K, reserved 1048576K

总结

当内存比较紧张的时候,即新生代内存放不下的时候,有时候会直接将对象分配到老年代,或者直接在回收次数较少(未达到15次)的情况下,直接将新生代对象弄到老年代。

2 垃圾回收器

2-1 垃圾回收器概述

名称 特点 适合场景 目标 新生代 老年代
串行垃圾回收器 单线程 堆内存小,适合个人电脑(cpu个数) 采用复制的垃圾回收算法 采用标记+整理的垃圾回收算法
吞吐量优先垃圾回收器 多线程 堆内存大,多核CPU 并行,让单位时间内STW的时间最短 复制算法 标记+拷贝
响应时间优先的垃圾回收器(简称CMS) 多线程 堆内存大,多核CPU 并发,尽可能让单次STW最短 复制算法 标记清除算法产生内存碎片需要退化成单线程的垃圾整理回收器

CMS垃圾回收器后来被G1垃圾回收器取代

2-2 串行垃圾回收器

开启串行垃圾回收器的JVM参数
-XX:+UseSerialGC    = Serial + SerialOld
// Serial:工作在新生代,采用复制的垃圾回收算法
// SerialOld:工作在老生代,采用标记+整理的垃圾回收算法

总结:触发垃圾回收时,让多个线程在一个安全点停下来,然后使用单线程的垃圾回收器去进行垃圾回收,垃圾回收完成后,再让其他线程运行。

2-3 吞吐量优先的垃圾回收器

开启吞吐量优先的垃圾回收器的JVM参数

开启/关闭的参数
  • 默认的多线程垃圾回收器,前者是开启新生代回收器,采用复制算法,后者是开启老年代回收器,采用标记+拷贝算法。下面选项只要开启一个,那么另外一个也会开启。
-XX:+UseParallelGC , -XX:+UseParallelOldGC 
  • 开启自适应动态调整新生代的大小,晋升阈值
-XX:+UseAdaptiveSizePolicy 
二个指标调整的参数(ParallelGC会根据设定的指标去调整堆的大小到达下面期望设定的目标)

指标1)1/(1+ratio) = 垃圾回收的时间/总的运行时间

  • ratio默认值时99,即垃圾回收的时间不超过总时间1%。但一般设为19。
  • 如果达不到目标,ParallelGC会调整堆内存大小来达到这个目标,通常是调大,这样垃圾回收的次数会减少,从而提高吞吐量
-XX:GCTimeRatio=ratio          

指标2)每次垃圾回收的时间限制( 最大暂停的毫秒数)

  • 默认值是200ms
  • 显然将堆内存空间变小有助于减少每次垃圾回收的时间
-XX:MaxGCPauseMillis=ms   

总结:显然指标1)与指标2)是有冲突的。


-XX:ParallelGCThreads=n        //垃圾回收并行的线程数目

总结:采用多线程方式进行垃圾回收,垃圾回收的线程数目通常根据CPU的核数进行设置。在垃圾回收阶段,并行的垃圾回收线程会充分占用CPU。在非垃圾回收阶段,用户线程会充分利用CPU资源。

2-4 响应时间优先的垃圾回收器(CMS垃圾回收器)

缺点:采用的标记清除算法产生内存碎片需要退化成单线程的垃圾整理回收器,造成响应时间变长。

开启的JVM参数

  • 注意这个是并发的采用标记清除算法的垃圾回收,这里区别于之前的垃圾回收器,该垃圾回收器能够在进行垃圾回收的同时运行其他非垃圾回收线程(也存在时间阶段需要停止,但不是所有阶段停止)。
  • 老年代并发的垃圾回收器会出现失败的情况,这时老年代垃圾回收器会退化成单线程的垃圾回收器(SerialOld)
-XX:+UseConcMarkSweepGC // use concurrent mark sweep(会产生垃圾碎片) 工作在老年代的垃圾回收器
-XX:+UseParNewGC        // 工作在新生代的垃圾回收器

重要的初始参数

-XX:ParallelGCThreads=n        // 并行的垃圾回收线程数,通常等于CPU的核心数(垃圾回收并行阶段)
-XX:ConcGCThreads=threads      // 并发的线程数目,通常设为并行垃圾回收线程数的1/4(垃圾回收并发阶段)
其他参数
-XX:CMSInitiatingOccupancyFraction=percent // 执行垃圾回收的内存占比,预留空间给浮动垃圾
-XX:+CMSScavengeBeforeRemark
// 在重新标记前,对新生代进行垃圾回收,减少并发清理的垃圾对象,+开启,-关闭

  • 浮动垃圾是指并发清理过程中用户线程新产生的垃圾,需要等待下次并发清理。

并发工作流程概述

step1:老年代发生内存不存的现象。
step2:ConcMarkSweepGC会进行一个初始标记动作(初始标记需要STW即阻塞非垃圾回收线程),初始标记只标记根对象,所以速度非常快,暂停时间也非常短。
step3:完成初始标记后,之前阻塞的线程又可以运行了,这个时候垃圾回收线程进行并发标记。
step4:并发标记结束后,需要再次阻塞非垃圾回收线程,进行一个所谓的重新标记,
step5:重新标记完成后,阻塞的线程又可以运行了。垃圾回收线程也并发的清理垃圾对象。

总结:初始标记与重新标记需要阻塞线程。 在并发阶段,由于垃圾回收线程占用资源,所以系统的吞吐量会受到一定的影响,但是系统的响应速度由于并发执行不会受到垃圾回收的明显影响(相比较其他垃圾回收器,STW时间只需要进行初始标记与重新标记,并且能够不阻塞其他线程进行垃圾的标记与清除)

参考资料

01 JVM基础课程

posted @ 2021-04-12 10:19  狗星  阅读(169)  评论(0编辑  收藏  举报
/* 返回顶部代码 */ TOP