垃圾回收

JVM 垃圾回收 (Garbage Collection, GC) 是 Java 虚拟机自动管理堆内存的核心机制。它负责识别并回收程序中不再使用的对象所占用的内存,防止内存泄漏,极大地简化了开发人员的内存管理工作。

垃圾回收器如果发现某个对象不再使用,就可以回收该对象。

b2af5602-f0d0-408c-a09a-7714c1b6f84a e5157ef9-aa76-4ba7-9185-bc32389e5d5f
  • 自动垃圾回收,自动根据对象是否使用由虚拟机来回收对象

    • 优点:降低程序员实现难度、降低对象回收bug的可能性
    • 缺点:程序员无法控制内存回收的及时性
  • 手动垃圾回收,由程序员编程实现对象的删除

    • 优点:回收及时性高,由程序员把控回收的时机
    • 缺点:编写不当容易出现悬空指针、重复释放、内存泄漏等问题

如果需要手动触发垃圾回收,可以调用System.gc()方法。
语法: System.gc()
注意事项:
调用System.gc()方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收Java虚拟机会自行判断。

如何判断对象可以回收

引用计数法

引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。

缺点:

  1. 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
  2. 存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题。如下图:
9df039034d2d5bb0a25498826e744cd9

可达性分析法

通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

0635cbe8

  • JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收
  • Java 中可以作为 GC Root 的对象:
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的Native方法)引用的对象

引用对象

39e05348c91a7142b1977434ff27c2a3

1. 强引用(StrongReference)

被强引用关联的对象不会被回收。只有GC Root都不引用该对象时,才会回收强引用对象

2. 软引用(SoftReference)

如果一个对象只有软引用引对象时,当程序内存不足时,就会将软引用中的数据进行回收。在JDK 1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中。

3. 弱引用(WeakReference)

如果一个对象只有弱引用该对象时,在垃圾回收时,无论内存是否充足,就会将弱引用中的数据进行回收。在JDK 1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。

4. 虚引用(PhantomReference)(不常见)

虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。

5. 终结器引用(FinalReference)(不常见)

终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做。

垃圾回收算法

1. 标记-清除算法

  1. 虚拟机执行垃圾回收的过程中,使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。将所有存活的对象进行标记。
  2. 然后垃圾收集器根据标识清除没有被标记也就是非存活对象,给堆内存腾出相应的空间

a4248c4b-6c1d-4fb8-a557-86da92d3a294

优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。

缺点:

  • 会产生大量不连续的内存碎片,导致无法给大对象分配内存。由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。
  • 分配速度慢。由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。
  • 标记和清除过程效率都不高。

2. 标记-整理算法

标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。

  1. 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
  2. 整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。

902b83ab-8054-4bd2-898f-9a4a0fe52830

优点:不会产生内存碎片。

缺点:内存变动更频繁,需要整理所有存活对象的引用地址,效率不高。

3. 复制算法

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理,最后会把位置互换。

e6b733ad-606d-4028-b3e8-83c3a73a3797

优点:不会产生内存碎片;吞吐量高,复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理算法少了一次遍历的过程,因而性能较好,但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动

缺点:内存使用效率低,每次只能让一半的内存空间来为创建对象使用。

4. 分代垃圾回收算法

现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)。

分代垃圾回收将整个内存区域划分为年轻代(复制算法)和老年代(标记 - 清除 或者 标记 - 整理 算法):

6ef8cd7b-e23a-4fab-b03c-05f350d71d82
  1. 分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。

  2. 随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC。

    Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区。

  3. 接下来,S0会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC。

    此时会回收eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0。注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。

  4. 如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。

  5. 当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。

    如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。

  6. 特殊情况:当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代

相关 JVM 参数

含义 参数
堆初始大小
必须是1024倍数且大于1MB
-Xms
堆最大大小
必须是1024倍数且大于1MB
-Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
伊甸园区和幸存区的比例,默认8,新生代1G,
伊甸园区800MB,S0和S1各100MB
-XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
打印GC日志 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

垃圾回收器

为什么分代GC算法要把堆分成年轻代和老年代?首先我们要知道堆内存中对象的特性:

  • 系统中的大部分对象,都是创建出来之后很快就不再使用可以被回收,比如用户获取订单数据,订单数据返回给用户之后就可以释放了。
  • 老年代中会存放长期存活的对象,比如Spring的大部分bean对象,在程序启动之后就不会被回收了。
  • 在虚拟机的默认设置中,新生代大小要远小于老年代的大小。

分代GC算法将堆分成年轻代和老年代主要原因有:

  1. 可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。

  2. 新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高。

  3. 分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收(full gc),STW时间就会减少。

垃圾回收器是垃圾回收算法的具体实现。

由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用。

具体的关系图如下:

5aa9617f-42a0-4ae5-866c-988448fca737

image

Serial 收集器

Serial 收集器是最基本的、发展历史最悠久的收集器。是一种单线程串行回收年轻代的垃圾回收器,只会使用一个线程进行垃圾收集工作,使用标记-复制算法

b0a87eb9-406f-4552-bf2b-f88b752f164a

优点

  • 单线程、简单高效(与其他收集器的单线程相比)。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程( “Stop The World” ),直到它结束。

