Java虚拟机(JVM)的垃圾回收机制(Garbage Collection, GC)是自动管理内存的重要部分。其主要任务是回收不再使用的对象,从而释放内存以供程序使用。
在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:
- 新生代内存(Young Generation)
- 老生代(Old Generation)
- 永久代(Permanent Generation)
JDK8版本,堆内存:
- 新生代内存(Young Generation)
- 老生代(Old Generation)
- 元空间(MetaSpace)
永久代(PermGen)被替换为元空间(MetaSpace)原因:
内存管理问题:
- PermGen: 永久代使用的是JVM堆内存的一部分,因此其大小是有限的,且在运行时不可动态调整。如果PermGen空间不足,会导致java.lang.OutOfMemoryError: PermGen space错误。
- MetaSpace: 元空间使用的是本地内存(Native Memory),不再受限于堆内存的大小限制。可以根据系统的可用内存动态调整其大小,从而减少内存不足的风险。
内存碎片问题:
- PermGen: 由于PermGen空间是固定大小的,随着应用程序的运行,可能会出现内存碎片化的问题,导致内存使用效率降低。
- MetaSpace: 元空间利用本地内存,不容易出现内存碎片化的问题,且垃圾回收机制更加灵活。
动态调整:
- PermGen: 永久代的大小需要在JVM启动时指定,且调整后不易改变。如果需要更多的PermGen空间,通常需要重新启动JVM。
- MetaSpace: 元空间的大小可以在运行时动态调整,提供了更好的灵活性来适应不同的应用需求。
性能优化:
- MetaSpace: 元空间的设计目标之一是减少垃圾回收的停顿时间,提高性能。元空间的垃圾回收机制可以更好地管理类的元数据,减少对应用程序性能的影响。
总结 - 将PermGen替换为MetaSpace使得JVM的内存管理更加灵活和高效,解决了PermGen的一些固有问题,如内存大小限制、碎片化、动态调整困难等。
垃圾回收的基本概念
垃圾: 指不再被程序引用的对象或内存块。垃圾回收的目的是清除这些对象,释放内存空间。
垃圾回收器: 负责自动检测和回收垃圾的机制。JVM中有多种不同类型的垃圾回收器,根据不同的回收策略和性能需求进行选择。
垃圾回收的主要算法
标记-清除算法 (Mark-Sweep):
- 标记阶段: 遍历所有活动的对象并标记它们。
- 清除阶段: 清除所有未标记的对象,释放它们占用的内存。
- 特点: 简单,但可能导致内存碎片化。
标记-整理算法 (Mark-Compact):
- 标记阶段: 与标记-清除算法相同。
- 整理阶段: 将所有活跃对象移动到内存的一端,整理出连续的内存空间,清除空闲区域。
- 特点: 避免了碎片化问题,但可能需要更多的时间来移动对象。
复制算法 (Copying):
- 分代复制: 将内存分为两部分,通过复制活跃对象到另一部分的内存来进行回收。
- 特点: 高效但需要额外的内存空间,主要用于新生代的垃圾回收。
分代收集算法 (Generational Collection):
- 定义: 根据对象的生命周期将堆内存分为不同的区域(年轻代和老年代)。
- 特点: 年轻代中的对象通常生命周期较短,因此频繁进行垃圾回收;老年代中的对象生命周期较长,因此较少进行垃圾回收。
- 实现: 在年轻代中使用复制算法,在老年代中使用标记-整理算法。
垃圾回收的主要区域
年轻代 (Young Generation):
- 组成: 包括Eden区和两个Survivor区(S0和S1)。
- 特点: 新生对象首先分配在Eden区,经过多次垃圾回收后仍然存活的对象会被移动到Survivor区,然后可能会晋升到老年代。
老年代 (Old Generation):
- 定义: 存储长期存在的对象。老年代的垃圾回收频率低,但回收过程可能会更复杂。
- 特点: 包括Tenured Generation和PermGen(在Java 8之前)或Metaspace(Java 8及以后)。
永久代 (Permanent Generation)(Java 7及之前):
- 定义: 用于存储类的元数据、静态变量和常量池。
- 特点: 在Java 8之后被Metaspace取代。
元空间 (Metaspace)(Java 8及以后):
- 定义: 用于替代永久代,存储类的元数据等信息。
- 特点: 使用本地内存,而不是堆内存。
内存分配和回收原则
对象优先在 Eden 区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
大对象直接进入老年代的行为是由虚拟机动态决定的,它与具体使用的垃圾回收器和相关参数有关。大对象直接进入老年代是一种优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本。
- G1 垃圾回收器会根据 -XX:G1HeapRegionSize 参数设置的堆区域大小和 -XX:G1MixedGCLiveThresholdPercent 参数设置的阈值,来决定哪些对象会直接进入老年代。
- Parallel Scavenge 垃圾回收器中,默认情况下,并没有一个固定的阈值(XX:ThresholdTolerance是动态调整的)来决定何时直接在老年代分配大对象。而是由虚拟机根据当前的堆内存情况和历史数据动态决定。
长期存活的对象将进入老年代
大部分情况,对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
空间分配担保
定义:是指在对象分配时,JVM 确保在当前的堆内存区域有足够的空间来容纳新的对象。如果没有足够的空间,这可能会导致垃圾回收(GC)操作的触发,甚至可能导致内存分配失败。
如何工作
年轻代与老年代:
在 Java 堆中,内存通常被分为年轻代(Young Generation)和老年代(Old Generation)。年轻代又分为 Eden 区和两个 Survivor 区。
空间分配担保主要应用于年轻代,因为对象在年轻代创建时,JVM 需要确保有足够的空间来容纳新创建的对象。
空间分配担保的实现
- 预分配策略: 在年轻代中,JVM 会根据当前的内存使用情况和预计的对象分配情况来预分配一定的空间。如果发现空间不足,JVM 可能会在对象分配之前触发一次 Minor GC,以清理年轻代并回收内存。
- 空间预留: 为了减少频繁的垃圾回收,JVM 可能会在年轻代中预留一定的空闲空间。这样,在对象分配时可以避免直接触发垃圾回收。
垃圾回收触发:
如果年轻代中没有足够的空间来分配新的对象,JVM 会触发 Minor GC。Minor GC 会尝试清理年轻代,回收不再使用的对象,从而释放出足够的空间。
如果 Minor GC 不能释放足够的空间,JVM 可能会将部分对象晋升到老年代。老年代的垃圾回收相对较少,但如果老年代空间不足,可能会触发 Full GC(也称为 Major GC),这会对性能产生更大的影响。
垃圾回收器的类型
串行垃圾回收器 (Serial GC):
- 特点: 单线程回收,适用于单核处理器或较小的应用程序,必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
- 参数: -XX:+UseSerialGC
- 新生代采用标记-复制算法,老年代采用标记-整理算法。
并行垃圾回收器 (Parallel GC):
- 特点: 多线程回收,适用于多核处理器,能提高垃圾回收的效率。
- 参数: -XX:+UseParallelGC
并发标记-清除垃圾回收器 (Concurrent Mark-Sweep GC, CMS):
- 特点: 旨在减少垃圾回收时的停顿时间,适用于需要低停顿的应用程序。
- 参数: -XX:+UseConcMarkSweepGC
G1垃圾回收器 (Garbage-First GC):
- 特点: 设计用于大内存应用,能够分区回收,平衡延迟和吞吐量。
- 参数: -XX:+UseG1GC
- 特点:
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
G1 收集器的运作大致分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
ZGC (Z Garbage Collector) 和 Shenandoah GC:
- 特点: 低延迟垃圾回收器,旨在减少停顿时间,适用于要求极低延迟的应用。
- 参数: -XX:+UseZGC 或 -XX:+UseShenandoahGC
ZGC 可以将暂停时间控制在几毫秒以内,且暂停时间不受堆内存大小的影响,出现 Stop The World 的情况会更少,但代价是牺牲了一些吞吐量。ZGC 最大支持 16TB 的堆内存。ZGC 在 Java11 中引入,处于试验阶段。经过多个版本的迭代,不断的完善和修复问题,ZGC 在 Java15 已经可以正式使用了。
JDK 默认垃圾收集器
(使用 java -XX:+PrintCommandLineFlags -version 命令查看):
- JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
- JDK 9 ~ JDK20: G1
GC种类
针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
- 部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
- 整堆收集 (Full GC):收集整个 Java 堆和方法区。
死亡对象判断方法
引用计数法
问题:难以解决循环引用问题
可达性分析算法
- 定义:通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
引用类型总结
问题
- 哪些对象可以作为 GC Roots 呢?
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
- JNI(Java Native Interface)引用的对象
- 对象可以被回收,就代表一定会被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
Object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 Java 语言的负担,影响了 Java 语言的安全和 GC 的性能。JDK9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!
总结
JVM的垃圾回收机制通过多种算法和策略来管理内存,自动清除不再使用的对象。不同的垃圾回收器适用于不同的应用场景,可以根据需求和性能要求选择适当的垃圾回收策略。