JVM2️⃣垃圾回收
1、四种引用
1.1、强引用
强引用(StrongReference),使用最普遍的引用。
-
具有强引用的对象不会被垃圾回收。
-
若内存不足,抛出 OutOfMemoryError 错误。
-
当一个对象没有被任何 GC Root 强引用时,该对象才能被垃圾回收(显式赋值 null 或超出生命周期)。
// 强引用 String str = "hello"; // 取消强引用 str = null;
1.2、软引用
软引用(SoftReference),描述有用但非必需的对象(如缓存)。
-
GC 后仍内存不足时,会触发 Full GC,回收软引用的对象。
-
可以与引用队列关联使用,释放软引用自身
-
软引用的对象被回收后,软应用自身会进入关联的引用队列;
-
判断软引用是否入队,可释放软引用自身。
// 软引用 SoftReference<String> softStr = new SoftReference<>("hello"); // 关联引用队列 ReferenceQueue<String> queue = new ReferenceQueue<>(); SoftReference<String> softStr = new SoftReference<>("hello", queue); if(queue.poll != null){ softStr = null; }
-
1.3、弱引用
弱引用(WeakReference),描述非必需的对象,且生命周期更短。
-
垃圾回收时,无论内存是否充足,弱引用的对象都会被 GC 回收。
-
垃圾回收的线程优先级较低,未必能迅速发现并回收。
-
GC 只回收新生代的弱引用,Full GC 回收所有弱引用。
-
可以与引用队列关联使用,释放弱引用自身。
-
弱引用的对象被回收后,弱应用自身会进入关联的引用队列;
-
判断弱引用是否入队,可释放弱引用自身。
// 弱引用 WeakReference<String> weakStr = new WeakReference<>("hello"); // 关联引用队列 WeakReference<String> queue = new WeakReference<>(); WeakReference<String> weakStr = new WeakReference<>("hello", queue); if(queue.poll != null){ weakStr = null; }
-
1.4、虚引用
虚引用(PhantomReference),“形同虚设”,相当于没有引用。
-
任何时候都可能被垃圾回收。
-
必须与引用队列关联使用,释放虚引用自身。
- 虚引用的对象被回收时,虚引用自身会进入关联的引用队列;
- 判断虚引用是否入队,可释放虚引用自身,或采取其它措施(如 ByteBuffer 释放直接内存)
ReferenceQueue<String> queue = new ReferenceQueue<String>(); PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
-
实例:ByteBuffer 被引用对象回收时,虚引用 Cleaner 入队,由 Reference Handler 线程调用虚引用相关方法(unsafe.freeMemory() 方法)释放直接内存。
* 终结器引用-FinalReference
- 无需手动编码,但其内部关联引用队列。
- 必须与引用队列关联使用
- 在垃圾回收时,会将终结器引用入队.
- Finalizer 线程(优先级低)通过终结器引用找到被引用对象,调用它的 finalize() 方法。
- 下次垃圾回收时,会回收被引用对象。
2、对象“已死”判断算法
2.1、引用计数器
在对象中添加一个引用计数器。
-
若该对象在某处被引用则计数加 1,引用失效时则将引用计数减 1。
-
当引用计数为零时判断可回收。
-
问题:对象循环引用。A 和 B 的引用计数器都不为零,但两个对象已经没有意义。导致内存泄漏。
2.2、可达性分析(❗)
JVM 采用可达性分析算法,判断对象是否可回收。
-
以树的形式呈现对象引用,根节点是 GC Roots 对象。
-
做法:从 GC Roots 开始向下搜索并作标记,遍历完树后,未被标记的对象则判断为可回收的对象。
-
作为GC Roots的对象
- System Class:系统核心类
- Native Stack:本地栈
- Thread:线程的活动栈帧中的对象
- Busy Monitor:被加锁的对象
-
实例:
-
状态1:list 是一个 GC Roots 对象。
-
状态2:list 不再是 GC Roots 对象。
public static void main(String[] args){ // 1 List<Object> list = new ArrayList<>(); // 2 list = null; }
-
3、GC 算法(❗)
3.1、标记清除
Mark Sweep
-
做法:通过可达性分析算法标记存活对象,回收未被标记的对象。
-
特点:速度快,但会造成内存碎片。
- 标记完成后直接清除,回收速度快。
- 若有一个较大的对象实例需要在堆中分配内存空间。可能无法找到足够的连续内存,不得不再次触发 GC。
3.2、复制
Copy,新生代使用
将内存分为两块,每次只使用其中一块。
-
做法
- 通过可达性分析算法,标记存活对象。
- 将存活对象复制到未使用的内存块中,清除旧内存区域并交换两个内存角色。
-
特点:没有内存碎片,但牺牲部分内存用于复制(双倍)。
3.3、标记整理(压缩)
Mark Compact,老年代使用
-
做法:
- 通过可达性分析算法,标记存活对象。
- 将存活对象的内存压缩到内存一端,清除边界的内存。
-
特点:没有内存碎片,避免在老年代使用复制算法的效率问题。
4、垃圾回收(❗)
- Partial GC
- Minor GC:新生代
- Major GC:(CMS)老年代
- Mixed GC:(G1)整个新生代,部分老年代
- Full GC:整堆(新生代、老年代)和方法区
4.1、分代垃圾回收
4.1.1、堆结构划分
思想:根据对象的生命周期,将内存划分为新生代和老年代。
- 新生代:大部分对象不会存活很久,适合复制算法。
- Eden:新创建的对象(1)
- Survivor:存活对象(2)
- 老年代:大部分对象会继续存活,适合标记整理算法。
- 方法区
- 永久代:1.6 属于堆
- 去永久代:1.7 部分移出
- 元空间:1.8 本地内存
4.1.2、过程描述(❗)
新创建的对象被分配在 Eden 区,初始时两块 survivor 区都为空。
- Eden 区满时,触发 Minor GC
- 将 Eden 区存活对象复制到 From 区,且存活对象年龄 +1。回收不存活的对象(清空 Eden 区)。
- 下次触发 Minor GC 时,Eden 和 From 区的情况同上。
- 当触发 Minor GC 时,Eden 和 From 区都满的情况下
- 将 Eden 和 From 区的存活对象复制到 To 区,且存活对象年龄 +1。回收不存活的对象(清空 Eden 和 From 区)。
- 此时 From 区为空,To 区存在年龄不同的对象(从 From 过来的,从 Eden 过来的)。
- 交换 From 和 To 区,让 To 区始终为空。
- 之后每次触发 Minor GC,重复步骤 2 和 3(交换 From 和 To)。
- 当存活对象的年龄达到一个阈值(
-XX: MaxTenuringThershold
,默认 15),就会从新生代晋升到老年代(Promotion)。 - 当触发 Minor GC 时,JVM 会检查晋升对象的大小
- 若大于老年代的剩余空间,则触发 Full GC。
- 否则,检查是否设置了允许担保失败
+HandlePromotionFailure
- 是则只进行 Minor GC,否则进行 Full GC。
- 意味着设置了该参数,触发 Minor GC 时可能触发 Full GC,哪怕老年代还有剩余一一部分内存。
4.1.3、Stop-The-World
STW(Stop the world):Java 全局暂停的现象。
- 多数是由 GC 触发,此时本地方法可以执行但不能与 JVM 交互。
- 少数是由 Dump 线程、堆 Dump、死锁检查触发。
STW 使用户线程阻塞,垃圾回收线程执行结束后,用户线程才恢复运行。
- 目的
- 避免无法清理干净
- GC 工作需要在一个能确保一致性的快照中进行。
- 危害:长时间服务停止,没有响应。
4.2、对象进入老年代(❗)
- 长期存活对象
- 存活对象的年龄达到一个阈值,就会从新生代晋升到老年代
- 参数:
-XX: MaxTenuringThershold
,默认 15。
- To 区内存不足
- Minor GC 时 From 区满
- 且 Eden、From 区对象大小,大于 To 区可用内存。
- 动态年龄判定
- 在 From 区中,所有相同年龄对象占用的总内存超过 From 区的一半。
- 则不小于该年龄的对象就会被移动到老年代,无需达到阈值。
- 巨型对象
- 新创建的对象大于某个阈值,即使 Eden 区有足够内存,也会直接存入老年代。
- 参数:
PretenureSizeThreshold
,默认 3M。
4.3、GC 触发条件
CMS 收集器 可以单独进行 Minor GC 和 Major GC。
其它垃圾收集器只会单独进行 Minor GC,在 Full GC 触发时进行 Major GC。
- Partial GC:部分
- Minor GC:新生代 Eden 满触发,采用复制算法。
- Major GC:CMS 老年代内存不足时触发,采用标记整理算法。
- Full GC:整堆(新生代、老年代)和方法区,触发时机如下
- 调用 System.gc() 时,但不一定会执行(线程优先级低)
- 老年代空间不足。
- 方法区空间不足。
- Minor GC 后,从新生代晋升的对象大小,大于老年代的可用内存。
- Minor GC 时 From 区满,且 To 区内存不足,且老年代的可用内存小于该对象大小。
5、垃圾回收器(❗)
- 垃圾回收器(连线表示可以搭配使用)
- 新生代:Serial、ParNew、Parallel Scavenge
- 老年代:Serial Old、Parallel Old、CMS
- 整堆:G1
- 相关概念
- 吞吐量:CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值
- 吞吐量 = 用户代码运行时间 / (用户代码运行时间 + 垃圾回收时间)
- 例:JVM 共运行 100min,GC 消耗 1min,则吞吐量为 99%
- 并行收集:多个垃圾回收线程同时工作,用户线程阻塞。
- 并发收集:垃圾回收线程与用户线程同时工作。
- 吞吐量:CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值
5.1、新生代 GC
Serial、ParNew、Parallel Scavenge
5.1.1、Serial(串行)
Serial 是最基本的,发展最悠久的 GC。
-
特点
- 单线程,复制算法
- 简单高效:由于没有线程交互的开销,专注于垃圾收集。
- STW:需要暂停所有工作线程,直到垃圾回收结束(Stop the world)
-
应用场景:堆内存较小,Client 模式下的 JVM。
5.1.2、ParNew(并行)
ParNew 可以看作 Serial 的多线程版本。
-
特点:除了多线程外,其它特点与 Serial 一致
-
应用场景:Server 模式下的 JVM 的首选新生代 GC。它是除了 Serial 外,唯一能与 CMS 搭配使用的。
-
-XX:ParallelGCThreads
:并行垃圾回收线程数,默认为 CPU 核数。
5.1.3、Parallel Scavenge(吞吐量优先)
Parallel Scavenge 与吞吐量相关,也称吞吐量优先收集器(JDK 8 默认)
-
特点:类似 ParNew,但增加 GC 自适应调节策略
-
目标:高吞吐量(单位时间内,GC 总时间最短)
-
应用场景:要求高吞吐量和 CPU 资源敏感。
-
-XX:ParallelGCThreads
-
-XX:+UseAdaptiveSizePolicy
:GC 自适应调节策略(JVM 动态设置新生代内存比例,晋升阈值等) -
吞吐量控制
-XX:GCTimeRatio
:GC 时间占比。默认99,对应 t = 1%【r = (1-t)/t】-XX:MaxGCPauseMillis
:最大 GC 暂停时间,默认 200ms
5.2、老年代 GC
Serial Old、Parallel Old、CMS
5.2.1、Serial Old
Serial 的老年代版本,标记整理算法。
应用场景:
- 堆内存较小,Client 模式下的 JVM
- 也可以在 Server 模式下使用
- 在 JDK 1.5 之前,与 Parallel Scavenge 搭配使用;
- CMS 的后备方案,并发收集失败时使用(Concurrent Mode Failure)
5.2.2、Parallel Old
Parallel Scavenge 的老年代版本,标记整理算法。
应用场景:要求高吞吐量和 CPU 资源敏感。
5.2.3、CMS(并发,最快响应时间)
CMS(Concurrent Mark Sweep),标记清除算法。
-
特点:多线程并发收集,与用户线程并发运行。
-
应用场景:要求响应速度快,停顿时间短,用户体验高。(web 应用,B/S 服务)
-
目标:最快响应时间(单次 GC 时间最短)
-
过程
-
初始标记:标记 GC Roots 对象(STW)
-
并发标记:可达性分析算法,找出存活对象(与用户线程并发运行)
-
重新标记:修正部分变动的标记(STW)
- 在并发标记阶段,垃圾回收线程与用户线程并发运行;
- 可能导致部分对象的标记发生改变。
-
并发清除:回收标记对象(与用户线程并发运行)
-
-
缺点
-
CPU 资源敏感。
-
内存碎片:采用标记清除算法。
-
浮动垃圾:并发清除阶段,垃圾回收时其它用户线程可能产生新的垃圾。可能出现 Concurrent Model Failure 而再次触发 Full GC。
-
5.3、整堆 GC
G1:Garbage First
JDK 7 可用,JDK 9 默认,取代 CMS
-
做法:将整个堆划分为相等大小的独立区域(Region)。
-
特殊区域:Humongous,巨型对象(一个对象占用的内存超过 Region 内存的一半)
- 在以前的垃圾回收器中,巨型对象会被分配在老年代。如果该对象只是短期存在,会降低 GC 的效率。
- Humongous 专门存放巨型对象,若一个 H 区装不下。会寻找连续的 H 区来存储。(甚至引发 Full GC 来产生连续的 H 区)
-
特点:
-
并行与并发:充分利用多核 CPU,缩短 STW 时间。
-
分代回收:独立管理整个堆,采用不同方式管理新生代和老年代。
-
空间整合:不会产生内存碎片。
-
5.3.1、Young GC
Eden 区满时触发。
- 将 Eden 区的数据移动到 Survivor 区。若 Survivor 区内存不足,部分数据会直接晋升到老年代。
- Survivor 区的数据移动到新的 Survivor 区,也有部分数据晋升到老年代。
- 最终 Eden 区空,GC 完成,用户线程恢复运行。
寻找 GC Roots
问题:如何找到所有的 GC Roots 对象?
- 在堆中,任何 Region 的任意对象之间都可以发生引用关系。
- 采用可达性分析算法时,需扫描整个堆,效率低。
解决:Remembered Set 和 CardTable。
- Remembered Set:HashTable,Key 是其它 Region 的其实地址,Value 是一个集合(元素是 CardTable 的 Index)
- G1 中每个 Region 都有一个 Remembered Set(point-in)。
- 当其它 Region 引用了当前 Region 的对象时,记录在 RSet 中。
- CardTable:通常为字节数组
- 逻辑上分为固定大小的连续区域(卡),默认都未被引用。
- 脏卡:当一个地址空间被引用时,对应数组下标的卡值标记为 0
- RSet 也记录该数组下标。
- 具体过程
- 程序在对 Reference 类型进行写操作时,产生一个
Write Barrier
暂时中断写操作。 - 检查 Reference 引用对象是否处于多个 Region 中(即跨代引用:老年代中引用了新生代中的对象)。
- 如果是,标记脏卡,被引用 Region 的 Remembered Set。
- 程序在对 Reference 类型进行写操作时,产生一个
Young GC 过程
- 根扫描:扫描静态和本地对象
- 更新 RSet:处理 Dirty card 队列,更新 RSet
- 处理 RSet:检测从新生代指向老年代的对象
- 对象拷贝:将存活对象拷贝到 survivor/old 区域
- 处理引用队列:软引用,弱引用,虚引用
5.3.2、Mix GC
回收所有新生代,部分老年代。
包括全局并发标记和拷贝存活对象两个阶段。
全局并发标记
- 初始标记:标记 GC Roots 对象(STW)
- 根区域扫描
- 并发标记:可达性分析算法,找出存活对象(与用户线程并发运行)
- 最终标记:修正部分变动的标记(STW)
- 并发清除:对各个 Region 的回收价值和成本及逆行排序,根据 MaxGCPauseMillis 来制定回收计划。(与用户线程并发运行)
并发标记
三色标记算法
从 GC Roots 开始遍历并标记
- 黑色:GC Roots 对象,或者已处理完的对象
- 灰色:正在处理的对象
- 白色:未处理的对象。扫描完所有对象后仍为白色说明是不可达对象。
步骤
-
初始:从 GC Roots 开始遍历,正在处理的对象为灰色,处理后为黑色。
-
扫描完所有对象后,可达对象都是黑色,不可达的对象为白色,需要被清理。
存在问题:对象处理遗漏问题。
并发清除阶段:垃圾回收线程与用户线程并发进行,可能引起对象的指针改变。
-
某一状态:A 已处理完,B 正在处理,C 是 B 的子对象且未处理。
-
在处理 C 前,用户线程修改了 C 对象的指针,将 C 与 已处理完的 A 引用
-
此时 GC 会认为 B 已处理结束,置为黑色。并且认为 A 也处理完成,结束对象扫描。
-
此时 C 是白色,被认为是垃圾而被清理。这显然是不合理的。
解决:SATB(snapshot-at-the-beginning)
- 初始标记阶段,生成一个快照标记存活对象。
- 并发标记阶段,将所有被修改的对象入队。
- 可能存在游离的垃圾,将在下次被回收。