🌴 对象存活判定
🪨 引用计数法
给每个对象添加一个引用计数器:
- 当有地方引用该对象,计数器
+1
- 当引用失效,计数器
-1
- 当计数器为
0
时,表示对象不可用
该方法简单高效,但主流的虚拟机都没有选择该方法管理内存,难以解决对象之间循环引用的问题。
🪨 可达性分析法
通过一系列称为 GC Root
的对象作为起点,从这些起点向下搜索,节点走过的路径称为引用链,当一个对象与 GC Root
之间没有任何引用链相连的话,说明对象不可用,需要被回收。
可以作为 GC Root
的对象:
- 虚拟机栈(栈帧中局部变量表)引用的对象
- 本地方法栈中引用的对象
- 类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁(synchronized)持有的对象
🌴 垃圾回收算法
🪵 标记-清除
- 标记出所有需要回收的对象
- 标记结束后,统一回收掉所有被标记的对象
- 执行效率不稳定,大部分对象需要回收时,需要大量标记和清除操作
- 内存空间碎片化问题
🪵 标记-复制
- 将内存分为两块,每次只使用其中的一块
- 当一块内存使用完后,将存活的对象复制到另外一块去
- 将已经使用的空间一次性清除掉
- 可用内存变少,有空间浪费问题
🪵 标记-整理
- 标记出所有需要回收的对象
- 让这些存活对象向内存的一端移动
- 直接清除掉边界以外的内存
🪵 分代收集
根据对象存活周期的不同,将 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
,直到收集结束。
- 新生代采用 标记-复制 算法,老年代采用 标记-整理 算法。
- 简单高效
- 依旧是现在 HotSpot 虚拟机运行在客户端模式下的默认新生代垃圾回收方式
🪵 ParNew
Serial 的多线程版本,可以使用多条线程进行垃圾回收。(新生代)
- 采用标记-复制算法
🪵 Parallel Scavenge / Parallel Old
- 基于 标记-复制 算法实现的新生代垃圾收集器,能够并行收集的多线程收集器,目标是达到一个可控制的“吞吐量”。
- 基于 标记-整理 算法实现的老年代多线程收集器。
Java 8 默认的收集器为 Parallel Sacvenge + Parallel Old
吞吐量 TPS :系统在单位时间内处理请求的数量
🪵 CMS
Concurrent Mark Swap
获取最短停顿时间的收集器。
是 HotSpot 虚拟机第一款真正意义上的并发垃圾收集器,实现了用户线程和垃圾回收线程同时工作。基于 标记-清除 算法的 老年代 收集器。
CMS 收集器整个过程分为四个步骤:
🐾 初始标记
标记与 GC Root 直接关联的对象。暂停用户线程(stop the world),但速度较快。
🐾 并发标记
用户线程和垃圾回收线程同时工作,从 GC Root 直接关联的对象开始遍历,根据可达性分析,找出存活对象。
🐾 重新标记
修正并发标记过程中,由于程序继续运行,导致的标记错误。暂停用户线程(stop the world)。
🐾 并发清除
并发地清理未被标记的区域。
缺点:
- 对 CPU 资源敏感
- 无法处理浮动垃圾:运行过程中,有一些垃圾在当次无法处理,需要等待下次垃圾回收
- 会产生内存碎片,导致连续空间减少,导致 Full GC
🪵 G1
G1 开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。
将 Java 堆分为 2048 个大小相同的独立 Region,每个 Region 的大小依据堆空间的大小而定,在 JVM 生命周期中不会改变。
每个 Region 都可以根据需要,作为新生代的 Eden 区,Survivor 区,或者是老年代;新生代和老年代在内存中不是连续的空间,收集器会根据区域的不同采取不同的策略。
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 中无法容纳,直接送到老年代,让老年代进行空间分配担保。