十分钟让你搞懂JVM中的GC垃圾回收机制(分代回收)

本文探讨了Java垃圾回收的必要性、内存区域划分、引用计数和可达性分析的判断方法,以及标记-清除、复制和标记-整理等清理策略。重点介绍了JVM的分代回收机制,对象在新生代和老年代的迁移规则。最后概述了垃圾回收器的角色和重要收集器的发展历程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


0. 为什么要有垃圾回收?

JVM 中的垃圾回收机制, 可以自动的释放不再使用的内存. 可以有效的防止程序中内存泄漏问题. 本文我们就来聊一下垃圾回收机制.

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

关注收藏, 开始学习吧🧐


1. 垃圾回收哪个内存区域?

Java运行时内存的有许多区域。对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。

因此本文所讲的有关内存分配和回收关注的为Java 堆 与 方法区 这两个区域。

垃圾回收, 应该分为两步:

  1. 找到垃圾.
  2. 清理垃圾.

对于 Java 来说, 垃圾回收, 回收的是 “对象”, 而不是 “字节”. 所以我们会认为 “垃圾” 指的就是 “已经死亡的对象”, 那么我们如何判定这个对象是否已经死亡, 是否是垃圾呢?

2. 如何找到垃圾(死亡对象的判断)

如果一个对象, 在后续的代码中, 不会被继续使用到了, 就可以视为是垃圾了. 那么我们该怎样判断这个对象不会再被继续使用到了呢?

在 Java 中, 使用一个对象, 只有一种途径, 就是搞个引用指向它, 然后通过引用来访问这个对象.

  • 一个对象, 只要有引用指向, 就无法确保后续孩会不会进行使用.
  • 一个对象, 如果没有引用指向, 那么这个对象以后一定无法被使用.

所以我们可以认为, 一个对象, 如果没有任何引用指向它, 就代表着之后也不会被使用到了, 那它就是一个 “垃圾” 了. 接下来我们来介绍两个主流的判断方法.

2.1 引用计数法

该方法简单来说, 就是给每个对象中安排一个计数器, 每次有引用指向它时, 内置的计数器就 +1. 每次有引用被销毁时, 计数器就 -1. 当计数器成为 0 时, 意味着该对象就变成垃圾了.

引用计数法实现简单, 判定效率也比较高, 在大部分情况下都是一个不错的算法, 比如 Python 语言就采用引用计数法进行内存管理.

但上述方案, 并不是 JVM 中判断垃圾所使用的方案. 是因为引用技术方案, 还存在着两个无法忽视的问题:

  1. 空间利用率会比较低, 浪费了不必要的内存空间. 如果一个对象本体只有 4 个字节时, 给引用计数分配 2 个字节, 就相当于浪费了 50% 的内存空间.
  2. 可能会存在循环引用的问题, 导致对象不能被正常识别为垃圾.

观察循环引用问题.
在这里插入图片描述
当前是简单画了一个内存草图, 我们执行左面的伪代码, 就会造成右面的内存布局. 可以看到, 明明已经没有引用指向对象了, 可是对象中的计数器, 还没有到 0. 此时这俩对象的引用计数不为 0, 就不能被视为垃圾, 也就无法被识别清理.

2.2 可达性分析法

上面我们说到, Java并不是采用引用计数法来判断对象是否已经"死亡", 其采用的其实是 “可达性分析法”.

JVM 首先会从现有代码中能直接访问到的引用出发, 尝试遍历所有能访问的对象. 只要这个对象能被访问到, 就被视为 “可达”, 反之为不可达. 完成整个遍历后, 视所有 “不可达” 的对象为垃圾.

此算法的核心思想为 : 通过一系列称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到 GC Roots 没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。以下图为例:

在这里插入图片描述
对象 Object5-Object7 之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。

在 Java 中,可作为GC Roots的对象包含下面几种:

  1. 栈上的局部变量.
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象。

2.3 两种算法的差别

  • 引用计数消耗的是空间, 通过引用计数的数值, 不需要持续判断, 就可以非常快速的得知这个对象是否是垃圾.
  • 可达性分析消耗的是时间, 不消耗额外的空间开销, 通过持续性的扫描, 就可以得知哪些对象是垃圾.

3. 如何清理垃圾(死亡对象的回收)

我们找到垃圾之后, 就应该对它进行清理了. 目前主流的一些清理方法有以下几种.

3.1 标记-清楚法

"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。但该算法也有一些问题.

"标记-清除"算法的不足主要有两个 :

  1. 效率问题 : 标记和清除这两个过程的效率都不高
  2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

在申请内存时, 都是申请的连续内存空间, 释放内存, 就可能会破坏原有的连续性, 导致 “有内存, 但无法申请”.

在这里插入图片描述
假设当前剩余四块空间(黑色的)可以申请, 每个空间大小是 1mb, 总的空闲空间是 4mb, 但其实实际上已经无法继续分配大于 1mb 的内存了. 这种问题就叫做 “内存碎片” 问题.

3.2 复制法

"复制"算法是为了解决"标记-清理"的问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。

在这里插入图片描述

就是把一个内存, 分成两份空间 A 和 B, 只用一份A. 清理时, 把 A 中存活下来的对象全部复制到 B 中, 把 A 中内存统一释放 接下来就继续使用 B 空间. 再次清理时, 把存活下来的对象再复制回 A 中去, 把 B 中资源统一释放. 当前流程一直循环进行.

