欢迎浏览高耳机的博客
希望我们彼此都有更好的收获
感谢三连支持!
🙉上篇博客介绍了JVM的类加载机制,它负责将代码从编译器生成的.class
文件动态加载到运行时,确保程序的顺利执行。JVM类加载三步解读: 双亲委派模型如何维护Java生态-优快云博客https://blog.youkuaiyun.com/Chunfeng6yugan/article/details/144350327?spm=1001.2014.3001.5501
类加载机制只是JVM的一部分,而内存管理才是编程中更为关键的环节。JVM内置的垃圾回收器和算法能有效管理内存,减少泄漏风险。因此,本篇博客将重点聚焦于JVM的垃圾回收机制。
目录
一、为什么要进行垃圾回收?
🍉程序运行过程中会不断创建和使用对象,而Java堆中存放着几乎所有的对象实例,这些对象占用内存。如果这些对象在使用完毕后,内存没有被释放,就会导致内存泄漏。随着时间推移,内存逐渐被耗尽,程序将无法分配新的内存,最终可能导致系统崩溃。垃圾回收机制就是为了解决这一问题,它能够自动释放不再使用的内存,使程序无需手动管理内存,减少内存泄漏的风险,提高程序的可靠性和开发效率。
二、垃圾回收主要回收哪个内存区域?
🍊对于程序计数器、虚拟机栈和本地方法栈,其生命周期与线程紧密绑定,随线程的启动而创建,随线程的结束而销毁。这些区域的内存分配和回收具有明确的规律,当方法执行完毕或线程终止时,相关内存会自动释放。
元数据区中需要加载的类对象,通常都是有上限的。因此,垃圾回收主要针对的是堆内存中的对象。堆内存是运行时数据区中最大的一部分,用于存储通过 new
关键字创建的对象。
三、标记的过程
🍍垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经“死去”。判断对象是否已“死”(可回收)有如下两种常用的标记算法:
(1) 引用计数
该方法为每个对象维护一个引用计数器,每当一个地方引用该对象时,计数器加 1;当引用失效时,计数器减1。当计数器降为 0 时,对象不再被引用,可被回收。
优点
实现简单:能够快速判断对象是否存活。
缺点
消耗额外空间
例如,一个类中只有一个int
成员(4个字节),为了使用引用计数,至少需要一个short
(2个字节)来存储引用计数器。这意味着内存额外占用了50%。无法解决循环引用问题
例如,两个对象相互引用,但又都不可达。此时,它们的引用计数都不会降为0,导致无法被垃圾回收器回收。
🥥Java(JVM)并没有采用引用计数标记方法
(2) 可达性分析
可达性分析的大致逻辑是,垃圾回收器从 GC Roots(垃圾回收根节点) 出发,沿着引用链尽可能多地访问对象,标记它们为存活对象。这个过程类似于在图中进行遍历,而不是树的遍历。
直白解释:
GC Roots 是起点:GC Roots 是一组特殊的对象,比如虚拟机栈中的局部变量、方法区中的静态变量、本地方法栈中的JNI引用等。
从起点出发:垃圾回收器从这些 GC Roots 开始,沿着对象之间的引用关系(比如对象A引用对象B)进行访问。
尽可能访问更多对象:垃圾回收器会尽可能多地通过引用链访问到其他对象,标记这些对象为“存活”。
图的遍历:因为对象之间的引用关系可能形成复杂的图结构(比如循环引用),所以这个过程更像是图的遍历,而不是树的遍历。
举个例子:
🍑假设有一个对象A,它引用了对象B,而对象B又引用了对象C。垃圾回收器从GC Roots(比如对象A)出发,通过引用链访问到对象B,再从对象B访问到对象C。如果对象D没有被任何对象引用,那么它就无法被访问到,最终会被标记为垃圾回收的目标。
四、回收的过程
🍅垃圾回收阶段会根据不同的垃圾回收算法,对已标记的可回收对象进行处理。以下是几种常见的垃圾回收算法及其回收过程:
(1) 标记 - 清除算法
标记阶段:
通过可达性分析法直接针对内存中的对应对象进行释放。
清除阶段:
清除未被标记的对象,并回收其占用的内存。该算法简单,但容易产生内存碎片,并且标记和清除的效率都较低。
(2) 复制算法
标记阶段:
同样通过可达性分析法标记存活对象。
回收阶段:
将存活对象复制到另一个内存区域,然后清理原内存区域。这样每次回收时都是一次对整个半区的回收,内存分配时也不需要考虑内存碎片等问题,只需要移动堆顶指针,按顺序分配即可。不过,复制算法需要有较大的内存空间作为担保,空间利用率较低。同时,如果被标记的存活对象较大较多,那么复制的开销成本也会增大。
(3) 标记 - 整理算法
标记阶段:
同样通过可达性分析法标记存活对象。
回收阶段:
让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。这样可以避免标记 - 清除算法产生的内存碎片问题,但其标记和整理的效率相对较低。
(4) 分代算法*
分代:
根据对象的存活周期将内存划分为不同的区域,每一轮GC过后,存活对象的"年龄"则会+1,通常包括新生代和老年代。新生代中存在"伊甸区","幸存区0"和"幸存区1"。新生代中的对象大多是“朝生夕灭”的,存活率低,而老年代中的对象存活率高。
垃圾回收:
新的对象将会被创建在"伊甸区",一轮GC过后,被标记的存活对象会被移动至随机一个"幸存区"。多轮GC过后,"年龄"较大的对象会被移动至老年代。新生代使用复制算法,老年代使用标记 - 整理或标记 - 清除算法(根据对象存活率选择合适的算法)。
🥑分代回收,是JVM的GC中的基本思想方法
五、垃圾回收器的典型实现
🫐自 Java 语言诞生以来,垃圾收集器也随之出现,而如今众多的垃圾收集器是随着技术发展而逐渐演变的产物。
最早的垃圾收集器是 Serial,它是一个单线程的垃圾收集器,仅支持串行执行。随后,为了提升性能,出现了 Serial Old,这是专为老年代设计的单线程收集器。随着时间推移,为了进一步提高效率,人们开发了 ParNew,这是 Serial 的多线程版本,能够并行执行垃圾回收任务。
然而,随着对性能要求的不断提高,人们开始追求更高的吞吐量,即单位时间内成功回收垃圾的数量。因此,出现了 Parallel Scavenge 和 Parallel Old,这两个垃圾收集器分别针对新生代和老年代,以吞吐量优先为目标,通过多线程并行回收来提升效率。
随后,为了兼顾吞吐量和减少垃圾回收的停顿时间,CMS(Concurrent Mark Sweep) 垃圾收集器应运而生。CMS 是一个并发标记-清除收集器,能够在垃圾回收过程中与用户线程并发执行,从而显著减少停顿时间。在 JDK 1.8(含)之前,CMS 是大多数业务系统的主流垃圾收集器。
然而,CMS 也存在一些局限性,例如在高并发场景下可能出现“并发模式失败”(Concurrent Mode Failure),导致 Full GC 的频繁触发。为了解决这些问题,JDK 1.8 引入了 G1(Garbage First) 垃圾收集器。G1 是一个基于区域划分的垃圾收集器,它将堆内存划分为多个独立的区域(Region),并根据垃圾回收的优先级动态选择回收区域。G1 的目标是在几乎不需要停止程序的情况下完成垃圾回收,从而实现低延迟和高吞吐量的平衡。
!!! 了解Minor GC 和 Full GC?这两种 GC 有什么不一样?
Minor GC(新生代 GC)
Minor GC 是指发生在新生代的垃圾收集。由于 Java 对象大多具有“朝生夕灭”的特性,即大部分对象在创建后很快就会失效,因此 Minor GC 非常频繁。它通常采用复制算法,回收速度较快。新生代的 Minor GC 主要负责清理 Eden 区和 Survivor 区,将存活对象复制到另一个 Survivor 区,然后清理 Eden 区和当前使用的 Survivor 区。Full GC(老年代 GC 或 Major GC)
Full GC 是指发生在老年代的垃圾收集。当 Full GC 发生时,通常会伴随至少一次的 Minor GC(但并非绝对,例如在 Parallel Scavenge 收集器中,可能会直接触发 Full GC)。老年代的对象生命周期较长,因此 Full GC 的频率相对较低,但每次执行的时间通常比 Minor GC 慢 10 倍以上。老年代的垃圾回收通常采用标记-整理算法或标记-清除算法,以处理对象的长期存活和内存碎片问题。
🙉本篇博客探讨了 JVM 的垃圾回收机制,解释了其重要性、主要针对的内存区域(堆内存)、标记和回收的过程等问题。通过这些内容,你可以快速掌握垃圾回收的核心知识点,了解如何通过选择合适的回收器和优化策略提升程序性能,从而更好地利用 JVM 的自动内存管理功能来保障 Java 的稳定运行。
希望这篇博客能为你理解JVM垃圾回收提供一些帮助
如有不足之处请多多指出
我是高耳机