Skip to content

一、垃圾收集器

JVM 在进行 GC 垃圾回收时,并非每次都对上面三个内存(新生代、老年代、元空间)区域一起回收的,大部分时候 回收的都是指新生代。 针对 Hotspot VM 的实现,它里面的 垃圾收集器 按照回收区域又分为两大种类型:

Partial GC 部分收集

新生代收集 (Minor GC / Young GC)

  • 只针对新生代(Eden,s0,s1)区堆内存进行垃圾回收。

老年代收集 (Major GC / Old GC)

  • 只是老年代的堆内存进行垃圾回收。
  • 目前,只有 CMS 收集器,会有单独收集老年代的垃圾回收。

混合收集(Mixed GC)

  • Mixed GC 收集器
  • 收集整个新生代以及部分老年代的堆内存垃圾收集器。
  • 目前,只有 G1 会有这种行为。

触发条件:

  • 新生代空间不足时,就会触发 Minor GC (这里的年轻代满指的是 Eden 区满), 会顺带触发S0 区的 GC,也就是 被动触发 GC(每次 Minor GC 会清理年轻代的内存)
  • 因为 Java 对象大多都具备 朝生夕灭 的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • Minor GC 会引发 STW(Stop The World),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
java
package com.calvin.jvm.heap.gc.example;

/**
 * 新生代空间不足(Eden 区)触发 MinorGC 
 *
 * @author Calvin
 * @date 2023/8/24
 * @since v1.0.0
 */
public class MinorGcTrigger {


    /**
     * 触发条件: Eden区(75MB), 程序启动和对象占用超出了75MB, 触发 Minor GC
     */
    public static void eDenSpaceFull() throws InterruptedException {
        // A对象 => 占用25MB
        byte[] a = new byte[1024 * 1024 * 25];
        System.out.println("A对象: " + a.length / 1024 /1024 + "MB") ;

        // 停顿 5 秒
        Thread.sleep(5000);

        // B对象 => 占用25MB
        byte[] b = new byte[1024 * 1024 * 25];
        System.out.println("B对象: " + b.length / 1024 /1024 + "MB");

        // C对象 => 占用25MB
        byte[] c = new byte[1024 * 1024 * 25];
        System.out.println("C对象: " + c.length / 1024 /1024 + "MB");

        // 停顿 500 秒
        Thread.sleep(5000 * 100);
    }

    /**
     * 主方法: -Xms300m -Xmx300m -XX:+PrintGCDetails
     *
     * @param args 参数
     * @throws InterruptedException 中断异常
     */
    public static void main(String[] args) throws InterruptedException {
        eDenSpaceFull();
    }


}
  • 当前执行程序: -Xmx300m 最大内存为 300MB, (默认 NewRatio=2 => 新生代 1/3 老年代 2/3)

  • 新生代占比 300MB _ 1/3 = 100MB, 老年代占比: 300MB _ 2/3 = 200MB

  • 新生代: (默认 SurvivorRatio=8 => 8:1:1)

    • eden 区占比: 100MB * 6/8 = 75MB
    • SO 区占比: 100MB * 1/8 = 12.5MB
    • S1 区占比: 100MB * 1/8 = 12.5MB
  • 执行结果

log
A对象: 25MB
B对象: 25MB

## 当前执行了一次 Moinr GC
[GC (Allocation Failure) [PSYoungGen: 58982K->880K(90624K)] 58982K->52088K(298496K), 0.0222460 secs] [Times: user=0.08 sys=0.01, real=0.02 secs]
C对象: 25MB