这个算法虽然提高了一些效率, 但也有些不足的地方,

  • 内存李永利比较低, 他真正能利用的内存空间, 只有原先内存空间的一半, 很浪费内存空间.
  • 在对象存活率较高时会进行比较多的复制操作, 效率会变低.

3.3 标记-整理法

针对 “标记-清楚” 提出了一种为"标记-整理"的算法。标记过程仍与 “标记-清除” 过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。

在这里插入图片描述

这样的方式, 避免了刚才复制算法中内存利用率比较低的问题. 但是也有个很明显的个问题, 就是这里搬运的成本, 也是比较高的.

4. JVM使用的回收方法

设计 JVM 中垃圾回收算法时, 虽然已经有了这么多的回收机制, 但还是没有哪个方法能让设计 JVM 的大佬们满意, 于是他们想了个办法, 集百家之长, 结合上面的回收方案, 搞一个综合性质的方案, 在不同的场景下, 使用不同的回收方式, 做到扬长而避短.

4.1 什么是分代回收

分代算法和上面讲的 3 种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收。对于不同的情况设置更符合该情况的规则,从而达到更高的效率,这就是分代算法的设计思想。

当前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

4.2 哪些对象会进入新生代?哪些对象会进入老年代?

  • 新生代:一般创建的对象都会进入新生代;
  • 老年代:大对象和经历了 N 次(一般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代移动到老年代。

4.3 工作过程

在这里插入图片描述

  • 堆内存为两个区:新生代 (Young) 和老年代 (Old);
  • 新生代默认占堆内存的 1/3,老年代默认占堆内存的 2/3;
  • 新生代又分为 Eden 区、Survivor From区、Survivor To区默认比例是 8:1:1.

先理解两种GC:

  1. Minor GC 又称为 新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
  2. Full GC 又称为 老年代GC 或者 Major GC : 指发生在老年代的垃圾收集。出现了 Major GC,经常会伴随至少一次的 Minor GC。Major GC 的速度一般会比 Minor GC 慢10倍以上。

工作过程

  • 所有新创建的对象都在 Eden 区,当 Eden 区内存满后将 Eden 区+ Survivor From 区存活的对象复制到 Survivor To区;
  • 清空 Eden 区与 Survivor From 区;
  • 同时 Survivor From 与 Survivor To 分区进行交换;
  • 每次 Minor GC 存活对象年龄加 1,当年龄达到 15(默认值)岁时,被移到老年代;
  • 当 Eden 的空间无法容纳新创建的对象时,这些对象直接被移至老年代;
  • 当老年代空间占用达到阈值时,触发 Full GC;
  • 以上流程循环执行。

在这里插入图片描述

5. 垃圾回收器是什么

如果说上面我们讲的收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。

以下这些收集器是不同版本推出的重要的垃圾收集器, 读者了解一下即可.
在这里插入图片描述


总结

✨ 本文重点讲解了垃圾回收的一些概念, 包括怎么找垃圾, 怎么清理垃圾等等.
✨ 想了解更多知识, 请持续关注博主, 本人会不断更新学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.

再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!

### Spring Boot 应用中单线程处理大量数据导致 CPU 占用过高的解决方案 #### 1. 利用多线程并行处理 为了提升处理速度和降低CPU占用率,可以采用多线程技术来并发执行任务。通过合理分配工作负载到多个线程上,能够显著减少总耗时。 ```java import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service public class DataProcessingService { @Async public void processData(List<Data> dataList, int start, int end) { for (int i = start; i < end && i < dataList.size(); ++i) { // 执行具体的数据处理逻辑 processSingleData(dataList.get(i)); } } private void processSingleData(Data data) { // 实现具体的业务逻辑 } } ``` 配置 `application.properties` 文件启用异步支持: ```properties spring.task.execution.pool.max-size=20 spring.task.execution.pool.queue-capacity=100 ``` 这会创建一个最大容量为20的任务池[^1]。 #### 2. 减少上下文切换开销 当有过多活跃线程竞争资源时会发生频繁的上下文切换,从而影响整体性能。可以通过调整线程数量至最优水平来缓解此现象;另外也可以考虑批量提交任务给固定大小的工作队列而非直接启动新线程。 #### 3. 使用缓存机制减轻数据库压力 对于重复读取相同记录的情况,引入本地或分布式缓存(如Redis),可有效避免不必要的磁盘I/O操作,进而间接改善CPU利用率。 #### 4. 调整JVM参数优化垃圾回收行为 适当调节堆内存大小、新生代比例等GC相关设置有助于防止因频繁全量收集而造成的停顿时间增加问题。 例如,在 `application.yml` 中指定 JVM 参数: ```yaml server: jvmRoute: ${RANDOM_VALUE} management: endpoints.web.exposure.include: '*' --- spring: config.activate.on-profile: prod systemProperties: JAVA_TOOL_OPTIONS: "-Xms512m -Xmx2g -XX:+UseG1GC" ``` 以上措施均能在不同程度上帮助解决Spring Boot应用中由单线程循环处理大批量数据所引发的一系列性能瓶颈问题。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

慧天城寻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值