JVM 垃圾回收机制详解:从标记过程、回收算法到垃圾收集器

  • 🏠博客主页:小谢同学的小破站
  • ✍️本文由 小谢同学的小破站 原创,首发于 CSDN 💻
  • ☕JavaSE专栏:JavaSE
  • 📗JavaEE初阶专栏:JavaEE初阶
  • 📘JavaEE进阶专栏:JavaEE进阶
  • 🧩数据结构专栏:数据结构
  • ⚙️算法专栏:算法
  • 📚MySQL初阶专栏:MySQL初阶
  • 🔐MySQL进阶专栏:MySQL进阶
  • 🌐计算机网络专栏:计算机网络
  • 💻C语言专栏:C语言
  • 👍欢迎点赞👍 收藏⭐ 留言📝,发现错误欢迎指正!
  • ✨脚踏实地,持续深耕,奔赴自己的目标✨
    ----- 📌分割线 📌-------

JVM垃圾回收机制

  • [1. 为什么要有垃圾回收?](#1. 为什么要有垃圾回收?)

    • [1.1 垃圾回收主要回收哪个内存区域?](#1.1 垃圾回收主要回收哪个内存区域?)
    • [1.2 如何识别出垃圾对象?](#1.2 如何识别出垃圾对象?)
      • [1.2.1 方案一:引用计数](#1.2.1 方案一:引用计数)
      • [1.2.2 方案二:可达性分析](#1.2.2 方案二:可达性分析)
  • [2. JVM垃圾回收过程](#2. JVM垃圾回收过程)

    • [2.1 标记‑清除](#2.1 标记‑清除)
    • [2.2 复制算法](#2.2 复制算法)
    • [2.3 标记整理](#2.3 标记整理)
    • [2.4 分代回收思想](#2.4 分代回收思想)
  • [3 垃圾回收器有哪些典型实现?](#3 垃圾回收器有哪些典型实现?)

  • [4. 总结](#4. 总结)

前言:

   这里是小谢同学整理的JVM相关"如何实现垃圾回收的"相关笔记。笔记用于自我复盘巩固,有错误欢迎大家指出,专栏还有 Java、网络、C 语言等系列笔记欢迎翻阅~同时也希望这篇文章能够帮助到你!

为什么要有垃圾回收?

 Java 程序运行过程中会不断通过new来创建对象,对象会占用内存空间。当部分对象不再被业务逻辑使用时,如果内存不及时释放,就会持续占用内存,最终耗尽内存资源,引发 OOM(OutOfMemoryError,内存溢出)

 对比 C/C++ 语言,需要开发者手动调用 API 申请和释放内存,很容易出现忘记释放内存、重复释放内存等问题。

据说:C++曾经也讨论过要不要引入Java中的垃圾回收机制(GC),但是这个机制不满足C++追求极致性能的理念,就放弃了~后面C++搞了一个智能指针(但是这个指针好像不是特别智能)

 Java 引入垃圾回收(GC,Garbage Collection)机制,能够自动识别无效对象,回收无用内存,把开发者从手动内存管理中解放出来,提升开发效率,降低内存泄漏风险。

类似于Java雇佣了一个保洁阿姨,房间中的垃圾每天都会有保洁阿姨来进行收拾~

垃圾回收主要回收哪个内存区域?

 JVM 运行时数据区分为五部分:程序计数器、虚拟机栈、本地方法栈、堆、方法区。

我们知道:

  • 栈溢出:原因是我们程序中出现死循环了
  • 堆溢出:new的对象太多了

但是这么多个区域,JVM的垃圾回收机制服务于哪一个区域?

  1. 程序计数器,虚拟器栈,本地方法栈这三个部分属于线程所以,线程结束后会自动进行释放->此处不需要引入垃圾回收机制
  2. 堆区中的内存,所以new出来的对象实例都会保存在堆区中->堆区是GC最核心的回收区域
  3. 方法区(元空间)也会存在垃圾回收,但是主要回收的是废弃的类,无效常量,此处的回收性价比比较低->不是GC重点

总结:垃圾回收主要回收堆区,回收的单位是对象,不会出现只释放一般的情况~
[🔙 返回目录](#🔙 返回目录)

如何识别出垃圾对象?

判断对象是否具有引用指向来判断哪些对象是死亡对象(垃圾)也就是不再使用的对象,哪些对象还存活。HotSpot 虚拟机提供两种识别方案。

 遵循的原则: 宁可放过也不要错杀~

方案一:引用计数

 引用计数:给每一个对象身上再安排一个空间,这个空间存储一个整数,用这个整数来表示指向对象的引用个数~

例如:创建A对象

此时有两个对象引用了A,此时存储的个数就会变成2

如果此时引用计数的值为0,就会判定为垃圾,后续就会进行回收~

同时也引用计数也有缺点

  1. 如果我new出来的对象很小呢?岂不是一个计数器就占了我一半的空间?但是如果这样的对象非常多呢?->此时就会造成很多不可必要的浪费
  2. .产生循环引用,会导致出现误判(出现这个情况,就不会进行回收)

假设此时有一个类为Test

class Test {

 Test t;

}

java 复制代码
Test a = new Test();
Test b = new Test();
java 复制代码
a.t = b;
b.t = a;

此时如果我将a和b置为null呢?

java 复制代码
a = null;
b = null;


JVM中并没有使用引用计数的方式,JVM中使用的是可达性分析
[🔙 返回目录](#🔙 返回目录)

方案二:可达性分析

 可达性分析是衡量从一个节点到另一个节点的可到达程度的分析方法,可应用于图论、交通网络和计算机内存管理等领域。

 在Java虚拟机(JVM)中,可达性分析用于判断对象是否可以被回收。算法原理如下:

  1. 根对象(GC Roots):确定程序中必须存活的对象作为起点。
  2. 引用链搜索:从根对象出发,沿对象引用关系向下搜索,形成引用链。
  3. 判断存活:如果某对象无法通过任何引用链到达GC Roots,则该对象不可达,可被垃圾回收

简单来说:

设定一系列 GC Roots 作为根节点,沿着引用链向下遍历:

  • 遍历能够到达的对象:存活对象,不能回收;
  • 从 GC Roots 出发遍历不到的对象:判定为垃圾对象。

GCRoots 常见来源:

  1. 虚拟机栈中局部变量所引用的对象
  2. 类静态变量引用的对象
  3. 常量引用的对象
  1. 本地方法栈 JNI(native 方法) 引用的对象
    [🔙 返回目录](#🔙 返回目录)

JVM垃圾回收过程

 标记完成之后,JVM就知道XXX是垃圾了,就会执行回收操作,主要有标记-清除,复制算法,标记整理三种回收方式,基于这三种回收方式衍化出分代回收

标记‑清除

标记-清除的方式:

  1. 标记:从 GC Roots 遍历,标记全部存活对象;
  2. 清除:把所有未标记的垃圾对象直接回收。

    问题:
     如果每次都是这样的清除,就会出现东一块西一块的问题,也就是内存碎片,假设我申请了很多个小的内存,后面不使用的时候,被垃圾回收掉了,每个内存碎片隔的距离很近->如果我此时申请了一个很大内存的对象呢?总的空间足够,但是会申请失败
     这就是标记清楚带来的内存碎片化问题~

    [🔙 返回目录](#🔙 返回目录)
复制算法

复制算法:

 会将内存划分为两块同等大小的内存区域只使用其中一块

  1. 标记出存活对象;
  2. 将全部存活对象复制到另一块空闲内存
  3. 直接清空当前使用区域全部内存。

    ❤️ 优点:不会产生内存碎片。
    ❤️ 缺点:可用内存直接缩减一半,内存开销大。
    [🔙 返回目录](#🔙 返回目录)
标记整理

标记整理:

  1. 会标记所以存活的对象。
  2. 将所有存活的对象向内存的一端移动压缩。
  3. 直接清理掉边界以外的全部垃圾内存。

    ❤️ 优点:没有内存碎片,保留完整连续内存
    ❤️ 缺点:需要移动对象,开销非常较大
    [🔙 返回目录](#🔙 返回目录)
分代回收思想

 对于Java的实际情况来说,分代回收的思想是结合了上述的策略,构成了一个更加复杂的方案;

 分代回收:会根据对象的情况/特点 来采取不同的方案~

 根据对象的年龄来采取方案(GC扫描的轮次,是根据周期性进行的~)->如果某一个对象经过了很多次的GC轮次还没有消亡,就有很大的可能性继续活下去

 经验规律来总结的: 一半来说,如果某一个对象比较大,有很大的概率活继续活下去,就不会去进行回收,总体来讲就是要G早就G了

 ❤️ 类似于C语言这个语言,早期特别古老的语言,到现在还没有消亡,也就是说,未来C语言还是有很大的可能性活下去

基于对象存活的时间不同,把堆划分为新生代、老年代,不同代使用适配的回收算法:

新生代又分为:伊甸区,幸存区

  1. 新生代 :对象朝生夕灭,大部分对象很快死亡,使用复制算法,Minor GC 频率高;
  2. 老年代:对象存活时间长,存活对象多,使用标记‑清除、标记‑整理,Major GC 频率低。
  1. 当我们新new出一个对象时,会放入到伊甸区当中
  2. 伊甸区对象进过一次GC的时候,此时会将绝大部分淘汰掉~(经验规律,大部分新的对象生命周期都很短) ~>幸存下来的对象会通过复制算法进入到幸存区,幸存区分为两部分,每次使用其中一部分
  3. 下一轮GC也会堆幸存区的对象进行扫描,还会再次淘汰掉大部分,没有淘汰的对象还会通过复制算法进入到另一个幸存区

由于经验规律发现,每轮GC淘汰掉大部分的新对象因此触发复制的对象就很少解决了复制算法,拷贝开销大的问题另一方面,幸存区只是占据新生代的10%浪费的内存空间比较小了~~

  1. 随着每一轮的GC扫描,对象就会在新生代的幸存区中来回进行拷贝,每次经过一次拷贝,年龄就会+1
  2. 经过一定的时间之后,对象的年龄达到了一定的阈值,此时就会将这个对象拷贝到老年代
  3. 对象进入到老年代,针对老年代的GC频率会比新生代降低很多,减少扫描的开销~老年代发现垃圾就会采用标记整理的方式进行处理
  • 针对老年代的扫描,称为Major GC
  • 针对新生代的扫描,称为Minor GC
  • 这两者整体称为Full GC

 就类似于我们去找实习的过程~ 投递简历的时候就会筛选掉大部分人,然后笔试,面试,有一面二面三面等等,经过层层筛选最终拿到offer~

小插曲->类的一生

⼀个对象的一生:我是⼀个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺⻓时间。
 有⼀天Eden区中的⼈实在是太多了,我就被迫去了survivor区的"From"区(S0区),自从去了Survivor区,我就开始漂了,有时候在Survivor的"From"区,有时候在Survivor的"To"区(S1区),居无定所。
 直到我18岁的时候,爸爸说我成⼈了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。
 在老年代里,我⽣活了很多年(每次GC加⼀岁)然后被回收了。

[🔙 返回目录](#🔙 返回目录)

垃圾回收器有哪些典型实现?
收集器 适用内存代 特点
Serial 收集器 新生代 单线程收集,STW,适合客户端单机程序
Serial Old 收集器 老年代 单线程,标记-整理,Serial 的老年代版本
Parallel Scavenge 新生代 多线程,优先追求吞吐量
Parallel Old 老年代 多线程标记-整理,配合 Parallel Scavenge
CMS 收集器 老年代 并发标记清除,追求低停顿;会产生内存碎片,JDK9 之后废弃
G1 收集器 全堆(新生代 + 老年代) 分区化回收,可预测停顿时间,JDK9 默认收集器
ZGC 收集器 全堆 极低延迟,STW 几乎不受堆内存大小影响,JDK15 默认收集器
总结
  1. GC 目的:自动回收无用对象内存,防止 OOM,简化开发者内存管理;
  2. GC 主要回收Java 堆内存
  3. JVM识别垃圾采用可达性分析算法,以 GC Roots 作为起点;引用计数存在循环引用的缺陷;
  4. 三大基础回收算法:标记‑清除、复制算法、标记‑整理;结合对象生命周期衍生分代回收;
  1. 垃圾回收器是算法的实现:Serial、Parallel、CMS、G1、ZGC,分为吞吐量优先和低停顿优先两大类。
    [🔙 返回目录](#🔙 返回目录)
相关推荐
JavacKaka1 小时前
Redis大Key导致生产事故复盘博客-2026-09-18
java
xieliyu.1 小时前
前端基础:常见CSS选择器用法
前端·css·笔记·vscode
周玉奎先生1 小时前
河南少商新材料CGM灌浆料:扎根河南工程现场的水泥基灌浆材料
经验分享·笔记·零售
动词ing1 小时前
【学习笔记】动态规划+背包问题
笔记·学习
右耳朵猫AI1 小时前
Java周刊2026W38 | Micronaut 修补三漏洞、JDK 27 提速 54%、Jetty 修复抖动测试
java·后端·spring
事已至此先睡覺吧1 小时前
第二篇:Java 基础语法:变量、数据类型、运算符与类型转换
java
Wang's Blog2 小时前
Java 项目实战: 外卖平台-后台退出功能与首页iframe架构
java·服务器·redis
Wang's Blog2 小时前
Java 项目实战: 外卖平台-软件开发流程与项目整体介绍
java·服务器·redis
她说..2 小时前
常见设计模式-模板方法模式
java·spring·设计模式·springboot