一、JVM 内存模型概述
在了解 GC 流程之前,先简单回顾一下 JVM 的内存模型。JVM 将内存划分为几个不同的区域,主要包括:
- 堆(Heap):用于存放对象实例。堆分为两个主要区域:新生代(Young Generation)和老年代(Old Generation)。
- 新生代(Young Generation):新创建的对象首先分配到新生代,其中又分为三个部分:Eden 区、From Survivor 区和 To Survivor 区。
- 老年代(Old Generation):存储那些生命周期较长、不再频繁变化的对象。
- 方法区(Method Area):用于存放类结构、常量、静态变量等元数据,JDK 8 及以后用元空间(Metaspace)取代了永久代(PermGen)。
- 栈(Stack):每个线程都有自己的栈,用于存放局部变量、操作数栈等。
二、GC 类型及触发条件
JVM 中的 GC 主要分为以下几种类型:
- Minor GC:主要发生在新生代。当新生代的 Eden 区被填满时触发,将存活的对象移到 Survivor 区。通常执行频繁,但速度较快。
- Major GC 或 Full GC:主要发生在老年代。当老年代被填满或某些情况下新生代不足以容纳新对象时触发。Full GC 回收整个堆(包括新生代和老年代),执行速度较慢,但回收效率高。
三、完整的 GC 流程
一个完整的 GC 流程可以分为以下几个步骤:
1. 新生代垃圾回收(Minor GC)
1.1 新生代分配和对象创建
- 当应用程序创建对象时,JVM 首先尝试将对象分配到新生代的 Eden 区。
- 如果 Eden 区空间不足以容纳新对象,JVM 会触发一次 Minor GC。
1.2 Minor GC 触发和执行
- 标记存活对象:Minor GC 会首先遍历新生代中的所有对象,标记仍然存活的对象(即在根可达性分析中依然可达的对象)。
- 清除非存活对象:标记完成后,清除新生代中所有不可达(垃圾)对象的内存空间。
- 对象复制和晋升:将存活的对象从 Eden 区复制到 Survivor 区(通常是 From Survivor 区),并根据年龄(即对象经历过的 Minor GC 次数)决定是否将对象晋升到老年代。
1.3 Survivor 区的交换
- 新生代中有两个 Survivor 区,分别称为 From Survivor 和 To Survivor。每次 Minor GC 后,存活对象会从 Eden 区和 From Survivor 区复制到 To Survivor 区。
- 在下一次 Minor GC 时,From Survivor 和 To Survivor 区的角色互换,确保新生代中始终有一个空闲的 Survivor 区。
2. 对象的晋升(Promotion)
2.1 晋升条件
- 对象年龄:每个对象在 Survivor 区中每经历一次 Minor GC,年龄(Age)会增加 1。当对象的年龄达到一个阈值(通常是 15)时,JVM 会将该对象晋升到老年代。
- Survivor 区不足:如果 Survivor 区的空间不足以容纳一次 GC 之后所有存活的对象,这些对象也会直接晋升到老年代。
- 大对象:大对象(例如大数组或大量字符串)可能直接进入老年代,以避免在新生代复制时导致频繁的 GC。
2.2 晋升过程
- 晋升过程就是将符合条件的对象从新生代(Survivor 区)移动到老年代,以确保新生代有足够的空间供新的对象分配。
3. 老年代垃圾回收(Major GC 或 Full GC)
3.1 触发条件
- 老年代满了:当老年代的空间被填满或接近满时,会触发 Major GC 或 Full GC。
- 新生代无法容纳新对象:有时,新生代无法容纳新对象且老年代也接近满,会触发 Full GC。
- 手动触发:开发者可以通过调用
System.gc()
手动请求 JVM 执行 Full GC(但不保证立即执行)。
3.2 Major GC 的执行
- 标记存活对象:JVM 首先标记老年代中存活的对象,类似于 Minor GC 的标记过程。
- 清除和压缩:Major GC 通常会清除不可达的对象,并可能执行内存压缩(Compact),即将存活对象移动到连续的内存块中,释放出大片的可用空间,避免内存碎片问题。
3.3 Major GC 的代价
- Major GC 或 Full GC 的代价较高,因为它回收整个堆内存,通常需要停止所有应用线程(STW,Stop-The-World),这会导致应用程序暂停。为了减少 Major GC 的频率和影响,通常需要优化老年代的使用情况,避免频繁触发 Full GC。
四、GC 算法
JVM 中主要使用以下几种垃圾回收算法:
-
标记-清除算法(Mark-Sweep):最基本的 GC 算法。标记阶段遍历对象图,标记所有存活的对象,清除阶段回收所有未标记的对象。其缺点是会产生内存碎片。
-
标记-整理算法(Mark-Compact):在标记存活对象后,移动存活对象使它们在内存中连续排列,并清除后面的内存。适用于老年代,减少碎片化问题。
-
复制算法(Copying):主要用于新生代,将存活的对象从一块内存复制到另一块内存,清除原内存中的所有对象。该算法效率高,但浪费内存(需要两倍的空间)。
-
分代收集算法(Generational GC):结合了上述算法,将堆内存划分为新生代和老年代,根据对象的生命周期采用不同的 GC 算法。新生代使用复制算法,老年代使用标记-清除或标记-整理算法。
五、GC 日志分析与优化
通过分析 GC 日志,开发者可以评估 GC 的效率并发现性能瓶颈。例如,可以查看 Minor GC 和 Major GC 的频率、每次 GC 的耗时、堆的使用情况等。通过调整 JVM 参数(如堆大小、分区比例、GC 算法选择),可以优化应用程序的内存管理,减少 GC 对性能的影响。
一些常用的 JVM 参数包括:
-Xms
和-Xmx
:设置堆的初始和最大大小。-XX:NewRatio
:设置新生代和老年代的比例。-XX:SurvivorRatio
:设置 Eden 区和 Survivor 区的比例。-XX:MaxTenuringThreshold
:设置对象在 Survivor 区的最大年龄,超过此年龄对象晋升到老年代。
六、总结
JVM 的 GC 是一个复杂但至关重要的机制,通过分代收集算法有效地管理内存。新生代的 Minor GC 主要负责处理生命周期较短的对象,而老年代的 Major GC 则处理生命周期较长的对象。理解对象的晋升过程、GC 的执行步骤及其对应用性能的影响,是优化 Java 应用程序性能的重要方面。通过合理配置 JVM 参数和分析 GC 日志,可以减少 GC 对应用程序的性能影响,提升系统的稳定性和响应速度。