Heap
 PSYoungGen      total 90624K, used 28453K [0x00000007b9b00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 77824K, 35% used [0x00000007b9b00000,0x00000007bb5ed408,0x00000007be700000)
  from space 12800K, 6% used [0x00000007be700000,0x00000007be7dc010,0x00000007bf380000)
  to   space 12800K, 0% used [0x00000007bf380000,0x00000007bf380000,0x00000007c0000000)
 ParOldGen       total 207872K, used 51208K [0x00000007ad000000, 0x00000007b9b00000, 0x00000007b9b00000)
  object space 207872K, 24% used [0x00000007ad000000,0x00000007b0202020,0x00000007b9b00000)
 Metaspace       used 3710K, capacity 4536K, committed 4864K, reserved 1056768K
  class space    used 410K, capacity 428K, committed 512K, reserved 1048576K

Full GC 全部收集

  • 收集整个新生代(Young Gen)、老年代(Old Gen)、元空间(Metaspace) 的垃圾收集器。

  • 触发条件:

    • 老年代、方法区空间不足时,通过 Minor GC后进入 老年代的平均大小 > 老年代的可用内存
    • Eden区S0 => S1 复制时,对象大小 > S1可用内存,则把该对象转存到老年代,且老年代的可用内存 < 对象大小
    • Full GC 是开发或调优中尽量要避免的, 这样STW时间会短一些。
  • 案例: 大对象直接晋升老年代

java
package com.calvin.jvm.heap.gc.example;

/**
 * 老年代 Major GC / Full GC 触发
 *
 * @author Calvin
 * @date 2023/9/5
 * @since v1.0.0
 */
public class MajorGcOrFullGcTrigger {


    /**
     * Full GC
     * <p>
     * 当前执行程序:  -Xmx300m  => 最大内存为300MB
     * 触发机制:
     * - 调用System.gc()时,系统建议执行Full GC,但是不必然执行.
     * - 老年代空间不足.
     * - 方法区空间不足.
     * - 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
     * - 由Eden区、survivor space0(From Space)区向 survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小.
     * - full gc 是开发或调优中尽量要避免的。这样暂停时间会短一些。
     *
     * @throws InterruptedException
     */
    public static void fullGc() throws InterruptedException {
        // A对象 => 占用80MB
        byte[] a = new byte[1024 * 1024 * 80];
        System.out.println("A对象: " + a.length / 1024 / 1024 + "MB");

        // B对象 => 占用220MB
        byte[] b = new byte[1024 * 1024 * 220];
        System.out.println("B对象: " + b.length / 1024 / 1024 + "MB");

        Thread.sleep(1000 * 10);
    }


    /**
     * 主方法
     *
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        fullGc();
    }
}
  • 当前执行程序: -Xmx300m 最大内存为 300MB, (默认 NewRatio=2 => 新生代 1/3 老年代 2/3)
  • 新生代占比 300MB _ 1/3 = 100MB, 老年代占比: 300MB _ 2/3 = 200MB
  • 新生代: (默认 SurvivorRatio=8 => 8:1:1)
    • eden 区占比: 100MB * 6/8 = 75MB
    • SO 区占比: 100MB * 1/8 = 12.5MB
    • S1 区占比: 100MB * 1/8 = 12.5MB
  • 老年代: 200MB
  • 当前对象内存使用已使用 300MB + 启动程序加载包对象内存,已超出了 300MB, 所以对象内存溢出 OOM.

执行结果

log
************A对象: 80MB
####### 执行2次 MinorGC #######
[GC (Allocation Failure) [PSYoungGen: 4669K->512K(90624K)] 86589K->82440K(298496K), 0.0007327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 512K->480K(90624K)] 82440K->82408K(298496K), 0.0023379 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

####### 执行一次 FullGC 回收失败 #######
[Full GC (Allocation Failure) [PSYoungGen: 480K->0K(90624K)] [ParOldGen: 81928K->82285K(207872K)] 82408K->82285K(298496K), [Metaspace: 3168K->3168K(1056768K)], 0.0027795 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]

####### 再一次执行 MinorGC 回收失败 #######
[GC (Allocation Failure) [PSYoungGen: 0K->0K(90624K)] 82285K->82285K(298496K), 0.0009091 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

####### 再一次执行 FullGC 回收失败 #######
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(90624K)] [ParOldGen: 82285K->82268K(207872K)] 82285K->82268K(298496K), [Metaspace: 3168K->3168K(1056768K)], 0.0021149 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

####### OOM 内存溢出 #######
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.calvin.jvm.heap.gc.example.MajorGcOrFullGcTrigger.fullGc(MajorGcOrFullGcTrigger.java:51)
	at com.calvin.jvm.heap.gc.example.MajorGcOrFullGcTrigger.main(MajorGcOrFullGcTrigger.java:66)

####### 打印GC使用详情 #######
Heap
 PSYoungGen      total 90624K, used 2335K [0x00000007b9b00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 77824K, 3% used [0x00000007b9b00000,0x00000007b9d47c68,0x00000007be700000)
  from space 12800K, 0% used [0x00000007be700000,0x00000007be700000,0x00000007bf380000)
  to   space 12800K, 0% used [0x00000007bf380000,0x00000007bf380000,0x00000007c0000000)
 ParOldGen       total 207872K, used 82268K [0x00000007ad000000, 0x00000007b9b00000, 0x00000007b9b00000)
  object space 207872K, 39% used [0x00000007ad000000,0x00000007b2057198,0x00000007b9b00000)
 Metaspace       used 3200K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 349K, capacity 388K, committed 512K, reserved 1048576K

  • 简称 STW
  • 即在执行垃圾收集算法时, Java 应用程序的所有的线程(除了垃圾收集收集器线程之外)都被挂起。
  • 此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行。

二、【新生代】收集器

Serial 串行-收集器

  • 是一个单线程垃圾收集器
  • 使用 copy 复制算法
  • 使用场景: 内存少(5MB ~ 20MB), 主要应用于客户端
  • 会产生 STW

shell
  # 选择Serial收集器参数:
  java -XX: +UseSerialGC xxx.jar

ParNew 多线程-收集器

  • 是一个多线程垃圾收集器
  • 使用 copy 复制算法
  • 使用场景: 内存上升(20MB ~ 1G), 主要应用于服务端
  • 协助收集器: 可以和 CMS (收集老年代) 更好使用
  • 会产生 STW

shell
# 选择ParNew收集器参数:
java -XX: +UseParNewGC xxx.jar

Parallel Scavenge 并行清理-收集器

吞吐量: 就是处理器用于运行用户代码的时间与处理器总消耗时间的比值 (吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)) 高吞吐量: 可以最高效率的利用处理器资源,吞吐量高,就意味着垃圾收集时间短,而更多的精力投入到程序的主要运算任务上。

  • 是一个并行多线程垃圾收集器
  • 使用 copy 复制算法
  • 使用场景: 内存上升(20MB ~ 1G) 主要应用于服务端
  • CMS 和 Parallel Scavenge 区别:
    • CMS: 关注点是尽可能地缩短垃圾收集时用户线程的停顿时间
    • Parallel Scavenge: 目标则是达到一个可控制的吞吐量(Throughput)
  • 优点: 还具有自适应的调节策略(通过 -XX: +UseAdaptiveSizePolicy 参数控制)
    • 当该参数激活后,将不需要人工指定新生代(-Xmn)、Eden 与 Survivor 的比例(-XX: SurvivorRatio)
    • 虚拟机会根据当前系统的运行情况收集性能监控信息,动态的调整这些参数以提供最合适的停顿时间或者最大吞吐量
  • JDK 1.8 默认垃圾收集器:
    • Parallel Scavenge + Parallel Old 组合, 使用命令 -XX: +PrintCommandLineFlags -version

shell
# Paraller Scavenge 控制参数:
-XX: +UseParallelGC
# 吞吐量大小,是垃圾收集时间占总时间的百分比
-XX: GCTimeRatio
# 最大垃圾收集停顿时间,收集器尽力保证内存回收花费的时间不超过设定的值
-XX: MaxGCPauseMillis

三、【老年代】收集器

Serial Old 串行-收集器

  • 是一个单线程垃圾收集器
  • 使用 Mark-sweep-compact 标记整理算法
  • 会产生 STW

shell
# 选择Serial Old收集器参数:
java -XX: +UseSerialOldGC xxx.jar

Parallel Old 并行-收集器

  • 是一个并行多线程垃圾收集器
  • 使用 Mark-sweep-compact 标记整理算法
  • 产生问题: 单线程,导致用户线程停顿过久,所以为了解决单线程问题,使用多线程,而产生垃圾收集器。

shell
# 选择Parallel Old收集器参数:
java -XX: +UseParallelOldGC xxx.jar

CMS (concurrent mark sweep)并发标记-收集器

  • 是一个并发标记垃圾收集器

  • 主要工作在老年代,为了解决STW

  • CMS 新特性: 垃圾线程和用户线程同时进行, 在最耗时的并发标记并发清除阶段,CMS 无需暂停用户线程,CMS 收集器的内存回收过程是可以与用户线程一起并发执行的。

  • 场景: 应用于 B/S 系统的服务端上。

  • 是基于标记-清除算法实现的,运作过程分为四步:其中,初始标记重新标记这两个步骤仍然需要暂停用户线程

    • 1.初始标记:仅仅只标记一下 GC ROOTS 直接关联的对象,速度很快。
    • 2.并发标记:从 GC ROOTS 的直接关联对象开始遍历整个对象图耗时虽然长,但是不需要停顿用户线程
    • 3.重新标记:其目的是为了修正并发标记期间,因用户程序继续运行而导致标记变动的那一部分对象
    • 4.并发清除:清理标记阶段判定为死亡的对象
  • CMS 的优点也为 CMS 带来了部分负面的影响: 【浮动垃圾】

    • 由于 CMS 并发标记并发清理阶段是和用户线程同时进行的,程序难免会在运行中产生新的垃圾对象,如果这部分的浮动垃圾产生在并发标记结束后,那么 CMS 无法在当次收集中处理掉。
    • CMS 面临的另一问题是,并发标记虽然不会停止用户线程,但是会占用一部分CPU资源从而导致应用程序变慢。
    • CMS 默认启动的回收线程数是(处理器核心数量+3)/ 4当处理器核心数量不足4个时,CMS对程序的影响会变大
    • CMS 收集结束后会产生打量的内存空间碎片可能会导致大对象因无法找到连续的内存空间而引发一次Full GC
  • 由于 CMS 与用户线程同时运行的情况,CMS 必须预留足够的内存空间用户线程使用,因此 CMS 无法像其他垃圾回收器一样等着老年代几乎被填满时再收集。

    • JDK5 默认 CMS 收集器当老年代使用了 68%的空间后被激活,而这一阈值在 JDK6 时提升到了 92%,无论何种阈值,CMS 都面临着“并发失败”的情况
    • 即 CMS 预留空间不足以让程序运行产生的新对象的需要。当出现并发失败的情况时,虚拟机将采用预案:冻结用户线程,启用 Serial Old 收集器来重新进入老年代回收垃圾,Serial Old 的特性造就了停顿时间更长的情况.

shell
# 调整CMS触发的百分比, 使用 CMS
java -XX: CMSInitiatingOccupancyFraction -XX:+UseConcMarkSweepGC xxx.jar

参考: 三色标记

  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过,它是安全存活的
    • 如果有其他对象引用指向了黑色对象,无须重新扫描一遍
    • 黑色对象不可能直接(不经过灰色对象)指向某个白色对象
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过
  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。

四、【不分代】收集器

G1 (Garbage First) 收集器

  • G1收集器专门针对以下应用场景设计:

    • 可以像CMS收集器一样可以和应用并发运行
    • 压缩空闲的内存碎片,却不需要冗长的GC停顿
    • 对GC停顿可以做更好的预测
    • 不想牺牲大量的吞吐量性能
    • 不需要更大的Java 堆空间
  • G1 与 CMS 区别:

    • G1 会压缩空闲内存使之足够紧凑,做法是用regions代替细粒度的空闲列表进行分配,减少内存碎片的产生。
    • G1 的 STW更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间
  • 在G1中,被分成一块块大小相等的heap region,一般有默认2048个,默认一个为 1M,这些region在逻辑上是连续的。

    • 每块region都可以作为独立的新生代,幸存区和老年代
    • Region来表示Eden 区、Survivor(S0/S1)区、Old 区、Humongous (H)区 等。
      • Eden 区 : 新生代
      • Survivor 区: 新生代
      • Old 区: 老年代
      • Humongous 区: 主要存放一些比较大的对象连续的,一个对象大于region的一半时,称之为巨型对象,G1不会对巨型对象进行拷贝,回收时会考虑优先回收。
  • G1中的GC收集:

    • G1保留了YGC并加上了一种全新的MIXGC用于收集老年代
    • G1中没有Full GC,G1中的Full GC是采用serial old Full GC
  • 运作过程大致可划分为以下四个步骤:
    • 初始标记(Initial Marking)

      • 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象
      • 这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
    • 并发标记(Concurrent Marking)

      • 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
      • 当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象
    • 最终标记(Final Marking)

      • 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
    • 筛选回收(Live Data Counting and Evacuation)

      • 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。
      • 这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

当发生 YGC 时:

  • 会对 Eden存活对象 迁移 Survivor区,然后进行回收Eden 区。
  • 会对满的Survivor存活对象达到一定阈值新生代对象,迁移到 Old区, 然后进行回收Survivor区。

shell
# 使用 G1 收集器, 打印时间戳、打印GC日志详情
java -XX:+UseG1GC \
-XX:+PrintGCTimeStamps \
-XX:+PrintGCDetails \
xxx.jar

ZGC 收集器

暂未编写,敬请期待 ......

Shenandoah 收集器

暂未编写,敬请期待 ......