目录
(二)为什么了解 GC 对开发者尤为重要,特别是对于性能调优和内存管理
(一)分代收集(Generational Collection)
(二)分区收集(Region-based Collection)
干货分享,感谢您的阅读!
在 Java 的世界里,内存管理是每个开发者都无法忽视的关键环节。而垃圾回收(GC)作为 Java 内存管理的核心,虽然大多数时候在后台默默工作,却直接影响着应用的性能与稳定性。你是否曾在应用性能问题中迷茫,不知道是内存泄漏导致的崩溃,还是不合适的垃圾回收策略拖慢了响应速度?你是否觉得 GC 似乎总是在不经意间拖慢了应用的运行速度?
让我们一起从垃圾回收的基础开始,探索优化内存管理的无限可能!
历史主要基本文章回顾:
涉猎内容 | 具体链接 |
垃圾回收基本知识内容 | Java回收垃圾的基本过程与常用算法_java垃圾回收过程-CSDN博客 |
CMS调优和案例分析 | CMS垃圾回收器介绍与优化分析案列整理总结_cms 对老年代的回收做了哪些优化设计-CSDN博客 |
G1调优分析 | Java Hotspot G1 GC的理解总结_java g1-CSDN博客 |
ZGC基础和调优案例分析 | 垃圾回收器ZGC应用分析总结-CSDN博客 |
从ES的JVM配置起步思考JVM常见参数优化 | 从ES的JVM配置起步思考JVM常见参数优化_es jvm配置-CSDN博客 |
高频面试题汇总 | JVM高频基本面试问题整理_jvm面试题-CSDN博客 |
一、Java 垃圾回收(GC)基本概念和重要性分析
(一) Java 垃圾回收(GC)基本概念回顾
Java 垃圾回收(Garbage Collection, GC)是 Java 语言内存管理的一项重要机制,它负责自动回收不再使用的内存空间,避免了手动管理内存的复杂性和潜在的内存泄漏问题。
在 Java 中,内存主要分为两部分:堆(Heap)和栈(Stack)。栈用于存储局部变量和方法调用信息,而堆则用于存储动态分配的对象。每当创建一个对象时,它会被分配到堆中。如果这个对象不再被引用,那么它就成为了垃圾对象,需要被垃圾回收机制回收。
在了解垃圾回收(GC)之前,我们需要先掌握一些与 GC 相关的基础概念。GC 本身可以从不同的角度理解,具体语境下的含义会有所不同。接下来,我们将从几个关键点来讲解这些概念。
1.GC 三种常见语义
- Garbage Collection(垃圾回收):指的是垃圾回收的技术和过程,是整个内存管理的核心部分。简单来说,它就是“自动清理不再使用的内存”。
- Garbage Collector(垃圾回收器):这是实现垃圾回收技术的具体组件,也就是负责执行垃圾回收任务的“工具”或“程序”。它确保 JVM 会自动回收不再使用的内存空间。
- Garbage Collecting(垃圾收集):这是指垃圾回收的实际操作或行为,也就是垃圾回收器具体执行回收的过程。
2.Mutator:应用程序的内存管理角色
在垃圾回收(GC)的过程中,Mutator 是指负责创建对象的部分,通常就是我们的应用程序本身(Mutator 实际上就是应用程序中的工作线程或“应用线程”)。Mutator 可以被视作“垃圾制造者”,因为它不断地在堆内存中分配新的对象,这些对象随着时间的推移将成为“垃圾”——即不再被使用的对象。
可以把 Mutator 理解为一个生产工厂,每次它生产一个对象时,就会在堆内存中占用一定的空间。垃圾回收器则像是一个清理工人,负责定期回收那些不再被需要的对象,以保持内存空间的清洁和高效。Mutator 通过生产新对象不断向内存中添加“垃圾”,而垃圾回收器通过清理这些垃圾来维持系统的性能。
每次 Mutator 分配一个对象时,这个对象通常会被放入堆内存的“年轻代”(Young Generation)区域。这是因为大多数对象在创建后很快就变得不再需要,因此将对象放在年轻代中能够提高回收的效率。垃圾回收器会定期扫描堆内存,查找不再被使用的对象,并回收它们释放内存。
在多线程环境中,每个线程(Mutator 线程)通常会拥有一个 TLAB(线程本地分配缓冲区)。TLAB 是为每个线程分配的专用内存区域,能够减少内存分配时的锁竞争,提高分配效率。这样,每个线程就能独立地快速分配对象,而不必和其他线程争抢内存。
3.TLAB(线程本地分配缓存)
TLAB(Thread Local Allocation Buffer)是指每个线程独享的一块内存区域。为什么需要 TLAB 呢?因为它可以避免多线程环境下的内存分配竞争,提升分配效率。具体来说,当一个线程需要分配内存时,它会首先检查自己的 TLAB 中是否有可用空间。如果有,它就直接在 TLAB 中分配内存,而不需要和其他线程争抢内存资源。
这种机制能显著提高内存分配的速度,减少锁竞争,从而提高程序的性能。TLAB 是通过 CAS(Compare-and-Swap)操作来保证线程安全的。
4.Card Table(卡表)
Card Table,也叫卡表,是用于标记内存页状态的一种数据结构。在 Java 中,堆内存是被划分成多个小块的(我们可以把这些小块称为“卡页”)。Card Table 用来记录哪些卡页已经被修改过,具体来说,记录了某个卡页中是否有指向其他内存区域(例如老年代)对象的引用。
Card Table 的作用是帮助垃圾回收器更高效地处理跨代引用。什么是跨代引用呢?就是说,一个年轻代的对象可能会引用到老年代的对象。为了在垃圾回收过程中快速查找这些跨代引用,Card Table 会标记出这些卡页的状态。
当一个线程修改了某个对象的引用(例如从年轻代的对象引用了老年代的对象),它会触发一个写屏障(Write Barrier),然后 Card Table 会将相关卡页标记为“脏”。在垃圾回收时,GC 就能更高效地查找并回收不再使用的对象。
这些概念看似简单,但它们构成了垃圾回收机制的核心内容,掌握它们可以帮助我们更好地理解 JVM 是如何管理内存的,以及如何通过优化 GC 来提升程序的性能。为了避免 GC 中的停顿时间过长,我们需要在理解这些概念的基础上,选择合适的回收策略和优化方案。
(二)为什么了解 GC 对开发者尤为重要,特别是对于性能调优和内存管理
1. 应用性能影响分析
- GC 暂停时间:垃圾回收会占用 CPU 资源,并可能导致应用程序在 GC 执行期间出现“停顿”。虽然 GC 在后台运行,但如果没有优化,停顿时间可能会影响应用的响应时间和吞吐量,尤其是在高并发或实时性要求较高的系统中。因此,开发者需要理解 GC 的工作原理,以便在需要时进行调优,减少不必要的停顿。
- 吞吐量:GC 的效率直接影响到 Java 应用的吞吐量(即单位时间内能够处理的任务量)。不同的 GC 算法会影响吞吐量,因此了解如何配置 GC 和选择合适的回收器可以提升系统性能。
2. 内存管理与泄漏防止
- 堆内存管理:GC 负责管理堆内存,在堆中动态创建和销毁对象。虽然 GC 能自动回收不再使用的对象,但开发者必须了解何时以及如何创建对象,以避免过度的对象分配或无效对象的持续存在,这会导致内存占用不断增加,甚至出现内存泄漏。
- 内存泄漏:虽然 GC 可以自动回收大多数不再使用的对象,但开发者仍需确保对象引用正确地清理。如果对象的引用未能正确释放,GC 也无法识别它们为垃圾,可能会导致内存泄漏。了解 GC 的工作原理可以帮助开发者避免这种情况。
3. 优化应用的资源利用
- 堆的大小和分代设置:开发者可以通过设置 JVM 参数来调整堆的大小、分代的划分以及垃圾回收策略,以便最大化内存的利用效率。如果堆设置过小,频繁的 GC 会造成性能损失;如果堆设置过大,则可能导致长时间的 GC 停顿。通过理解 GC,开发者可以精细调控应用的内存使用。
- 垃圾回收器选择:不同的垃圾回收器(如 Serial, Parallel, CMS, G1 等)适用于不同的应用场景。选择合适的回收器可以帮助在高吞吐量、低延迟或低内存占用等方面做出权衡。开发者需要根据应用的特点选择最适合的回收器,以优化资源利用。
4. 减少性能瓶颈
- 多线程与并发:现代的垃圾回收器(如 G1、ZGC)支持多线程并发回收,这能显著减少 GC 停顿时间。开发者可以通过调整并发设置、调整回收策略来进一步减少性能瓶颈,提升系统响应速度。
- 分代收集与垃圾回收算法:Java 使用分代回收策略,年轻代对象和老年代对象有不同的回收方式。了解这些差异,可以帮助开发者避免过度频繁的年轻代回收,或优化老年代的回收策略,从而减少 GC 的负担。
5. 实时监控与优化
- GC 日志分析:通过分析 GC 日志,开发者可以识别内存使用情况,找出可能的性能瓶颈和内存问题。GC 日志能提供很多有价值的信息,帮助开发者理解应用在运行过程中如何与垃圾回收器交互,以及在什么情况下会发生长时间的 GC 停顿。
- 调整参数:GC 提供了许多可以调整的参数(如堆大小、垃圾回收器类型、垃圾回收线程数等)。开发者可以通过这些参数优化垃圾回收过程,从而实现最佳的内存管理和性能。
6. 支持大规模应用与高并发
- 对于大规模分布式系统或高并发应用,GC 的影响尤为显著。频繁的 GC 停顿会导致任务处理延迟,影响整体吞吐量。了解如何优化 GC,选择合适的回收策略和调整 JVM 参数,可以有效提升大规模应用的稳定性和响应速度。
了解 GC 能帮助开发者:识别和避免内存泄漏 + 优化垃圾回收过程,减少停顿时间 + 精确调控内存使用,提升性能 + 避免性能瓶颈,特别是在高并发场景下。因此,掌握 Java 垃圾回收的原理和调优技巧,是每个 Java 开发者提升应用性能和系统稳定性的重要一步。
二、GC 的基本原理
(一)堆(Heap)和栈(Stack)
1.重点关注 堆 和 栈
Java 程序的内存分配大致分为以下几个区域:
- 栈(Stack)
- 堆(Heap)
- 方法区(Method Area)
- 程序计数器(Program Counter) 和 本地方法栈(Native Stack)
每个区域在 Java 程序的生命周期中都有不同的角色。这里我们重点关注 堆 和 栈:
-
栈(Stack):每个线程都有自己的栈,栈是用来存储方法调用过程中的局部变量和方法调用栈帧的。栈的内存分配是 自动的,它遵循先进后出的原则,也就是方法调用时,局部变量会自动被压入栈中,方法执行完毕,栈中的局部变量会自动被销毁。栈的内存分配是由 JVM 自动管理的,不需要开发者干预。
-
堆(Heap):堆是 Java 中用来存储对象的区域。与栈不同,堆中的内存管理是由 垃圾回收器(GC) 自动进行的。堆内存是程序运行时动态分配的,主要用于存储应用程序运行时创建的对象。
2.GC核心堆简要
GC 主要工作在 Heap 区和 MetaSpace 区(上图蓝色部分),堆 是 Java 内存中最大的一块区域,主要用于存储对象和数组。它有几个关键的作用:
- 存储对象:所有通过
new
操作符创建的对象都被存放在堆中,堆中的内存分配和释放是由 JVM 和 GC 自动管理的。 - 动态内存分配:堆内存的分配并不需要提前设定大小。它是 动态分配 的,也就是说,程序运行过程中可以根据需要不断分配和释放内存。
- 垃圾回收:堆内存中的对象,如果不再被引用,垃圾回收器会定期回收这些对象,释放它们占用的内存空间。这样可以避免内存泄漏(即内存不被回收)的问题。
堆内存通常被分为几个区域,最重要的是 年轻代(Young Generation) 和 老年代(Old Generation)。
-
年轻代(Young Generation):新创建的对象通常会首先分配到年轻代。年轻代内存相对较小,因为大部分对象在短时间内就会变成垃圾。年轻代又可以细分为:
- Eden 区:新对象通常分配在这里。
- Survivor 区:Eden 区的存活对象会被移动到 Survivor 区,如果对象能“幸存”多次 GC,它会被移到老年代。
-
老年代(Old Generation):如果一个对象在年轻代经过多次 GC 后依然存活,它会被提升到老年代。老年代用于存放生命周期较长的对象。老年代的回收频率较低,但每次回收时可能会导致更长时间的停顿。
这里分代的主要目的是 优化垃圾回收的效率。大多数对象在创建后不久就不再使用,因此,先将它们分配到年轻代并频繁回收,可以减少对老年代的回收压力。这种机制提高了垃圾回收的效率,避免了频繁回收老年代对象的性能损耗。
堆的核心作用
- 堆是对象的存储区域,所有通过
new
创建的对象都被分配到堆中。 - 堆的内存管理由垃圾回收器(GC)负责,GC 会定期回收不再使用的对象,释放内存。
- 堆内存分为年轻代和老年代,年轻代回收频繁,老年代回收较少,目的是提高垃圾回收的效率。
- 合理配置堆的大小对应用的性能优化至关重要。
3. 堆的内存管理与 GC 的配合
- 内存分配:当你通过
new
创建一个对象时,它会被分配到堆中的年轻代。如果对象存活下来,它会逐渐向老年代迁移。 - 垃圾回收:每当堆内存使用到一定程度时,GC 会触发回收。GC 通过标记清除、复制、压缩等算法回收不再被引用的对象,并释放内存。垃圾回收的目的是最大化堆内存的使用效率,减少内存碎片。
堆的大小对 Java 应用的性能有着重要影响。如果堆设置得太小,可能导致频繁的垃圾回收,影响性能;而堆设置过大,虽然可以减少 GC 的频率,但会增加每次 GC 的停顿时间。因此,合理地调节堆大小对于优化性能非常关键。
你可以通过 JVM 参数(如 -Xms
和 -Xmx
)来设置堆的最小和最大大小:
-Xms
:设置初始堆大小。-Xmx
:设置最大堆大小。
通过理解堆的作用和内存管理机制,你可以更好地进行性能调优,避免内存泄漏和不必要的垃圾回收停顿,提升应用的效率和稳定性。
(二)垃圾回收的目标
垃圾回收的核心目标是:
-
避免内存泄漏:内存泄漏指的是不再使用的对象仍占用内存,垃圾回收通过自动回收无引用对象,防止内存被浪费。若程序中有不再使用的对象持有引用,GC 无法回收,因此开发者要注意管理引用。
-
提高应用性能:内存泄漏会导致频繁的垃圾回收和长时间的停顿,从而影响性能。通过及时回收内存,GC 能减少内存浪费、降低 GC 停顿时间、避免内存碎片化,提升系统的响应速度和吞吐量。
简言之,垃圾回收通过自动回收不再使用的对象,既避免内存泄漏,又提升应用的性能。
(三)GC 触发机制
1. GC 触发的原因
GC 的核心任务是回收不再使用的对象,释放内存。在 JVM 中,GC 的触发通常是由以下几个因素引起的:
- 堆内存不足:当堆内存空间不足时,JVM 会触发 GC 来回收无用对象,释放内存空间。
- 手动触发:开发者可以通过调用
System.gc()
来建议 JVM 执行垃圾回收,虽然这只是一个“建议”,JVM 不一定会立即执行。 - 内存分配失败:当 JVM 无法为新对象分配内存时,会触发 GC 来清理堆内存,确保足够的空间。
2. GC 触发的时机
GC 触发的具体时机通常和内存的使用情况密切相关,可以分为以下几种常见的情况:
a. Young Generation(年轻代) GC
- 条件:当年轻代(Eden 区)没有足够空间来分配新的对象时,JVM 会触发一个 Minor GC(即年轻代回收)。
- 触发方式:JVM 会检查年轻代内存是否已满,若满则进行垃圾回收,回收不再被引用的对象,将剩余的对象移动到 Survivor 区或老年代。
- 特点:Minor GC 是一种快速的回收过程,因为年轻代通常包含的是短生命周期的对象。
b. Old Generation(老年代) GC
- 条件:当老年代内存不足时,JVM 会触发一个 Major GC(或 Full GC),它会回收老年代的对象。
- 触发方式:如果老年代中有大量长期存活的对象,JVM 就会触发 Full GC。Full GC 会清理整个堆,回收年轻代和老年代中的无用对象。
- 特点:Full GC 通常比 Minor GC 更慢,因为它需要回收整个堆内存。
c. 内存分配失败
- 条件:当堆内存中的空闲空间不足,JVM 无法为新的对象分配内存时,GC 会触发来释放内存。
- 触发方式:JVM 会进行一次 GC,尝试回收不再使用的对象,释放空间,确保新的对象能成功分配。
- 特点:这种情况通常会导致应用的停顿时间较长,尤其是在内存比较紧张时。
3. 如何优化 GC 触发机制?
-
调整堆大小:通过设置合适的堆大小,可以减少 GC 触发的频率和停顿时间。例如,适当增大堆的初始和最大值(
-Xms
和-Xmx
)可以避免频繁的垃圾回收。 -
选择合适的垃圾回收器:不同的垃圾回收器在触发和执行的方式上有所不同。例如,G1 GC 会在后台并行执行回收,减少停顿时间,适合大内存应用;而 ParallelGC 适合高吞吐量需求的应用,GC 触发更频繁,但速度较快。
-
监控和分析 GC:定期查看 GC 日志,分析 GC 的执行频率和耗时。通过调优堆内存的分配、选择不同的回收器,开发者可以有效控制 GC 的触发和执行。
GC 触发机制决定了何时清理堆内存,回收不再使用的对象,从而保证程序的内存不会耗尽并优化性能。垃圾回收的触发通常是由于堆内存空间不足、对象生命周期结束或手动调用 System.gc()
等原因。通过合理调整堆大小、选择适当的垃圾回收器和监控 GC 日志,开发者可以优化 GC 的触发时机,提高程序性能和稳定性。
三、Java回收垃圾的基本过程与常用算法
当 Java 程序运行时,对象会被动态地分配在堆内存中。随着程序的运行,有些对象可能不再被引用,成为垃圾。垃圾回收是指在程序运行时,对这些垃圾对象进行清理,以便腾出内存空间供新的对象使用。Java 垃圾回收的基本过程可以分为以下三个步骤:
需要注意的是,不同的垃圾回收器在执行垃圾回收时,可能会采用不同的算法和策略,因此对于不同的应用场景,需要选择合适的垃圾回收器,并对其进行适当的参数调优,以达到最优的垃圾回收效果。此部分可细见:Java回收垃圾的基本过程与常用算法。
(一)垃圾分类
垃圾分类指的是将堆中的对象分为存活对象和垃圾对象两类的过程,与强引用、软引用、弱引用、虚引用等引用类型没有直接关系。
在垃圾分类阶段,JVM会从一组根对象开始,通过对象之间的引用关系,遍历所有的对象,并将所有存活的对象进行标记。在标记过程中,对象会被打上标记,以便在垃圾回收的后续阶段进行处理。被标记的对象就是存活对象,未被标记的对象则被视为垃圾对象,可以被垃圾回收器回收。
强引用、软引用、弱引用、虚引用等引用类型是用于控制垃圾回收的过程中对对象的生命周期的。它们的作用是告诉垃圾回收器哪些对象是可以被回收的,哪些对象是不可以被回收的。
(二)垃圾查找
1.查找垃圾时机
不同的垃圾回收器,策略有所不同,以下只是列举:
- 申请新对象空间、加载Class时申请空间不足
- 老年代、永久代空间使用率到达了配置值(cms:CMSInitiatingOccupancyFraction=60,CMSInitiatingPermOccupancyFraction=60)
- 调用System.gc()
2.查找垃圾操作
查找垃圾的方法可以分为两种:引用计数法和可达性分析法。
引用计数法:它是一种简单的垃圾收集算法,它的基本思想是给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1。当计数器为0时,就可以认为这个对象已经不再被引用,可以将其回收。然而,引用计数法无法解决循环引用的问题,即对象之间形成了环状结构,导致它们的计数器都不为0,即使它们已经不再被程序使用。
可达性分析法:它是现代垃圾收集算法的主要实现方式。它的基本思想是从一组被称为"根对象"(如:全局变量、栈、方法区)开始,通过一系列引用关系,能够到达的对象被认为是"存活"的,无法到达的对象则被认为是垃圾,需要被回收。在可达性分析中,对象之间形成的循环引用也会被正确处理,因为它们与根对象之间没有引用链相连。
3.GC Roots
垃圾回收,并不是找到不再使用的对象,然后将这些对象清除掉。它的过程正好相反,JVM 会找到正在使用的对象,对这些使用的对象进行标记和追溯,然后一股脑地把剩下的对象判定为垃圾,进行清理。了解了这个概念,我们就可以看下一些基本的衍生分析:
- GC 的速度,和堆内存活对象的多少有关,与堆内所有对象的数量无关;
- GC 的速度与堆的大小无关,32GB 的堆和 4GB 的堆,只要存活对象是一样的,垃圾回收速度也会差不多;
- 垃圾回收不必每次都把垃圾清理得干干净净,最重要的是不要把正在使用的对象判定为垃圾。
那么,如何找到这些存活对象,也就是哪些对象是正在被使用的,就成了问题的核心。我们把这些正在使用的引用的入口,叫作GC Roots。概括来讲,GC Roots 包括:
- Java 线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等。也就是与我们栈帧相关的各种引用;
- 所有当前被加载的 Java 类;
- Java 类的引用类型静态变量;
- 运行时常量池里的引用类型常量(String 或 Class 类型);
- JVM 内部数据结构的一些引用,比如 sun.jvm.hotspot.memory.Universe 类;
- 用于同步的监控对象,比如调用了对象的 wait() 方法;
- JNI handles,包括 global handles 和 local handles。
对于这个知识点,不要死记硬背,可以对比着 JVM 内存区域划分那张图去看,入口大约有三个:线程、静态变量和 JNI 引用。
(三)垃圾清理
1.标记-清除(Mark-Sweep)
GC分为两个阶段,标记和清除。首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。缺点是清除后会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC。
2.标记-复制(Mark-Copy)
将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。
缺点需要两倍的内存空间。一种优化方式是使用eden和survivior区,具体步骤如下:
eden和survivior区默认内存空间占比为8:1:1,同一时间只使用eden区和其中一个survivior区。标记完成后,将存活对象复制到另一个未使用的survivior区(部分年龄过大的对象将升级到年老代)。
这样,相比普通的两块空间的标记复制算法来说,只有10%的内存空间浪费,而这样做的原因是:大部分情况下,一次young gc后剩余的存活对象非常少。
3.标记-整理(Mark-Compact)
标记-整理也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。
此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题。
一般年轻代中执行GC后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。
而年老代中因为对象存活率高,用标记复制算法时数据复制效率较低,且空间浪费较大。所以需要使用标记-清除或者标记-整理算法来进行回收。
所以通常可以先使用标记清除算法,当碎片率高时,再使用标记整理算法。
(四)分代收集算法
从上面对基础垃圾收集算法,都不是银弹,有各自不同的特点,不能应对所有的场景。在现代JVM中,通过大量实际场景的分析,可以发现,JVM内存中的对象,大致可以分为两大类:一类对象,他们的生命周期很短暂,比如局部变量、临时对象等。另一类对象则会存活很久,比如用户应用程序中DB长连接中的Connection对象。
上图中,纵轴为JVM内存使用情况,横轴为时间。图中可以发现,大多数对象的生命周期极短,很少有对象可以在GC后存活下来。基于此,诞生了分代思想。在JDK7中,Hotspot虚拟机主要将内存分为三大块,新生代(Young Genaration)、老年代(Old Generation)、永久代(Permanent Generation)
1.分代区域描述
主要基本区域归类分析如下:
- 新生代:新生代主要分为两个部分:Eden区和Survivor区,其中Survivor区又可以分为两个部分,S0和S1。该区域中,相对于老年代空间较小,对象的生存周期短,GC频繁。因此在该区域通常使用标记复制算法。
- 老年代:老年代整体空间较大,对象的生命周期长,存活率高,回收不频繁。因此更适合标记整理算法。
- 永久代:永久代又称为方法区,存储着类和接口的元信息以及interned的字符串信息。在JDK8中被元空间取代。
- 元空间:JDK8以后引入,方法区也存在于元空间。
2.分代垃圾回收算法执行过程
-
初始态:对象分配在Eden区,S0、S1区几乎为空。
-
随着程序的运行,越来越多的对象被分配在Eden区。
-
当Eden放不下时,就会发生MinorGC(即YoungGC),此时,会先标识出不可达的垃圾对象,然后将可达的对象移动到S0区,并将不可达的对象清理掉。这时候,Eden区就是空的了。在这个过程中,使用了标记清理算法及标记复制算法。
-
随着Eden放不下时,会再次触发minorGC,和上一步一样,先标记。这个时候,Eden和S0区可能都有垃圾对象了,而S1区是空的。这个时候,会直接将Eden和S0区的对象直接搬到S1区,然后将Eden与S0区的垃圾对象清理掉。经历这一轮的MinorGC后,Eden与S0区为空。
-
随着程序的运行,Eden空间会被分配殆尽,这时会重复刚才MinorGC的过程,不过此时,S0区是空的,S0和S1区域会互换,此时存活的对象会从Eden和S1区,向S0区移动。然后Eden和S1区中的垃圾会被清除,这一轮完成之后,这两个区域为空。
-
在程序运行过程中,虽然大多数对象都会很快消亡,但仍然存在一些存活时间较长的对象,对于这些对象,在S0和S1区中反复移动,会造成一定的性能开销,降低GC的效率。因此引入了对象晋升的行为。
-
当对象在新生代的Eden、S0、S1区域之间,每次从一个区域移动到另一个区域时,年龄都会加一,在达到一定的阈值后,如果该对象仍然存活,该对象将会晋升到老年代。
-
如果老年代也被分配完毕后,就会出现MajorGC(即Full GC),由于老年代通常对象比较多,因此标记-整理算法的耗时较长,因此会出现STW现象,因此大多数应用都会尽量减少或着避免出现Full GC的原因。
四、常见的垃圾回收器分析
在 Java 的 HotSpot 虚拟机(JVM)中,垃圾回收器(Garbage Collector, GC)是内存管理的关键组件之一。它负责自动清理不再被使用的对象,回收内存空间,从而避免内存泄漏和提高系统性能。随着应用程序的运行,堆内存中的对象会不断创建和销毁,GC 机制通过自动管理内存,帮助开发者避免手动管理内存的复杂性和潜在错误。
为了提高垃圾回收的效率,HotSpot JVM 采用了分代收集和分区收集两大类回收策略。这两种策略根据内存区域的不同需求,采取不同的回收方式,以满足不同的性能需求和应用场景。
(一)分代收集(Generational Collection)
分代收集是 HotSpot JVM 中最基础的垃圾回收机制,它将堆内存分为多个区域,通常包括年轻代(Young Generation)和老年代(Old Generation)。年轻代主要存放生命周期较短的对象,而老年代则存放长生命周期的对象。年轻代和老年代采用不同的回收策略:
- 年轻代回收(Minor GC):通常是一个较轻量的回收过程,因为大部分对象都在创建后很快被销毁。
- 老年代回收(Major GC 或 Full GC):老年代回收通常会更复杂,因为它涉及到的对象存活时间较长,回收过程可能会暂停较长时间。
通过分代收集,JVM 能够更加高效地回收那些生命周期短的对象,而无需对老年代进行频繁回收,从而提高回收效率。
以下是常见的分代收集器(Generational Garbage Collectors)及其特性:
垃圾回收器 | 特点 | 适用场景 | 并行性 | 停顿时间 |
---|---|---|---|---|
Serial | 单线程回收器,简单高效,回收时暂停应用程序。 | 单核或内存较小的环境,适用于小型应用。 | 不支持并行 | 长暂停时间 |
ParNew | Serial 的多线程版本,支持多核处理器,年轻代回收使用多个线程。 | 多核处理器,要求一定吞吐量的应用。 | 支持并行 | 较长的暂停时间 |
Parallel Scavenge | 通过多线程并行回收年轻代,最大化吞吐量。 | 大内存应用或要求高吞吐量的场景。 | 支持并行 | 短暂停时间,适合吞吐量要求高的应用 |
Parallel Old | 并行回收老年代,配合 Parallel Scavenge 使用。 | 需要回收大内存并关注吞吐量的应用。 | 支持并行 | 停顿时间较长 |
CMS | 低延迟回收,采用并发标记清除算法,减少停顿时间。 | 对低延迟有要求的应用(例如实时系统)。 | 并发回收 | 停顿时间较短 |
Serial Old | Serial 的老年代回收器,单线程回收老年代。 | 小型应用,老年代对象较少的情况。 | 不支持并行 | 停顿时间较长 |
我们可以侧重的总结如下:
- Serial 和 Serial Old:适合资源有限的单核或小内存系统,回收时会暂停整个应用,回收速度较慢。
- ParNew 和 Parallel Scavenge:适合多核 CPU 系统,能够通过并行处理提高回收效率,适用于吞吐量要求高的应用。
- Parallel Old:用于大内存场景,能够并行回收老年代,但停顿时间可能较长。
- CMS:适用于低延迟要求的应用,尽量减少停顿时间,尤其适合实时系统。
这里重点关注CMS,其本质内容可见:提升JVM性能:CMS垃圾回收器的优化分析与案例研究。
(二)分区收集(Region-based Collection)
分区收集是相较于分代收集的一种较为先进的垃圾回收策略,它通过将堆内存划分为若干个区域(Region),而不是固定的年轻代和老年代,从而更加灵活地管理内存。分区收集器能够根据区域的内存使用情况,动态地选择哪些区域需要进行回收,并对内存进行更细粒度的管理。
分区收集策略的代表是G1(Garbage First)垃圾回收器,它将堆分为多个区域,并动态调整各个区域的回收方式,以最小化 GC 停顿时间。通过这种灵活的内存管理,G1 能够应对大内存应用和低延迟需求。
以下是常见的分区收集器(Region-based Garbage Collectors)及其特性整理成的表格:
垃圾回收器 | 特点 | 适用场景 | 并行性 | 停顿时间 |
---|---|---|---|---|
G1(Garbage First) | 将堆内存划分为多个区域,动态选择回收的区域,适用于大内存和低停顿要求的应用。 | 大内存应用、对低停顿有要求的场景。 | 支持并行 | 中等停顿时间,适合大内存应用 |
ZGC(Z Garbage Collector) | 极低延迟的垃圾回收器,设计为几乎不发生停顿,适用于高并发和低延迟要求的系统。 | 高频交易、实时系统、超低延迟要求的应用。 | 支持并行 | 几乎没有停顿时间 |
Shenandoah | 低停顿垃圾回收器,采用并发回收和增量回收技术,减少停顿时间,特别适合大内存应用。 | 对低延迟有严格要求的大内存应用。 | 支持并行 | 极短停顿时间 |
Epsilon | 无垃圾回收的垃圾回收器,不进行任何回收操作,适合不需要垃圾回收的场景,如性能基准测试等。 | 性能测试、特殊场景。 | 不支持并行 | 无停顿时间 |
我们可以侧重的总结如下:
- G1:适用于需要平衡吞吐量和低停顿的大内存应用,能够在回收时避免长时间的应用停顿。
- ZGC 和 Shenandoah:适合对延迟有严格要求的应用,能够显著减少 GC 停顿时间,适用于高频交易和实时系统。
- Epsilon:一个特殊的回收器,不执行任何垃圾回收操作,适用于不需要回收的场景,如性能测试和一些特定需求的系统。
这里重点关注G1和ZGC,其本质内容可见:
(三)收集器选择的考虑因素
选择合适的垃圾回收器取决于应用的需求和资源限制。以下是几个常见的选择依据:
- 吞吐量要求:如果你的应用需要更高的吞吐量(即减少垃圾回收时间),可以选择 Parallel Scavenge 或 Parallel Old。
- 低延迟要求:如果你的应用对响应时间有严格要求,选择 CMS、G1、ZGC 或 Shenandoah 会更适合。
- 内存大小:如果你在大内存环境下运行,G1、ZGC 或 Shenandoah 等会更好地处理内存回收。
- 资源限制:如果系统资源较为紧张,Serial 和 Serial Old 是较轻量的选择。
按自己项目的需要进行最合适的选择!!!
五、GC 性能调优
话题过大,我们在以往的各子类收集器中已经讲解较详细了,请具体见以下博客:
另外,也可以根据行业内一些重要项目做参考来 分析,具体可见:
此处,我们不在单独展开。最后,附上一份这部分的面试总结:JVM高频基本面试问题整理。
六、总结
在本文中,我们深入探讨了 Java 垃圾回收(GC)的基本概念、原理、垃圾回收器的类型与选择,以及如何通过优化 GC 来提高应用程序的性能和稳定性。
-
GC 基本概念:垃圾回收是 Java 内存管理的重要机制,通过自动回收不再使用的内存,避免了内存泄漏问题。理解 GC 的基本概念,如垃圾回收、垃圾回收器、Mutator、TLAB 和 Card Table,帮助开发者掌握内存管理的核心原理。
-
GC 对开发者的重要性:GC 对应用程序的性能有着显著影响。开发者需要理解 GC 的工作原理,特别是如何优化 GC 的停顿时间、吞吐量、内存使用和资源管理,避免内存泄漏和性能瓶颈。
-
GC 触发机制与优化:了解 GC 触发的原因和时机,帮助开发者精确调控内存的使用和回收。通过合理设置堆内存、选择合适的垃圾回收器、分析 GC 日志等方法,可以显著提升程序的性能和响应速度。
-
垃圾回收算法与分代收集:通过分代收集和分区收集两大策略,Java 在垃圾回收过程中将堆内存划分为不同区域,以提高回收效率。开发者需要了解常见的回收器及其特点,选择最适合的垃圾回收策略,平衡吞吐量和延迟要求。
-
常见垃圾回收器分析:我们介绍了多种垃圾回收器,包括 Serial、ParNew、Parallel、CMS、G1、ZGC 和 Shenandoah 等。每个回收器在不同的应用场景下有不同的优势,开发者应根据项目需求和资源限制做出选择。
-
GC 性能调优与最佳实践:通过调节堆大小、调整 JVM 参数、选择合适的回收器,以及实时监控 GC 的执行情况,开发者可以进一步优化 Java 应用的性能。具体的优化方法和案例,开发者可以参考行业内的实践和相关的技术分析。
掌握 Java 垃圾回收机制对于每个开发者来说都至关重要。通过理解 GC 的工作原理、优化垃圾回收过程和合理选择垃圾回收器,开发者可以更好地管理应用的内存,提高性能,避免内存泄漏和资源浪费。只有深入理解 GC,才能在面对高并发、大内存和低延迟要求的系统时,做出恰当的性能调优和优化决策,确保 Java 应用程序的高效稳定运行。