缺点

  • 多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待。

适用场景

  • Java编写的客户端程序或者硬件配置有限的场景。

-XX:+UseSerialGC=serial + serialOld

SerialOld垃圾回收器

SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收。使用标记-整理算法

b0a87eb9-406f-4552-bf2b-f88b752f164a

ParNew 收集器

ParNew 收集器其实就是 Serial 收集器在多CPU下的优化,使用多线程进行垃圾回收。新生代采用标记-复制算法,老年代采用标记-整理算法。

-XX:+UseParNewGC 新生代使用ParNew回收器, 老年代使用串行回收器

parnew-garbage-collector

优点

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

缺点

  • 吞吐量和停顿时间不如G1,所以在JDK9之后不建议使用。

适用场景

  • JDK8及之前的版本中,与CMS老年代垃圾回收器搭配使用

CMS 收集器

CMS(Concurrent Mark Sweep),从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 标记-清除算法实现的。 老年代收集器。参数:XX:+UseConcMarkSweepGC。

CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。

cms-garbage-collector

CMS执行步骤:

1.初始标记,用极短的时间标记出GC Roots能直接关联到的对象。速度很快但是仍存在Stop The World问题

2.并发标记, 标记所有的对象,用户线程不需要暂停

3.重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。存在Stop The World问题

4.并发清理,清理死亡的对象,用户线程不需要暂停。但是清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。

CMS 垃圾回收器在 Java 9 中已经被标记为过时(deprecated),并在 Java 14 中被移除。

优点

  • 系统由于垃圾回收出现的停顿时间较短,用户体验好。

缺点

  • 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。

  • 无法处理在并发清理过程中产生的“浮动垃圾”,不能做到完全的垃圾回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。

  • 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。

适用场景:

  • 大型的互联网系统中用户请求数据量大、频率高的场景,比如订单接口、商品接口等

Parallel Scavenge垃圾回收器

Parallel Scavenge是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量(指 CPU 用于运行用户代码的时间占总时间的比值)。具备自动调整堆内存大小的特点。使用 标记-复制算法

优点

  • 吞吐量高,可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics)。为了提高吞吐量,虚拟机会动态调整堆的参数。

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

缺点

  • 不能保证单次的停顿时间。

适用场景

  • 后台任务,不需要与用户交互,并且容易产生大量的对象。比如:大数据的处理,大文件导出。

Parallel Old垃圾回收器

Parallel Scavenge 收集器的老年代版本。使用多线程和标记-整理算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

d6ecf303-9a1b-4df8-8fce-24f07e471ead

优点

  • 并发收集,在多核CPU下效率较高

缺点

  • 暂停时间会比较长

适用场景

  • 与Parallel Scavenge配套使用

G1垃圾回收器

JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间 ,但是会减少年轻代可用空间的大小。CMS关注暂停时间,但是吞吐量方面会下降。

而G1设计目标就是将上述两种垃圾回收器的优点融合:

1.支持巨大的堆空间回收,并有较高的吞吐量。

2.支持多CPU并行垃圾回收。

3.允许用户设置最大暂停时间。

G1出现之前的垃圾回收器,年轻代和老年代一般是连续的,如下图:

1dee5f2a-4608-4a2c-9df4-42697b1b8c50

G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。每个 Region 逻辑上可属于 Eden、Survivor、Old或 Humongous(存储大于 Region 一半大小的对象)。Region的大小通过堆空间大小/2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32m指定(其中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。

69b18970-293e-462d-91d8-fe8100105b44

G1 执行流程

1. Young GC(新生代回收)

年轻代回收(Young GC),回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数
-XX:MaxGCPauseMillis=n(默认200)设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间。

  • 触发条件

    Eden 区占满,或 G1 预测回收时间接近目标停顿时间。

  • 步骤

    1. 新创建的对象会存放在Eden区。当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC。

    2. 标记出Eden和Survivor区域中的存活对象。

    3. 根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域。

      G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。
      比如 -XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region。

    4. 后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。

    5. 当某个在Survivor区存活对象的年龄到达阈值(默认15),将被放入老年代。

    6. 部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region。

    7. 多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时
      (-XX:InitiatingHeapOccupancyPercent默认45%)会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。

2. Mixed GC(混合回收,核心流程)
  • 触发条件

    老年代占用达 45%(默认)或手动触发。G1对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1(Garbage first)名称的由来。最后清理阶段使用复制算法,不会产生内存碎片。

  • 步骤

    • 初始标记(STW):采用三色标记法标记从GC Root可直达的对象。 STW时间极短。
    • 并发标记(并行):递归标记所有存活对象,使用 SATB(快照)记录引用变化,避免漏标。
    • 最终标记(STW):处理并发标记期间的引用变更,修复漏标。
    • 筛选回收(STW):
      • 按 “回收收益” 排序 Region,选择回收集合(CSet)。
      • 复制存活对象到新 Region,清空原 Region。

    output

注意:如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法,此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。

3. Full GC
  • 触发条件

    • G1 在老年代内存不足时(老年代所占内存超过阈值)。
    • 如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理
    • 如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。
  • 特点

单线程全堆扫描,停顿时间极长,需通过调优避免

学习文献