Featured image of post JVM 垃圾回收

JVM 垃圾回收

🌴 对象存活判定

🪨 引用计数法

给每个对象添加一个引用计数器:

  • 当有地方引用该对象,计数器 +1
  • 当引用失效,计数器 -1
  • 当计数器为 0 时,表示对象不可用

该方法简单高效,但主流的虚拟机都没有选择该方法管理内存,难以解决对象之间循环引用的问题。

🪨 可达性分析法

通过一系列称为 GC Root 的对象作为起点,从这些起点向下搜索,节点走过的路径称为引用链,当一个对象与 GC Root 之间没有任何引用链相连的话,说明对象不可用,需要被回收。

gcroot

可以作为 GC Root 的对象:

  • 虚拟机栈(栈帧中局部变量表)引用的对象
  • 本地方法栈中引用的对象
  • 类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁(synchronized)持有的对象

🌴 垃圾回收算法

🪵 标记-清除

  • 标记出所有需要回收的对象
  • 标记结束后,统一回收掉所有被标记的对象

clear

  • 执行效率不稳定,大部分对象需要回收时,需要大量标记和清除操作
  • 内存空间碎片化问题

🪵 标记-复制

  • 将内存分为两块,每次只使用其中的一块
  • 当一块内存使用完后,将存活的对象复制到另外一块去
  • 将已经使用的空间一次性清除掉

copy

  • 可用内存变少,有空间浪费问题

🪵 标记-整理

  • 标记出所有需要回收的对象
  • 让这些存活对象向内存的一端移动
  • 直接清除掉边界以外的内存

move

🪵 分代收集

根据对象存活周期的不同,将 Java 堆分为新生代和老年代,依据各个区域的特点,选择回收算法。

  • 在新生代中,每次收集都会有大量的对象死去,选择 标记-复制 算法
  • 在老年代中,存活对象的几率较高,没有额外的空间进行分配担保,选择 标记-清除标记-整理 算法

🌴 垃圾收集方式

🪨 部分收集

收集堆部分区域。Partial GC

  • 新生代收集 Minor GC / Young GC

    目标只是新生代的垃圾收集。

  • 老年代收集 Major GC / Old GC

    目标只是老年代的垃圾收集,目前只有 CMS 收集器会有单独收集老年代的行为。

  • 混合收集 Mixed GC

    目标是整个新生代和部分老年代的垃圾收集,目前只有 G1 收集器会有这种行为。

🪨 整堆收集

收集整个堆和方法区。Full GC

  • 晋升到老年代的对象大于老年代的剩余空间
  • Minor GC 后,新生代存活对象超过了老年代的空间(分配担保机制)
  • 手动 System.gc()
  • (Java 8 之前)永久代空间不足

🌴 垃圾收集器

🪵 Serial / Serial Old

“单线程”的垃圾收集器,在进行垃圾收集时,必须暂停其他所有工作线程stop the world,直到收集结束。

  • 新生代采用 标记-复制 算法,老年代采用 标记-整理 算法。

serial

  • 简单高效
  • 依旧是现在 HotSpot 虚拟机运行在客户端模式下的默认新生代垃圾回收方式

🪵 ParNew

Serial 的多线程版本,可以使用多条线程进行垃圾回收。(新生代)

  • 采用标记-复制算法

parnew

🪵 Parallel Scavenge / Parallel Old

  • 基于 标记-复制 算法实现的新生代垃圾收集器,能够并行收集的多线程收集器,目标是达到一个可控制的“吞吐量”。
  • 基于 标记-整理 算法实现的老年代多线程收集器。

parallel

  • Java 8 默认的收集器为 Parallel Sacvenge + Parallel Old

  • 吞吐量 TPS :系统在单位时间内处理请求的数量

🪵 CMS

Concurrent Mark Swap 获取最短停顿时间的收集器。

是 HotSpot 虚拟机第一款真正意义上的并发垃圾收集器,实现了用户线程和垃圾回收线程同时工作。基于 标记-清除 算法的 老年代 收集器。

CMS

CMS 收集器整个过程分为四个步骤:

  • 🐾 初始标记

    标记与 GC Root 直接关联的对象。暂停用户线程(stop the world),但速度较快。

  • 🐾 并发标记

    用户线程和垃圾回收线程同时工作,从 GC Root 直接关联的对象开始遍历,根据可达性分析,找出存活对象。

  • 🐾 重新标记

    修正并发标记过程中,由于程序继续运行,导致的标记错误。暂停用户线程(stop the world)。

  • 🐾 并发清除

    并发地清理未被标记的区域。

缺点:

  • 对 CPU 资源敏感
  • 无法处理浮动垃圾:运行过程中,有一些垃圾在当次无法处理,需要等待下次垃圾回收
  • 会产生内存碎片,导致连续空间减少,导致 Full GC

🪵 G1

G1 开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。

g1heap

将 Java 堆分为 2048 个大小相同的独立 Region,每个 Region 的大小依据堆空间的大小而定,在 JVM 生命周期中不会改变。

每个 Region 都可以根据需要,作为新生代的 Eden 区,Survivor 区,或者是老年代;新生代和老年代在内存中不是连续的空间,收集器会根据区域的不同采取不同的策略。

g1

Region 中还会有一类特殊的巨大区域 Humongous,专门用来存储大对象。当对象的大小超过了一个 Region 的一半,就被判定为大对象。

G1 收集器回收的步骤:

  • 🐾 初始标记

    标记 GC Root 能够直接关联到的对象,需要停顿线程(stop the world),耗时较短。

  • 🐾 并发标记

    从 GC Root 开始对堆中的对象进行可达性分析,找出需要回收的对象,与用户线程同时执行。

  • 🐾 最终标记

    修正并发标记期间因用户程序继续运作而导致标记产生的变动。

  • 🐾 筛选回收

    对各个 Region 的回收价值和成本进行排序,会选择价值最大的区域进行回收。

    将回收的 Region 中的存活对象,复制到空的 Region 中,再清理掉整个旧的 Region 区域。

    涉及对象的移动,必须暂停用户线程(stop the world),有多条垃圾收集器并发执行。但是只回收一部分Region,时间是用户可控制的。

🌴 对象分配和回收原则

  • 🪨 对象优先在 Eden 区分配

大多数情况下,对象在新生代 Eden 区分配,当 Eden 区没有足够空间时,虚拟机会发起一次 Minor GC。

  • 🪨 大对象直接进入老年代

需要大量连续内存空间的 Java 对象(数组,很长的字符串),直接在老年代分配,避免在 Eden 区和两个 Survivor 区之间来回复制,产生大量的内存复制操作。

  • 🪨 长期存活的对象进入老年代

虚拟机给每个对象一个对象年龄计数器,每经过一次 MinorGC,年龄就会 +1,

当年龄增加到一定程度(默认 15),或某个年龄超过了 Survivor 区的一半时,会晋升到老年代。

  • 🪨 空间分配担保

确保在 Minor GC 之前,老年代还有容纳新生代所有对象的剩余空间。

进行一次 Minor GC 之后,Eden 区中任然存在大量对象,Survivor 中无法容纳,直接送到老年代,让老年代进行空间分配担保。