2. JVM内存模型与垃圾回收基础

在支付订单系统中,每一笔交易都对应着对象的创建、使用与销毁。理解JVM运行时数据区的布局、对象的生命周期以及垃圾回收(GC)算法的原理,是进行全链路性能调优的第一步。本章将深入剖析这些基础概念,为后续的JVM参数调优和问题诊断打下坚实的理论基础。

2.1 JVM运行时数据区

JVM在运行Java程序时,会将其管理的内存划分为若干个不同的数据区域。这些区域各有用途,创建和销毁的时间也不同。了解这些区域是理解内存分配与回收的关键。

区域名称 线程共享 主要用途 异常类型
程序计数器 否(线程私有) 记录当前线程执行的字节码行号指示器 无(唯一不会OOM的区域)
Java虚拟机栈 否(线程私有) 存储局部变量表、操作数栈、动态链接、方法出口等 StackOverflowError / OutOfMemoryError
本地方法栈 否(线程私有) 为JVM使用到的Native方法服务 StackOverflowError / OutOfMemoryError
堆(Heap) 是(所有线程共享) 存放对象实例,是垃圾回收的主要区域 OutOfMemoryError
方法区(元空间) 是(所有线程共享) 存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等 OutOfMemoryError
运行时常量池 是(方法区的一部分) 存放编译期生成的各种字面量和符号引用 OutOfMemoryError
调优提示: 在支付系统中,堆(Heap) 是调优的核心战场。订单对象、用户会话、缓存数据都分配在堆上。堆的大小和GC策略直接决定了系统的吞吐量和响应时间。元空间(Metaspace)取代了永久代(PermGen),默认使用本地内存,减少了因永久代空间不足导致的OOM问题。

2.2 对象生命周期

一个Java对象从创建到被回收,会经历以下几个阶段。理解这个生命周期有助于我们分析内存泄漏和优化对象分配。

  1. 创建阶段(Creation): 通过 new 关键字、反射、克隆或反序列化等方式在堆中分配内存。
  2. 使用阶段(In Use): 对象被栈中的引用变量所指向,程序通过该引用访问对象的属性和方法。
  3. 不可见阶段(Invisible): 对象仍然存在,但已经没有任何强引用指向它,程序无法再访问。
  4. 可回收阶段(Reachable): 垃圾回收器通过可达性分析算法判断对象不再被任何GC Roots引用,标记为可回收。
  5. 回收阶段(Collected): 垃圾回收器执行finalize()方法(如果重写且未被调用过),然后回收内存。
核心概念:GC Roots
在可达性分析中,作为起始点的对象集合。包括:
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。

2.3 堆内存结构与分代模型

为了更高效地进行垃圾回收,JVM将堆内存划分为不同的代(Generation)。这种分代模型基于“弱分代假说”(绝大多数对象朝生夕死)。

JVM堆内存分代模型 年轻代 (Young Generation) Eden 区 新对象分配 Minor GC 主要区域 S0 From S1 To 老年代 (Old Generation) 长期存活对象 Major GC / Full GC 元空间 (Metaspace) - 本地内存 晋升 默认比例:Eden : Survivor = 8 : 1 : 1(可通过 -XX:SurvivorRatio 调整)
避坑指南: 支付系统中,如果大量订单对象在创建后很快被回收,应关注Eden区的大小。Eden区过小会导致频繁的Minor GC,增加系统停顿。如果对象晋升到老年代的速度过快,可能导致老年代空间不足,触发Full GC,严重影响交易吞吐量。

2.4 垃圾回收算法原理

垃圾回收算法是GC的核心。不同的算法适用于不同的场景,现代JVM通常采用分代收集算法,即在不同代使用不同的算法。

2.4.1 标记-清除算法 (Mark-Sweep)

最基础的算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象。

  • 优点: 实现简单,不需要移动对象。
  • 缺点: 执行效率不稳定(标记和清除效率随对象数量增长而下降);产生大量不连续的内存碎片,导致后续大对象分配失败而提前触发GC。

2.4.2 标记-复制算法 (Mark-Copy)

将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。

  • 优点: 实现简单,运行高效;没有内存碎片。
  • 缺点: 可用内存缩小为原来的一半,空间浪费较大。
  • 应用: 现代JVM的年轻代采用此算法(Eden和Survivor区的配合)。

2.4.3 标记-整理算法 (Mark-Compact)

标记过程与“标记-清除”一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

  • 优点: 不会产生内存碎片;内存利用率高。
  • 缺点: 移动对象需要STW(Stop-The-World),暂停所有用户线程,影响系统响应时间。
  • 应用: 老年代通常采用此算法(或与标记-清除混合使用)。
三种基本GC算法对比 标记-清除 (Mark-Sweep) 标记前:存活对象 + 垃圾对象 标记后:清除所有垃圾对象 结果:产生内存碎片 标记-复制 (Mark-Copy) 将存活对象复制到另一块区域 清空当前区域 结果:无碎片,但空间减半 标记-整理 (Mark-Compact) 将存活对象向一端移动 清理边界外的内存 结果:无碎片,但需移动对象 算法对比总结 特性 标记-清除 标记-复制 标记-整理 内存碎片 严重 无 无 空间利用率 高 低(50%) 高 执行效率 中等 高 中等 STW时间 中等 短 长 适用场景 老年代(配合CMS) 年轻代 老年代
分代收集思想: 现代JVM(如G1、ZGC)并非单纯使用某一种算法,而是结合了多种算法的优点。
  • 年轻代(Young Gen): 使用标记-复制算法。因为大部分对象生命周期短,复制成本低,且能避免碎片化。
  • 老年代(Old Gen): 使用标记-整理或标记-清除算法。因为对象存活率高,复制成本高,需要减少移动或通过并发来降低STW时间。

2.5 对象分配与晋升策略

了解对象在堆中的分配路径,对于诊断内存问题至关重要。以下是对象分配的一般流程:

  1. 栈上分配: 如果对象没有发生逃逸(Escape Analysis),JVM会尝试在栈上分配内存,随栈帧弹出而自动销毁,减少GC压力。
  2. TLAB分配: 如果栈上分配失败,JVM会尝试在Eden区的线程本地分配缓冲区(Thread Local Allocation Buffer, TLAB)中分配,避免线程竞争。
  3. Eden区分配: 如果TLAB空间不足,则在Eden区公共区域分配。
  4. Minor GC与晋升: 当Eden区空间不足时,触发Minor GC。存活的对象被复制到Survivor区(S0或S1)。每经历一次Minor GC,对象年龄+1。
  5. 晋升到老年代: 当对象年龄达到阈值(默认15,可通过 -XX:MaxTenuringThreshold 设置),或Survivor区中相同年龄对象大小总和超过Survivor空间的一半时,对象晋升到老年代。
  6. 大对象直接进入老年代: 对于超过 -XX:PretenureSizeThreshold 设置的大对象,会直接在老年代分配,避免在Eden区和Survivor区之间发生大量复制。
支付系统实战案例: 在一次订单查询接口的性能压测中,我们发现Full GC频繁。通过分析GC日志,发现大量订单查询结果对象(大小约2MB)被直接分配到了老年代。原因是 -XX:PretenureSizeThreshold 设置过小(默认0,即不启用)。调整该参数后,大对象在Eden区分配,通过Minor GC回收,Full GC频率显著降低,接口TP99从500ms下降到80ms。