JVM 垃圾回收

目录

[JVM 垃圾回收](#JVM 垃圾回收)

[一. 如何判断对象可以回收](#一. 如何判断对象可以回收)

[1. 引用计数法](#1. 引用计数法)

[2. 可达性分析算法](#2. 可达性分析算法)

[3. 四种引用](#3. 四种引用)

[二. 垃圾回收算法](#二. 垃圾回收算法)

[1. 标记清除算法](#1. 标记清除算法)

[2. 标记整理算法](#2. 标记整理算法)

[3. 复制算法](#3. 复制算法)

[三. 分代回收](#三. 分代回收)

[VM 相关参数](#VM 相关参数)

[四. 垃圾回收器](#四. 垃圾回收器)

[1. 串行垃圾回收器 (Serial GC)](#1. 串行垃圾回收器 (Serial GC))

[(1) 工作原理](#(1) 工作原理)

[(2) 优缺点](#(2) 优缺点)

[(3) 适用场景](#(3) 适用场景)

[2. 并行垃圾回收器 (Parallel GC)](#2. 并行垃圾回收器 (Parallel GC))

[(1) 工作原理](#(1) 工作原理)

[(2) 优缺点](#(2) 优缺点)

[(3) 适用场景](#(3) 适用场景)

[3. 并发垃圾回收器 (Concurrent GC)](#3. 并发垃圾回收器 (Concurrent GC))

[① CMS 垃圾收集器](#① CMS 垃圾收集器)

[(1) 工作原理](#(1) 工作原理)

[(2) 优缺点](#(2) 优缺点)

[(3) 适用场景](#(3) 适用场景)

[② G1 垃圾回收器](#② G1 垃圾回收器)

[(1) Region 分区](#(1) Region 分区)

[(2) 回收过程](#(2) 回收过程)

[4. 小结](#4. 小结)

[五. GC 调优](#五. GC 调优)


JVM 垃圾回收

一. 如何判断对象可以回收

1. 引用计数法

只要一个对象被引用一次, 则它的计数+1 (一个对象被引用多少次, 计数就是多少).

当一个对象的引用计数 == 0 时, 该对象就会被回收.

  • 问题: 会出现循环引用.

如下图: A对象引用B对象, 同时B对象也引用A对象. 那么: A对象的引用计数 == 1 同时 B对象的引用计数也 == 1. 那么这两个对象永远不会被回收, 从而导致内存溢出问题.

2. 可达性分析算法

根对象 (GC Root): 肯定不能被当成垃圾 被回收 的对象, 就是根对象.

在垃圾回收之前, 会先对堆内存中所有对象进行一次扫描, 看每个对象是否被 GC Root 直接或间接引用 (能够沿着 GC Root 为起点的引用链找到该对象). 如果是, 这个对象就不能被回收 ; 如果不是, 这个对象就会被回收.

Memory Analyzer (MAT)

3. 四种引用
  1. 强引用

    只有所有 GC Roots 对象都不通过【强引用】引用该对象, 该对象才能被垃圾回收.

  2. 软引用 (SoftReference)

    仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象

    可以配合引用队列来释放软引用自身

  3. 弱引用 (WeakReference)

    仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象

    可以配合引用队列来释放弱引用自身

  4. 虚引用 (PhantomReference)

    必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存

  5. 终结器引用 (FinalReference)

    无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

二. 垃圾回收算法

1. 标记清除算法

是最基础的垃圾回收算法, 分为 "标记" 和 "清除" 两个阶段, 用于识别并回收内存中的垃圾对象.

两个阶段:

  • 标记出存活对象 --> 释放垃圾对象所占用的空间.

(1) 标记 : 将 能被 GC Roots 直接或间接引用到的对象 标记为 "存活对象", 剩余没有被标记到的对象就是 "垃圾对象".

(2) 清除: 遍历整个堆内存区域, 找到所有 "垃圾对象", 并将其内存块标记为 "空闲内存", 并更新空闲内存链表.

  • 优点: 实现简单, 速度快.

  • 缺点: 容易产生内存碎片 (很多不连续的内存空间).

2. 标记整理算法

是在标记清除算法的基础上, ++针对其内存碎片化问题进行改进++ 的一种垃圾回收算法. 其核心过程分为 标记, 整理, 清除 三个阶段.

三个阶段:

  • 标记出存活对象 --> 将存活对象移动到内存的一端, 避免内存碎片化 --> 释放垃圾对象占用的内存空间

(1) 标记 : 将 能被 GC Roots 直接或间接引用到的对象 标记为 "存活对象".

(2) 整理: 将所有存活对象移动到内存的一端, 另一端则是连续的空闲内存区域.

(3) 清除 : 直接将 ++除存活对象外的其他内存区域++ 标记为空闲.

  • 优点: 解决了内存碎片问题.

  • 缺点: 实现复杂, 速度慢, 在整理阶段要移动大量存活对象, 可能会导致程序暂停时间较长.

3. 复制算法

++复制算法++ 将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块. 当这一块内存用完了, 就将还存活着的对象复制到另外一块上面, 然后再把已使用过的内存空间全部释放掉.

(1) 内存分区 : 将堆内存划分为两块大小相等的区域 (From 区To 区). 其中, To 区 是空闲的.

(2) 标记: 标记出所有存活对象

(3) 复制: 将所有存活对象复制到 To 区中.

(4) 清除: 清空 From 区.

(5) 空间切换: 将 From 区 和 To 区 进行角色互换.

  • 优点: 效率高, 速度快 且 无内存碎片问题.

  • 缺点: 内存利用率低, 始终只有一半的内存空间可用, 造成了内存的浪费.

三. 分代回收

堆空间分为 新生代 和 老年代, 其中新生代又分为 伊甸园区, 幸存区 From 和 幸存区 To.

  1. 新创建的对象都分配在伊甸园区.

  2. 随着对象的增多, 新生代空间不足时, 就会触发 新生代垃圾回收 Minor GC (采用复制算法), 伊甸园区 和 From 区的存活对象会被复制到 To 区 (同时清空 from 区 和 伊甸园区). 然后 存活对象年龄+1, 并且 交换 From 区 和 To 区.

  3. 当新生代中存活对象的年龄达到一定阈值 (默认是15 (4bit 最大就能表示15)) 就会晋升到老年代中.

  4. 当老年代的内存空间不足时, 会先尝试触发一次 Minor GC. 如果 Minor GC 之后 空间仍不足, 就会触发 老年代垃圾回收 Full GC (采用标记清除法 或 标记整理法).

  5. Minor GC 和 *Full GC 都会触发 STW (Stop The World): 暂停其它用户的线程, 等垃圾回收结束, 用户线程才恢复运行. 但是 Minor GC 的 STW 时间更短, Full GC 的 STW 时间更长.

: 为什么要先触发一次 Minor GC ? --> Minor GC 回收新生代垃圾后, 能减少 "新生代对象晋升到老年代" 的压力, 可能原本要晋升的对象被回收掉了, 这样要晋升到老年代的对象就少了, 间接缓解了老年代的内存压力.

: 为什么要 STW 暂停用户线程 ? --> 为了避免 GC 线程与用户线程同时操作对象, 从而导致引用关系混乱, 引发内存安全问题.

: 为什么 Full GC 的 STW 时间比 Minor GC 更长? --> ① 新生代的存活对象少, 采用复制算法复制这些存活对象时效率很高 ; 而老年代存活对象很多, 标记耗时很长. ② 老年代的空间通常是新生代的 2~3 倍, Full GC 要扫描的空间比 Minor GC 更大, 所以更慢.

: 大对象直晋升机制 --> 如果一个对象占用的内存空间很大, 超过了新生代的空间大小 (例如 大对象9M, 新生代空间8M), 那么此时这个大对象就会直接不经过新生代, 直接进入老年代.

VM 相关参数
含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size)
动态调整幸存区比例 -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

四. 垃圾回收器

1. 串行垃圾回收器 (Serial GC)

-XX:+UseSerialGC = Serial + SerialOld

(1) 工作原理
  • 单线程执行: 仅使用一个 GC 线程完成所有垃圾回收工作.

  • 全程 STW: 在垃圾回收期间, 所有应用线程 (用户线程) 都会被暂停, 直到 GC 完成后才恢复运行.

  • 算法选择 : 年轻代用 ++复制算法++ , 老年代用 ++标记整理算法++ .

(2) 优缺点
  • 优点: 单线程无竞争, 在单核 CPU 下效率较高.

  • 缺点: STW 时间长, 应用暂停时间长, 影响响应速度.

(3) 适用场景
  • 内存较小的设备 (如个人电脑).

  • 单 CPU 环境.

  • 对 吞吐量 和 响应时间 要求不高.

2. 并行垃圾回收器 (Parallel GC)

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC

-XX:+UseAdaptiveSizePolicy

-XX:GCTimeRatio=ratio

-XX:MaxGCPauseMillis=ms

-XX:ParallelGCThreads=n

(1) 工作原理

++并行垃圾回收器++ 本质上就是 ++串行垃圾回收器++多线程版本. (也采用 复制算法 + 标记整理算法)

  • 多线程并行: 启动多个 GC 线程 (数量通常与 CPU 核心数相同, 也可手动设置) 同时执行垃圾回收.

  • 仍有 STW: 回收期间应用线程依然会被暂停, 但由于多线程并行处理, 相同工作量下的 STW 时间比串行 GC 短得多。

  • 优化目标 :提高吞吐量

(2) 优缺点
  • 优点: 充分利用多核 CPU: 多线程并行进行垃圾回收, 大幅缩短 STW 时间, 提高了吞吐量.

  • 缺点: 仍存在 STW. 在高并发场景下, 短暂的 STW 仍会使得响应延迟.

(3) 适用场景
  • 对吞吐量要求高, 对响应时间 (延迟) 要求不高的服务器应用.
3. 并发垃圾回收器 (Concurrent GC)
① CMS 垃圾收集器

初始标记 --> 并发标记 --> 重新标记 (用于修正对象引用) --> 并发清理

  1. 初始标记: 触发 STW. 标记根对象 GC Roots.

  2. 并发标记: 无 STW. 从初始标记的根对象出发, 遍历整个堆空间, 标记所有存活对象.

  3. 重新标记: 触发 STW. 处理并发标记期间 引用发生修改 的对象.

  4. 并发清理: 无 STW, 用户线程正常执行, 垃圾回收线程清理所有标记好的垃圾.

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads

-XX:CMSInitiatingOccupancyFraction=percent

-XX:+CMSScavengeBeforeRemark

(1) 工作原理
  • 分多阶段执行: 将垃圾回收拆分为多个阶段. 大部分阶段 (并发标记, 并发清除) 与用户线程同时执行. 仅初始标记, 重新标记等少数阶段需要短暂 STW.

  • 并发协调: 由于 GC 线程与用户线程同时执行, 会存在 "对象引用动态变化" 的问题 (如用户线程修改对象引用导致 GC 标记结果失效). 因此需要更复杂的算法来解决此问题.

(2) 优缺点
  • 优点: STW 时间极短, 仅在关键阶段暂停用户线程.

  • 缺点:

    • CPU 开销高, GC 线程与应用线程共享 CPU 资源, 可能导致应用运行速度变慢 (吞吐量下降)

    • 实现复杂:需要处理并发场景下的对象引用一致性,算法复杂(如 CMS 的并发标记 - 清除、G1 的并发标记)。

(3) 适用场景
  • 对响应时间 (延迟) 要求严格的高并发应用. 如: 电商网站, 金融交易系统.

: 什么是吞吐量? --> 指 应用运行时间 占总时间 (应用运行时间 + 垃圾回收时间) 的比例. 也可以说是"运行效率", "运行速度".

② G1 垃圾回收器

G1 ("Garbage First")

(1) Region 分区

传统 GC 将堆划分为年轻代 (Eden + Survivor) 和 老年代, 而 G1 将堆划分为 多个大小相等的独立区域 (Region). Region 的类型如下:

  • Eden Region: 伊甸园区

  • Survivor Region: 幸存区

  • Old Region: 老年代区

  • Humongous Region : H区 (是O区的一种). 大对象不经过新生代, 直接存进老年代的H区. (大小超过 Region 的 1/2 就可以算作大对象).

(2) 回收过程

G1 的垃圾回收分为 Young GC 和 Full GC.

① Young GC:

年轻代垃圾回收.

当 Eden 区域占满时触发年轻代回收 (Young GC) 仅回收年轻代的 Region.

++执行过程++:

标记 Eden 区和 幸存区 中的存活对象 (通过 GC Roots 遍历寻找). 将 Eden 区 和 幸存区 中的存活对象复制到另一块 幸存区中. 同时清空 Eden 区 和 已被回收的 幸存区, 变为空闲 Region. 同时 年龄达到阈值的对象晋升至老年代.

: Young GC 期间, 会触发 STW, 暂停所有用户线程.

② Mixed GC:

混合垃圾回收.

MixedGC 的过程 和 CMS 极为相似.

  1. 初始标记: 触发 STW. 标记根对象 GC Roots.

  2. 并发标记: 无 STW. 从初始标记的根对象出发, 遍历整个堆空间, 标记所有存活对象.

  3. 重新标记: 触发 STW. 处理并发标记期间 引用发生修改 的对象.

  4. 垃圾清理: 触发 STW. 为了保证 STW 的时间不会太长, G1 只筛选出回收价值高 (垃圾对象多)的 Region 进行清理, 而不是将所有垃圾全部清理.

4. 小结
  • 串行垃圾回收器: 适合内存小的个人电脑.

  • 并行垃圾回收器: 最大化系统吞吐量, 适用于对吞吐量要求较高的系统.

  • 并发垃圾回收器: 最小化响应时间, 适用于对延迟要求较高的系统.

五. GC 调优

  • 掌握 GC 相关的 VM 参数, 熟悉基本的空间调整.

  • 掌握调优相关工具.

  • 明白一点: 调优跟应用环境有关. 没有放之四海而皆准的法则.

相关推荐
寒月霜华2 小时前
java-File
java·开发语言
im_AMBER2 小时前
hello算法笔记 02
笔记·算法
Michelle80232 小时前
决策树习题
算法·决策树·机器学习
hn小菜鸡3 小时前
LeetCode 2540.最小公共值
数据结构·算法·leetcode
Tisfy3 小时前
LeetCode 0611.有效三角形的个数:双指针
算法·leetcode·题解·双指针
yujkss3 小时前
23种设计模式之【抽象工厂模式】-核心原理与 Java实践
java·设计模式·抽象工厂模式
Keying,,,,3 小时前
力扣hot100 | 多维动态规划 | 62. 不同路径、64. 最小路径和、5. 最长回文子串、1143. 最长公共子序列、72. 编辑距离
算法·leetcode·动态规划
我命由我123453 小时前
Android 实例 - Android 圆形蒙版(Android 圆形蒙版实现、圆形蒙版解读)
android·java·java-ee·android studio·安卓·android-studio·android runtime