前置知识: Java、Java

JVM 内存模型

14 min高级

JVM 内存模型深度解析:运行时数据区、JMM 形式化、分代 GC 算法、对象内存布局、现代收集器(G1/ZGC/Shenandoah)、内存调优与生产案例。

前置知识

学习目标

  • 掌握「0. 本节阅读指引(先读这一节)」的核心机制、典型用法与常见陷阱
  • 掌握「1. 历史动机与发展脉络」的核心机制、典型用法与常见陷阱
  • 掌握「2. 形式化定义与规范基础」的核心机制、典型用法与常见陷阱
  • 掌握「3. 运行时数据区」的核心机制、典型用法与常见陷阱
  • 掌握「4. 对象内存布局」的核心机制、典型用法与常见陷阱

0. 本节阅读指引(先读这一节)

本篇是「JVM 内存模型」进阶文档。

第一遍只读:3. 运行时数据区、4. 对象内存布局、6. 堆分代与对象晋升、7. 垃圾回收算法;5. Java 内存模型(JMM)第一遍只看结论(volatile、happens-before)。

可跳过:1-2 节(历史、形式化)与 8-12 节第二遍细读。

前置:047 多线程基础、053 JVM 类加载机制。

1. 历史动机与发展脉络

1.1 JVM 内存模型演进时间线

timeline
    title 发展时间线
    1991: Green Project(Oak 语言)启动 James Gosling 团队设计家电嵌入式语言 起初采用'标记-压缩'GC,无分代
    1995: Java 1.0 GA 分代收集(Serial + Serial Old) 永久代(PermGen)实现方法区 Object Header 64 位布局确立
    1997: Java 1.1:JIT 编译器引入
    1999: J2SE 1.2:HotSpot JVM 加入 HotSpot 准确式 GC,告别保守式 GC 分代收集正式确立
    2002: J2SE 1.4:Parallel GC(Parallel Scavenge + Parallel Old) 吞吐量优先,适合批处理
    2004: Java 5:CMS(Concurrent Mark Sweep)收集器 低延迟老年代收集 JMM 形式化(JSR 133,Manson/Pugh/Adve)
    2006: Java 6:压缩指针(Compressed Oops) 32 位指针寻址 64 位堆(< 32GB) 偏向锁(Biased Locking)
    2011: Java 7:G1(Garbage-First)收集器预览 Region 化堆布局 String 对象从 PermGen 移到 Heap
    2014: Java 8:移除 PermGen,引入 Metaspace 方法区使用本地内存 -XX:MaxMetaspaceSize 控制
    2017: Java 9:G1 成为默认收集器 CMS 标记为 deprecated
    2018: Java 11 LTS:ZGC 实验性(JEP 333) 着色指针,< 10ms 停顿 Epsilon GC(无操作收集器,用于性能测试)
    2019: Java 12:G1 可中断混合收集(JEP 344) Shenandoah 加入 OpenJDK(Red Hat 主导)
    2020: Java 15:ZGC 生产可用(JEP 377) Shenandoah 生产可用(JEP 379) 亚毫秒停顿进入生产
    2021: Java 17 LTS:偏向锁默认禁用(JEP 374) ZGC/Shenandoah 生态固化
    2023: Java 21 LTS:ZGC 分代模式(JEP 439) 分代 ZGC:年轻代/老年代分离
    2024-2025: Java 23/24 分代 ZGC 成为默认(JEP 474)并移除非分代模式(JEP 490) Java 25 Generational Shenandoah GraalVM Native Image AOT 主流化 CRaC(Coordinated Restore at Checkpoint)

1.2 三大设计哲学

  1. Write Once, Run Anywhere:JVM 屏蔽硬件内存差异,JMM 提供统一的可见性/有序性保证。
  2. Automatic Memory Management:GC 取代手动 malloc/free,提升开发效率但引入 STW 风险。
  3. Latency vs Throughput Trade-off:从 Serial→Parallel→CMS→G1→ZGC,持续在延迟与吞吐间权衡。

1.3 JMM 的诞生背景

1990 年代 Java 多线程程序在多核 x86 与弱内存模型 Alpha 处理器上行为不一致,引发大量”双重检查锁定失效”等 bug。2004 年 JSR 133 由 Manson、Pugh、Adve 重新形式化 JMM,发表于 POPL 2005。JMM 是首个被严格形式化的工业语言内存模型,影响后续 C++11、Rust、Go 的内存模型设计。


2. 形式化定义与规范基础

2.1 JVM 规范对运行时数据区的定义

JVM Specification §2.5 将运行时数据区定义为五个区域:

RuntimeDataArea=Heap∪MethodArea∪VMStack∪NativeStack∪PCRegister\text{RuntimeDataArea} = \text{Heap} \cup \text{MethodArea} \cup \text{VMStack} \cup \text{NativeStack} \cup \text{PCRegister}

其中 Heap 与 Method Area 是线程共享的,VM Stack、Native Stack、PC Register 是线程私有的。

2.2 JMM 的形式化模型

设 VV 为所有共享变量集合,TT 为线程集合,AA 为程序执行的所有内存操作序列。每个操作 a∈Aa \in A 形式化为五元组:

a=(thread(a),var(a),kind(a)∈{read,write,lock,unlock},value(a),order(a))a = (\text{thread}(a), \text{var}(a), \text{kind}(a) \in \{\text{read}, \text{write}, \text{lock}, \text{unlock}\}, \text{value}(a), \text{order}(a))

JMM 通过 happens-before 偏序关系 →hb\xrightarrow{hb} 定义合法执行:

Legal(A)  ⟺  ∀read r∈A:value(r)=value(wr) where wr→hbr∧∄w′:wr→hbw′→hbr\text{Legal}(A) \iff \forall \text{read } r \in A: \text{value}(r) = \text{value}(w_r) \text{ where } w_r \xrightarrow{hb} r \wedge \nexists w': w_r \xrightarrow{hb} w' \xrightarrow{hb} r

2.3 happens-before 八条规则

JLS §17.4.5 定义的 happens-before 规则:

  1. 程序次序规则:同一线程中,按代码顺序 aa 先于 bb,则 a→hbba \xrightarrow{hb} b
  2. 监视器锁规则:unlock 操作 →hb\xrightarrow{hb} 同一锁的后续 lock
  3. volatile 规则:volatile 写 →hb\xrightarrow{hb} 同一变量的后续读
  4. 线程启动规则:Thread.start() →hb\xrightarrow{hb} 线程内任意操作
  5. 线程终止规则:线程内任意操作 →hb\xrightarrow{hb} Thread.join() 返回
  6. 中断规则:Thread.interrupt() →hb\xrightarrow{hb} 被中断线程检测到中断
  7. 对象终结规则:构造函数结束 →hb\xrightarrow{hb} finalizer 开始
  8. 传递性:a→hbb∧b→hbc⇒a→hbca \xrightarrow{hb} b \wedge b \xrightarrow{hb} c \Rightarrow a \xrightarrow{hb} c

2.4 内存屏障的形式化

JMM 定义四种内存屏障:

屏障类型形式化语义作用
LoadLoadL1;LoadLoad;L2L_1; \text{LoadLoad}; L_2 ⟹ L1L_1 先于 L2L_2阻止读重排
StoreStoreS1;StoreStore;S2S_1; \text{StoreStore}; S_2 ⟹ S1S_1 先于 S2S_2 且刷新阻止写重排
LoadStoreL1;LoadStore;S2L_1; \text{LoadStore}; S_2 ⟹ L1L_1 先于 S2S_2阻止读后写重排
StoreLoadS1;StoreLoad;L2S_1; \text{StoreLoad}; L_2 ⟹ 全局排序最强屏障,开销最大

HotSpot 对 volatile 写插入 StoreStore + StoreLoad,对 volatile 读插入 LoadLoad + LoadStore。在 x86 强内存模型下,仅 volatile 写需要 lock addl 作为 StoreLoad 屏障;ARM 弱内存模型需要全部四种屏障。

2.5 GC Roots 的形式化定义

可达性分析的起点(GC Roots)形式化为:

GCRoots=StackRefs∪StaticRefs∪ConstantRefs∪JNIRefs∪SynchronizedRefs\text{GCRoots} = \text{StackRefs} \cup \text{StaticRefs} \cup \text{ConstantRefs} \cup \text{JNIRefs} \cup \text{SynchronizedRefs}
  • StackRefs\text{StackRefs}:所有线程栈帧中的本地变量表引用
  • StaticRefs\text{StaticRefs}:方法区中类的 static 字段
  • ConstantRefs\text{ConstantRefs}:方法区中的常量池引用
  • JNIRefs\text{JNIRefs}:本地方法栈中的 JNI global reference
  • SynchronizedRefs\text{SynchronizedRefs}:所有被 synchronized 持有的对象

2.6 对象存活判定

对象 oo 存活当且仅当:

∃r∈GCRoots:r→∗o\exists r \in \text{GCRoots}: r \xrightarrow{*} o

其中 →∗\xrightarrow{*} 表示引用链的传递闭包。


3. 运行时数据区

3.1 五大区域总览

区域线程共享存储内容异常OutOfMemory?
堆 (Heap)是对象实例、数组OutOfMemoryError: Java heap space是
方法区 (Method Area)是类元数据、常量池、static 字段OutOfMemoryError: Metaspace是
虚拟机栈 (VM Stack)否栈帧、局部变量、操作数栈StackOverflowError / OOM是(可动态扩展)
本地方法栈 (Native Stack)否Native 方法调用StackOverflowError / OOM是
程序计数器 (PC Register)否当前字节码行号无否

3.2 堆(Heap)

堆是 JVM 中最大的一块内存区域,所有对象实例和数组都在堆上分配(除逃逸分析优化的栈上分配外)。堆是 GC 管理的主要区域。

flowchart TD
    subgraph Heap[Heap]
        subgraph Young[Young Generation 1/3 heap]
            Eden[Eden 80% young]
            S0[Survivor 0 10% young]
            S1[Survivor 1 10% young]
        end
        Old[Old Gen 2/3 heap]
    end
    Eden --> S0
    S0 --> S1
    S1 --> Old

关键参数:

flowchart LR
    Eden[Eden<br/>80% of young] --- S0[Survivor 0<br/>10% young] --- S1[Survivor 1<br/>10% young]

3.3 方法区与元空间

方法区在 JDK 8 之前由永久代(PermGen)实现,位于 JVM 堆中,由 -XX:PermSize 与 -XX:MaxPermSize 控制。JDK 8+ 改为元空间(Metaspace),使用本地内存(native memory),通过 -XX:MetaspaceSize 与 -XX:MaxMetaspaceSize 控制。

// 演示 Metaspace OOM
// JVM 参数:-XX:MaxMetaspaceSize=32m
public class MetaspaceOOM {
    public static void main(String[] args) {
        int count = 0;
        try {
            while (true) {
                // 使用 CGLib 或 Javassist 不断生成新类
                // 这里用 ClassLoader 模拟
                ClassLoader loader = new ClassLoader() {
                    @Override
                    protected Class<?> findClass(String name) {
                        byte[] code = generateClassBytes(name);
                        return defineClass(name, code, 0, code.length);
                    }
                };
                loader.loadClass("Generated" + count++);
            }
        } catch (Throwable e) {
            System.out.println("Created " + count + " classes before OOM");
            e.printStackTrace();
        }
    }

    private static byte[] generateClassBytes(String name) {
        // 简化:实际应使用 ASM/ByteBuddy 生成字节码
        return new byte[0];
    }
}

3.4 虚拟机栈(VM Stack)

虚拟机栈描述 Java 方法执行的内存模型:每个方法调用创建一个栈帧(Stack Frame)。

栈帧结构:

flowchart TD
    Frame[栈帧 Stack Frame]
    Frame --> LV[局部变量表<br/>this(非静态方法)<br/>方法参数<br/>方法内局部变量<br/>long/double 占 2 槽]
    Frame --> OS[操作数栈<br/>字节码指令的工作区<br/>iadd/imul/invoke 用的栈]
    Frame --> DL[动态链接<br/>指向运行时常量池的方法引用]
    Frame --> RA[方法返回地址<br/>正常返回:调用者的 PC<br/>异常返回:异常表查找]
// 演示 StackOverflowError
public class StackOverflowDemo {
    private static int depth = 0;

    public static void recursive() {
        depth++;
        recursive();
    }

    public static void main(String[] args) {
        try {
            recursive();
        } catch (StackOverflowError e) {
            System.out.println("Stack depth: " + depth);
            // 默认栈大小(-Xss512k)下约 11000 层
            // -Xss1m 下约 23000 层
        }
    }
}

3.5 程序计数器(PC Register)

程序计数器是当前线程执行字节码的行号指示器。它是唯一不会 OOM 的区域,每个线程独立。在 native 方法执行时,PC 值为 undefined。

// 用 javap -v 查看 PC 与字节码行号对应
public class PCExample {
    public int add(int a, int b) {
        int c = a + b;
        return c;
    }
}
// 字节码:
//  0: iload_1
//  1: iload_2
//  2: iadd
//  3: istore_3
//  4: iload_3
//  5: ireturn

3.6 直接内存(Direct Memory)

JDK 1.4 NIO 引入基于 Channel 与 Buffer 的 IO,支持 native 内存分配,绕过堆,避免数据在 JVM 堆与 native 内存间拷贝。

import java.nio.ByteBuffer;

public class DirectMemoryDemo {
    public static void main(String[] args) {
        // 堆内 Buffer:分配在 JVM 堆,受 GC 管理
        ByteBuffer heapBuffer = ByteBuffer.allocate(1024);

        // 直接 Buffer:分配在 native 内存,不受 -Xmx 限制
        // 受 -XX:MaxDirectMemorySize 控制
        ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024 * 1024);

        // 直接内存的释放由 Cleaner(PhantomReference + ReferenceQueue)触发
        // 不保证及时释放,可能导致 OOM: Direct buffer memory
    }
}
# 直接内存相关参数
-XX:MaxDirectMemorySize=256m      # 最大直接内存
-Djdk.nio.maxCachedBufferSize=0   # 禁用 Buffer 缓存(避免线程本地缓存泄漏)

4. 对象内存布局

4.1 对象的三部分结构

HotSpot 对象在内存中由三部分组成:

flowchart TD
    Obj[对象 Object]
    Obj --> OH[对象头 Object Header]
    OH --> MW[Mark Word 64 bits<br/>hash、age、锁状态、GC 标记]
    OH --> CP[Class Pointer 32/64 bits<br/>开启压缩为 32 位]
    Obj --> ID[实例数据<br/>父类字段在前,子类字段在后<br/>相同宽度字段分配在一起<br/>字段对齐(8 字节边界)]
    Obj --> PD[对齐填充<br/>对象起始地址 8 字节对齐]

4.2 Mark Word 64 位布局

Mark Word 在不同锁状态下的位布局(64 位 JVM):

flowchart TD
    MW[Mark Word(64 bits)]
    MW --> U[无锁:hash(25) / age(4) / 0 / 01]
    MW --> B[偏向锁:thread(54) / epoch(2) / 1 / 01]
    MW --> L[轻量锁:ptr_to_lock_record(62) / 00]
    MW --> H[重量锁:ptr_to_heavy_monitor(62) / 10]
    MW --> G[GC 标记:- / 11]

字段说明:

  • hash:对象 hashCode(延迟计算,调用 System.identityHashCode 后填充)
  • age:对象年龄(经历 Minor GC 次数,默认晋升阈值为 15)
  • thread:偏向线程 ID
  • epoch:偏向时间戳(用于批量撤销)
  • ptr_to_lock_record:指向线程栈中 Lock Record 的指针
  • ptr_to_heavy_monitor:指向 ObjectMonitor 的指针

4.3 压缩指针(Compressed Oops)

默认开启(堆小于 32GB 时),将 64 位指针压缩为 32 位:

# 默认开启,堆 < 32GB 时自动启用
-XX:+UseCompressedOops

# 关闭(堆 ≥ 32GB 时强制关闭)
-XX:-UseCompressedOops

原理:JVM 将 8 字节对齐的对象地址除以 8,得到 32 位可寻址的偏移量。32 位指针可寻址 232×8=32GB2^{32} \times 8 = 32\text{GB}。

// 演示对象大小(需 JOL 工具)
// 添加依赖:org.openjdk.jol:jol-core:0.17
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;

public class ObjectSizeDemo {
    public static void main(String[] args) {
        // 空对象:16 字节(开启压缩指针)
        // - 12 字节头(8 Mark + 4 Class Pointer)
        // - 4 字节对齐填充
        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        // 含两个 long 字段的对象:24 字节
        // - 12 字节头
        // - 16 字节实例数据(两个 long)
        // - 不需要填充
        class TwoLongs {
            long a, b;
        }
        System.out.println(ClassLayout.parseInstance(new TwoLongs()).toPrintable());

        // 数组:16 + 元素大小
        int[] ints = new int[10];
        System.out.println(ClassLayout.parseInstance(ints).toPrintable());
        // 16 字节头(12 对象头 + 4 数组长度)+ 40 字节(10×4)= 56 字节
    }
}

4.4 字段对齐与填充

JVM 默认 8 字节对齐,可通过 -XX:ObjectAlignmentInBytes=16 调整为 16 字节对齐(堆 ≥ 64GB 时使用)。

// 字段重排序优化
class FieldOrdering {
    // 声明顺序
    boolean b;   // 1 字节
    long l;      // 8 字节
    int i;       // 4 字节
    short s;     // 2 字节
    char c;      // 2 字节

    // JVM 实际布局(父类在前,相同宽度相邻)
    // [header 12] [long 8] [int 4] [char 2] [short 2] [boolean 1] [padding 3]
    // 总计:32 字节
}

5. Java 内存模型(JMM)

5.1 JMM 的设计目标

JMM 屏蔽各种硬件内存访问差异,让 Java 程序在各平台下达到一致的内存访问效果。其核心是定义共享变量的可见性、有序性、原子性规则。

5.2 主内存与工作内存

JMM 抽象出主内存(Main Memory)与工作内存(Working Memory):

  • 主内存:所有共享变量的”权威”存储,对应物理主内存
  • 工作内存:每个线程私有的变量副本,对应 CPU 缓存与寄存器

线程对变量的操作规则:

  1. 不能直接读写主内存
  2. 必须先将变量从主内存读到工作内存
  3. 修改后写回主内存
  4. 不同线程间无法访问彼此的工作内存

5.3 三大特性

5.3.1 原子性(Atomicity)

JMM 保证以下操作原子:

  • 基本数据类型(除 long/double 外)的 read、load、store、write
  • lock、unlock 操作

更大范围的原子性需通过 synchronized 或 java.util.concurrent.atomic 保证:

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

public class AtomicityDemo {
    private int count = 0;                    // 非原子,多线程下不安全
    private final AtomicInteger atomicCount = new AtomicInteger(0);
    private final LongAdder adder = new LongAdder();  // 高并发下性能更好

    public void increment() {
        count++;                              // 非原子:读+1+写三步
        atomicCount.incrementAndGet();        // 基于 CAS,原子
        adder.increment();                    // 分段累加,最后求和
    }
}

5.3.2 可见性(Visibility)

当一个线程修改共享变量,其他线程能立即得知。

  • volatile:强制刷新主内存,使其他线程的工作内存缓存失效
  • synchronized:unlock 前将变量刷回主内存
  • final:构造函数结束后,final 字段对所有线程可见
public class VisibilityDemo {
    // 不加 volatile:可能死循环(JIT 优化为读寄存器)
    private boolean stop = false;

    // 加 volatile:保证可见性
    // private volatile boolean stop = false;

    public void stop() {
        stop = true;
    }

    public void run() {
        while (!stop) {
            // 不做任何事,可能被 JIT 优化为 while(true)
        }
        System.out.println("Stopped");
    }

    public static void main(String[] args) throws InterruptedException {
        VisibilityDemo demo = new VisibilityDemo();
        new Thread(demo::run).start();
        Thread.sleep(100);
        demo.stop();
        Thread.sleep(1000);
    }
}

5.3.3 有序性(Ordering)

程序执行顺序的保证。JMM 允许编译器、处理器进行指令重排序,但通过 happens-before 规则保证结果一致性。

public class OrderingDemo {
    private int x = 0, y = 0;
    private volatile boolean ready = false;

    public void writer() {
        x = 1;          // 普通写
        y = 2;          // 普通写
        ready = true;   // volatile 写,前面的写不能重排到其后
    }

    public void reader() {
        if (ready) {    // volatile 读,后面的读不能重排到其前
            // 此处保证看到 x=1, y=2
            System.out.println("x=" + x + ", y=" + y);
        }
    }
}

5.4 volatile 的实现原理

volatile 写在 HotSpot 中插入 StoreStore + StoreLoad 屏障:

[前置普通写]
StoreStore 屏障  ← 防止前面的普通写重排到 volatile 写之后
[volatile 写]
StoreLoad 屏障   ← 防止 volatile 写与后续的读重排,开销最大

volatile 读插入 LoadLoad + LoadStore 屏障:

[volatile 读]
LoadLoad 屏障   ← 防止后续读重排到 volatile 读之前
LoadStore 屏障  ← 防止后续写重排到 volatile 读之前
[后续普通读/写]

x86 强内存模型下,仅 StoreLoad 屏障需要 lock addl $0, 0(%rsp) 指令;ARM 弱内存模型需要全部四种屏障。

5.5 final 字段的语义

JMM 对 final 字段有特殊保证:构造函数结束前,final 字段的写入对所有线程可见(即使没有 volatile/synchronized)。

public final class ImmutablePoint {
    private final int x;
    private final int y;

    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
        // 构造函数结束后,其他线程看到的 x、y 一定已初始化
    }

    public int getX() { return x; }
    public int getY() { return y; }
}

// 安全发布:通过 volatile 或 final 保证可见性
public class SafePublication {
    private ImmutablePoint point;  // 不安全:可能看到部分构造的对象

    // 方式 1:volatile
    private volatile ImmutablePoint safePoint1;

    // 方式 2:final(构造时安全)
    public SafePublication(ImmutablePoint p) {
        this.point = p;  // 不安全
    }
}

5.6 双重检查锁定的正确实现

public class Singleton {
    // 必须用 volatile,否则可能看到部分构造的 instance
    // (分配内存 + 赋值引用 在 初始化字段 之前完成)
    private static volatile Singleton instance;

    private final String config;

    private Singleton() {
        this.config = loadConfig();
    }

    public static Singleton getInstance() {
        if (instance == null) {                  // 第一次检查,避免锁开销
            synchronized (Singleton.class) {
                if (instance == null) {          // 第二次检查,避免重复创建
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    private String loadConfig() { return "default"; }
}

6. 堆分代与对象晋升

6.1 分代假说

分代收集基于两个假说:

  1. 弱分代假说:绝大多数对象朝生夕死(90%+ 在新生代被回收)
  2. 强分代假说:熬过越多次 GC 的对象越难回收

6.2 新生代(Young Generation)

新生代占堆的 1/3(-XX:NewRatio=2),分为三个区:

flowchart LR
    Eden[Eden<br/>80% of young] --- S0[Survivor 0<br/>10% young] --- S1[Survivor 1<br/>10% young]

对象分配流程:

  1. 新对象优先在 Eden 区分配(TLAB 加速)
  2. Eden 满,触发 Minor GC
  3. 存活对象复制到 Survivor 区(标记-复制算法)
  4. Survivor 区交替使用(From/To)
  5. 经历 -XX:MaxTenuringThreshold=15 次 GC 后晋升老年代

6.3 TLAB(Thread Local Allocation Buffer)

为避免多线程分配时的 CAS 竞争,JVM 给每个线程分配一块私有的 Eden 区域(TLAB),线程内分配直接 bump pointer。

# TLAB 相关参数
-XX:+UseTLAB                 # 默认开启
-XX:TLABSize=64k             # TLAB 大小
-XX:MinTLABSize=1k
-XX:TLABWasteTargetPercent=1 # TLAB 占 Eden 的比例
-XX:+ResizeTLAB              # 动态调整 TLAB 大小
-XX:+PrintTLAB               # 打印 TLAB 统计

6.4 大对象直接进入老年代

大对象(如长数组)需要连续内存,避免在 Eden/Survivor 间复制开销:

-XX:PretenureSizeThreshold=1m   # 大于此阈值的对象直接在老年代分配
                                 # 仅对 Serial/ParNew 生效

6.5 对象晋升条件

对象晋升老年代的几种情况:

  1. 年龄达阈值:-XX:MaxTenuringThreshold=15(默认 15,CMS 为 6)
  2. 动态年龄判断:Survivor 中相同年龄所有对象大小总和 > Survivor 空间的 50%,年龄 ≥ 该年龄的对象晋升
  3. Survivor 空间不足:Minor GC 时 Survivor 放不下的存活对象通过担保机制进入老年代
-XX:MaxTenuringThreshold=15       # 最大晋升年龄
-XX:InitialTenuringThreshold=7    # 初始晋升年龄
-XX:+PrintTenuringDistribution    # 打印年龄分布
-XX:+UseAdaptiveSizePolicy        # 自适应调整(可能忽略 MaxTenuringThreshold)

6.6 老年代(Old Generation)

老年代占堆的 2/3,存放:

  • 长期存活的对象
  • 大对象
  • 晋升的对象

老年代满时触发 Major GC / Full GC,回收速度比 Minor GC 慢 10 倍以上。

6.7 元空间(Metaspace)

JDK 8+ 用元空间替代永久代:

维度PermGen(≤ JDK 7)Metaspace(≥ JDK 8)
位置JVM 堆内本地内存(native memory)
默认大小64m(64 位)无上限(受物理内存限制)
控制-XX:PermSize / -XX:MaxPermSize-XX:MetaspaceSize / -XX:MaxMetaspaceSize
GC 触发永久代满Metaspace 达阈值或 Full GC 时
调优易 OOM不易 OOM,但可能被 native OOM killer 杀死
-XX:MetaspaceSize=256m        # 触发 Full GC 的阈值
-XX:MaxMetaspaceSize=512m     # 最大元空间
-XX:MinMetaspaceFreeRatio=40  # GC 后最小空闲比例
-XX:MaxMetaspaceFreeRatio=70  # GC 后最大空闲比例

7. 垃圾回收算法

7.1 引用计数法(Reference Counting)

给对象添加引用计数器,引用加 1,失效减 1。无法解决循环引用问题,JVM 不使用。

# Python 使用引用计数 + 标记清除(处理循环引用)
a = []
b = [a]
a.append(b)
del a, b  # 两个对象的引用计数均为 1,但已不可达

7.2 可达性分析(Reachability Analysis)

从 GC Roots 出发,沿引用链搜索,不可达的对象为可回收对象。

GC Roots 包括:

  • 虚拟机栈中引用的对象(本地变量表)
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI 引用的对象
  • synchronized 持有的对象
  • JMXBean、JVMTI 等 JVM 内部引用

7.3 三种基础算法

7.3.1 标记-清除(Mark-Sweep)

标记阶段:从 GC Roots 遍历,标记所有存活对象
清除阶段:清除未标记的对象

优点:实现简单
缺点:内存碎片多,分配大对象时可能触发 Full GC
flowchart TD
    A[标记前:占用块 + 空闲碎片] --> B[清除后:碎片残留]

7.3.2 标记-复制(Mark-Copy)

将堆分为两半,每次只使用一半
GC 时将存活对象复制到另一半
然后清空当前半区

优点:无碎片,分配快
缺点:可用内存减半
flowchart TD
    A[使用区 + 空闲区] --> B[复制后:紧凑排列]

7.3.3 标记-整理(Mark-Compact)

标记阶段:标记存活对象
整理阶段:将存活对象向一端移动,清理边界外内存

优点:无碎片
缺点:移动对象开销大,需更新所有引用
flowchart TD
    A[标记前:占用块 + 空闲区] --> B[整理后:无碎片]

7.4 分代收集策略

区域算法理由
新生代标记-复制朝生夕死,复制开销小
老年代标记-清除 / 标记-整理存活率高,复制开销大

7.5 并发与并行的形式化定义

  • 并行(Parallel):多个 GC 线程同时工作,但应用线程暂停(STW)
  • 并发(Concurrent):GC 线程与应用线程同时运行(部分阶段 STW)
  • STW(Stop-The-World):暂停所有应用线程
收集器并行并发STW 阶段
Serial否否全程 STW
Parallel Scavenge是否全程 STW
CMS部分是初始标记、重新标记 STW
G1部分是初始标记、最终标记 STW
ZGC是是仅部分根扫描 STW(< 1ms)
Shenandoah是是仅初始标记 STW(< 10ms)

7.6 安全点与安全区域

GC 必须在安全点(Safepoint)或安全区域(Safe Region)执行,确保所有线程处于已知状态。

-XX:+PrintSafepointStatistics       # 打印安全点统计
-XX:PrintSafepointStatisticsCount=1
-XX:+SafepointTimeout               # 安全点超时报警
-XX:SafepointTimeoutDelay=1000      # 超时阈值(ms)

安全点通常设置在:

  • 方法返回
  • 循环回边( counted loop 除外)
  • 异常抛出
// 这个循环可能长时间不进入安全点
public void longLoop() {
    long start = System.nanoTime();
    // counted loop(int 计数)会被 JIT 优化,不进入安全点
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
        // 长时间运行,可能阻塞其他线程的 GC
    }
    // 改用 long 计数避免优化
    // for (long i = 0; i < Long.MAX_VALUE; i++) { }
}

8. 现代垃圾收集器

8.1 收集器总览

收集器作用域算法STW适用场景
Serial新生代标记-复制全程客户端、单核
Serial Old老年代标记-整理全程客户端、CMS fallback
ParNew新生代标记-复制(并行)全程配合 CMS
Parallel Scavenge新生代标记-复制(并行)全程吞吐量优先
Parallel Old老年代标记-整理(并行)全程吞吐量优先
CMS老年代标记-清除(并发)部分低延迟(已弃用)
G1全堆分区 + 标记-整理部分默认(Java 9+)
ZGC全堆着色指针 + 读屏障< 1ms超低延迟
Shenandoah全堆Brooks Pointer + 写屏障< 10ms低延迟

8.2 G1(Garbage-First)

G1 是 Java 9+ 的默认收集器,将堆划分为多个 Region(1—32MB),每个 Region 可动态切换为 Eden/Survivor/Old/Humongous。

flowchart TD
    G1[G1 Heap Layout<br/>Region(1-32MB)]
    G1 --> R1[E / S / O / E / H / H / O / O]
    G1 --> R2[O / E / O / S / O / E / O / -]
    G1 --> R3[- / O / E / O / O / - / S / E]
    Legend[E: Eden S: Survivor O: Old H: Humongous -: Free]

G1 工作流程:

  1. 初始标记(Initial Mark):STW,标记 GC Roots 直接引用的对象(搭便车一次 Minor GC)
  2. 并发标记(Concurrent Mark):从 GC Roots 遍历整个堆,标记存活对象
  3. 最终标记(Remark):STW,处理 SATB 缓冲区,修正并发标记期间的变更
  4. 筛选回收(Live Data Counting and Evacuation):STW,按回收价值排序,选择垃圾最多的 Region 回收
# G1 关键参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200        # 目标最大停顿时间(软目标)
-XX:G1HeapRegionSize=16m        # Region 大小(1-32MB,自动选择)
-XX:InitiatingHeapOccupancyPercent=45  # 触发并发标记的堆占用阈值
-XX:G1NewSizePercent=5          # 新生代最小比例
-XX:G1MaxNewSizePercent=60      # 新生代最大比例
-XX:G1MixedGCCountTarget=8      # 混合回收次数目标
-XX:G1MixedGCLiveThresholdPercent=85  # Region 存活率高于此值不回收
-XX:ConcGCThreads=4             # 并发标记线程数
-XX:ParallelGCThreads=8         # STW 阶段并行线程数

SATB(Snapshot-At-The-Beginning):并发标记期间,写屏障记录被覆盖的引用,保证标记结果基于快照。

// G1 SATB 写屏障(伪代码)
void satbWriteBarrier(Object* field, Object* newValue) {
    Object* oldValue = *field;
    if (oldValue != null) {
        satbQueue.enqueue(oldValue);  // 加入 SATB 队列
    }
    *field = newValue;
}

8.3 ZGC(Z Garbage Collector)

ZGC 是 Java 11 引入的低延迟收集器,目标:< 10ms 停顿(Java 21 分代模式 < 1ms),支持 TB 级堆。

核心机制:

  1. 着色指针(Colored Pointers):64 位指针的高 4 位用作 GC 状态标记
  2. 读屏障(Load Barrier):每次读对象引用时检查指针颜色,按需转移对象
  3. 并发转移(Concurrent Relocation):对象转移与应用线程并发执行
flowchart LR
    Color[4 bits Color] --- Addr[42 bits Address] --- Unused[18 bits Unused]

颜色位(4 bits):

  • Marked0 (M0):标记阶段 0
  • Marked1 (M1):标记阶段 1
  • Remapped:转移完成
  • Finalizable:finalizer 可达
# ZGC 参数(Java 21+)
-XX:+UseZGC
-XX:+ZGenerational           # 启用分代 ZGC(Java 21+,默认)
-XX:SoftMaxHeapSize=4g       # 软上限,ZGC 试图保持在此之下
-XX:ZAllocationSpikeTolerance=2  # 分配尖峰容忍度
-XX:ConcGCThreads=4          # 并发线程数
-XX:ParallelGCThreads=8      # STW 线程数
-XX:+ZProactive              # 主动 GC(即使堆未满也回收,避免紧急 GC)

ZGC 分代模式(Java 21+):

flowchart LR
    subgraph Heap[ZGC Generational Heap]
        Young[Young Generation<br/>Small, frequent]
        Old[Old Generation<br/>Large, infrequent GC]
    end
    Young --> YE[Eden]
    Young --> YS[Survivor]
    Old --> OR[Old Region]

8.4 Shenandoah

Red Hat 主导的低延迟收集器,与 ZGC 类似但实现不同。

核心机制:

  • Brooks Pointer(旧版):每个对象额外一个指针指向自身或转发地址
  • Load Reference Barrier(新版 2.0+):类似 ZGC 的读屏障
# Shenandoah 参数
-XX:+UseShenandoahGC
-XX:ShenandoahGCHeuristics=adaptive  # 自适应(默认)
-XX:ShenandoahGarbageThreshold=20    # Region 垃圾占比阈值
-XX:ShenandoahAllocationThreshold=20 # Region 触发 GC 的阈值

8.5 CMS(已弃用)

CMS(Concurrent Mark Sweep)是 Java 5 引入的低延迟收集器,Java 9 标记弃用,Java 14 移除。

四个阶段:

  1. 初始标记(Initial Mark):STW,标记 GC Roots 直接引用
  2. 并发标记(Concurrent Mark):并发遍历对象图
  3. 重新标记(Remark):STW,修正并发标记期间变更
  4. 并发清除(Concurrent Sweep):并发清除垃圾

缺点:

  • 标记-清除产生碎片
  • Concurrent Mode Failure 退化为 Serial Old(长时间 STW)
  • 浮动垃圾
  • 对 CPU 敏感

8.6 收集器选择决策树

flowchart TD
    T0["堆大小?"]
    T1["< 100MB → Serial"]
    T2["100MB - 4GB → Parallel(吞吐)或 G1(平衡)"]
    T3["4GB - 32GB → G1(默认)或 ZGC(低延迟)"]
    T4["> 32GB → ZGC 或 Shenandoah"]
    T5["延迟要求?"]
    T6["不在意(批处理) → Parallel"]
    T7["P99 < 200ms → G1"]
    T8["P99 < 10ms → ZGC / Shenandoah"]
    T9["P99 < 1ms → ZGC 分代模式"]
    T10["堆大小 > 16TB → ZGC(唯一支持)"]
    T0 --> T1
    T0 --> T2
    T0 --> T3
    T0 --> T4
    T4 --> T5
    T5 --> T6
    T5 --> T7
    T5 --> T8
    T5 --> T9
    T9 --> T10

8.7 收集器对比基准测试

// 使用 JMH 基准测试对比收集器吞吐
// 需要 org.openjdk.jmh:jmh-core
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Thread)
public class GCBenchmark {

    @Param({"g1", "zgc", "shenandoah"})
    private String gc;

    private List<byte[]> cache = new ArrayList<>();

    @Setup
    public void setup() {
        // 预热缓存
        for (int i = 0; i < 1000; i++) {
            cache.add(new byte[1024]);
        }
    }

    @Benchmark
    public void allocateAndRelease() {
        // 分配 100KB,模拟短期对象
        byte[] data = new byte[100 * 1024];
        // data 离开作用域后可回收
    }

    public static void main(String[] args) throws RunnerException {
        for (String gc : new String[]{"g1", "zgc", "shenandoah"}) {
            Options opt = new OptionsBuilder()
                .include(GCBenchmark.class.getSimpleName())
                .param("gc", gc)
                .jvmArgs("-XX:+Use" + (gc.equals("g1") ? "G1GC" :
                          gc.equals("zgc") ? "ZGC" : "ShenandoahGC"),
                         "-Xmx4g", "-Xms4g")
                .forks(1)
                .build();
            new Runner(opt).run();
        }
    }
}

9. 对比分析

9.1 JVM 内存模型 vs C++ 内存模型

维度Java(JMM)C++11
形式化时间2004(JSR 133)2011
内存序volatile(强)、final、happens-beforememory_order_relaxed/acquire/release/seq_cst
默认顺序程序次序 + 数据依赖memory_order_seq_cst
GC强制无(需手动或 RAII)
安全性高(运行时检查)中(编译期检查)

9.2 JVM 内存模型 vs Go 内存模型

维度JavaGo
模型happens-beforehappens-before
同步原语synchronized、volatile、finalsync.Mutex、channel、atomic
Channel 语义无(BlockingQueue 模拟)内建,send happens-before receive
GC分代(G1/ZGC)并发三色标记(无分代)
停顿G1 100ms、ZGC < 1ms通常 < 1ms

9.3 JVM 内存模型 vs Rust 内存模型

维度JavaRust
内存安全运行时(GC)编译期(所有权 + 借用)
并发安全synchronized/atomicSend/Sync trait
数据竞争可能(运行时检测难)编译期拒绝
内存回收GC所有权 + Drop
性能中等接近 C

9.4 垃圾收集器对比

收集器停顿时间吞吐量内存开销堆大小上限Java 版本
Parallel100-1000ms高低32GB1.6+
CMS50-200ms中中(碎片)32GB5-13(弃用)
G150-200ms中高中(5-20%)64GB7+
ZGC(旧)< 10ms低中高(10-15%)16TB11+
ZGC(分代)< 1ms中中(< 10%)16TB21+
Shenandoah< 10ms中中(Brooks)64GB12+

9.5 Metaspace vs PermGen

维度PermGenMetaspace
内存位置JVM 堆内本地内存
默认大小64MB无上限
OOM 风险高(类多即 OOM)低(但被 OS kill)
调试jmap -permstatjcmd VM.metaspace
释放Full GCFull GC 或主动
监控jstat -gcpermjstat -gcmetacapacity

10. 常见陷阱与最佳实践

10.1 陷阱 1:堆内存与容器内存混淆

错误:Docker 容器限制 2GB,JVM -Xmx2g,但 native 内存(Metaspace、线程栈、直接内存)导致 OOM killed。

# 错误配置
docker run -m 2g java -Xmx2g -jar app.jar

# 正确配置(保留 native 内存空间)
docker run -m 2g java \
  -XX:MaxRAMPercentage=70 \
  -XX:MaxMetaspaceSize=256m \
  -XX:MaxDirectMemorySize=256m \
  -Xss512k \
  -jar app.jar

10.2 陷阱 2:ParallelGCThreads 过多

# 错误:在 32 核机器上设置过多 GC 线程
-XX:ParallelGCThreads=32

# GC 线程过多会导致:
# 1. GC 线程间同步开销大
# 2. 抢占应用线程 CPU
# 3. 频繁上下文切换

# 正确:通常为 CPU 核数的 5/8
-XX:ParallelGCThreads=20  # 32 核机器

10.3 陷阱 3:忽略了 -XX:+DisableExplicitGC

// 业务代码调用 System.gc(),触发 Full GC
System.gc();  // 性能杀手

// 解决:禁用显式 GC
// -XX:+DisableExplicitGC
// 或 -XX:+ExplicitGCInvokesConcurrent(G1 下转为并发 GC)

10.4 陷阱 4:直接内存泄漏

// 错误:未释放 DirectByteBuffer
public void leak() {
    while (true) {
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
        // buffer 离开作用域后由 Cleaner 异步释放
        // 但若引用被长期持有,导致 OOM: Direct buffer memory
    }
}

// 正确:手动释放或使用 try-with-resources
public void safe() {
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
    try {
        // 使用 buffer
    } finally {
        // Java 9+ 提供了 Cleaner API
        // 或显式调用 sun.misc.Cleaner(不推荐)
    }
}

10.5 陷阱 5:ThreadLocal 内存泄漏

// 错误:ThreadLocal 在线程池中不清理
private static final ThreadLocal<byte[]> CACHE = ThreadLocal.withInitial(() -> new byte[1024 * 1024]);

public void process() {
    byte[] data = CACHE.get();
    // 使用后未 remove
}

// 线程池中的线程长期持有 ThreadLocal,导致内存泄漏
// 解决:使用后必 remove
public void process() {
    try {
        byte[] data = CACHE.get();
        // 使用 data
    } finally {
        CACHE.remove();  // 必须清理
    }
}

10.6 陷阱 6:finalize 导致 OOM

// 错误:finalize 慢导致对象堆积
class SlowFinalizer {
    @Override
    protected void finalize() throws Throwable {
        Thread.sleep(1000);  // 慢
    }
}

// 大量 SlowFinalizer 对象创建时,finalize 队列堆积,老年代 OOM
// Java 9+ 标记 finalize 为 deprecated,Java 18+ 标记 forRemoval

// 替代方案:Cleaner(Java 9+)
class Resource implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();
    private final Cleaner.Cleanable cleanable;

    public Resource() {
        cleanable = cleaner.register(this, () -> {
            // 清理逻辑
        });
    }

    @Override
    public void close() {
        cleanable.clean();  // 显式清理
    }
}

10.7 陷阱 7:String.intern() 滥用

// 错误:大量 intern 导致 PermGen/Metaspace OOM
for (int i = 0; i < 10000000; i++) {
    String s = ("prefix" + i).intern();
}

// intern 在 JDK 6+ 移到堆,但仍可能 OOM
// Java 7+ intern 在堆,但仍有开销
// 建议:避免 intern,使用 ConcurrentHashMap 自行缓存

10.8 陷阱 8:堆外内存未受监控

# 监控 native 内存(Java 23+ NMT 增强)
-XX:NativeMemoryTracking=summary
jcmd <pid> VM.native_memory summary

# 监控直接内存
jcmd <pid> VM.native_memory baseline
jcmd <pid> VM.native_memory detail.diff

10.9 陷阱 9:忽略 GC 日志导致性能问题无法诊断

# Java 9+ 统一日志
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100m

# 关键日志标签
-Xlog:gc*                       # GC 全部
-Xlog:gc+heap=debug             # 堆变化
-Xlog:gc+age=trace              # 对象年龄
-Xlog:safepoint                 # 安全点
-Xlog:gc+ergo*=debug            # 自适应决策

10.10 陷阱 10:CMS 退化为 Serial Old

# CMS 触发 Concurrent Mode Failure 时退化为 Serial Old
# 现象:偶发 10s+ 的 STW
# 原因:老年代增长过快,CMS 并发跟不上

# 解决:
# 1. 降低 -XX:CMSInitiatingOccupancyFraction=70(提前触发)
# 2. 增大 -XX:CMSTriggerRatio=80
# 3. 升级到 G1 或 ZGC

11. 工程实践

11.1 生产级 JVM 参数模板

# 通用 4GB 堆 + G1 收集器
JAVA_OPTS="
  # 堆配置
  -Xms4g -Xmx4g
  -XX:MaxRAMPercentage=70.0

  # 元空间
  -XX:MetaspaceSize=256m
  -XX:MaxMetaspaceSize=512m

  # 直接内存
  -XX:MaxDirectMemorySize=512m

  # 线程栈
  -Xss512k

  # G1 收集器
  -XX:+UseG1GC
  -XX:MaxGCPauseMillis=200
  -XX:G1HeapRegionSize=16m
  -XX:InitiatingHeapOccupancyPercent=45
  -XX:G1ReservePercent=20

  # JIT
  -XX:+TieredCompilation
  -XX:CICompilerCount=4

  # OOM 处理
  -XX:+HeapDumpOnOutOfMemoryError
  -XX:HeapDumpPath=/var/log/app/heapdump
  -XX:+ExitOnOutOfMemoryError
  -XX:+CrashOnOutOfMemoryError

  # GC 日志(Java 9+ 统一日志)
  -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100m

  # NMT(Native Memory Tracking)
  -XX:NativeMemoryTracking=summary

  # 异常处理
  -XX:+DisableExplicitGC
  -XX:+UseContainerSupport
  -XX:+UseStringDeduplication

  # 故障诊断
  -XX:+UnlockDiagnosticVMOptions
  -XX:+PrintCommandLineFlags
"

11.2 Docker 容器 JVM 配置

FROM eclipse-temurin:21-jre-alpine

# 设置容器资源限制
ENV JAVA_OPTS="
  -XX:MaxRAMPercentage=70.0
  -XX:InitialRAMPercentage=50.0
  -XX:MinRAMPercentage=25.0
  -XX:MaxMetaspaceSize=256m
  -XX:MaxDirectMemorySize=256m
  -XX:+UseZGC
  -XX:+ZGenerational
  -XX:+HeapDumpOnOutOfMemoryError
  -XX:HeapDumpPath=/tmp/heapdump
  -Xlog:gc*:file=/tmp/gc.log:time,uptime,level,tags:filecount=5,filesize=50m
  -XX:NativeMemoryTracking=summary
  -XX:+UseContainerSupport
  -XX:+ExitOnOutOfMemoryError
"

COPY target/app.jar /app/app.jar
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]

11.3 Kubernetes Deployment JVM 配置

apiVersion: apps/v1
kind: Deployment
metadata:
  name: java-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: registry.example.com/java-app:1.0.0
        resources:
          requests:
            memory: "2Gi"
            cpu: "1"
          limits:
            memory: "4Gi"
            cpu: "2"
        env:
        - name: JAVA_OPTS
          value: >
            -XX:MaxRAMPercentage=70.0
            -XX:+UseZGC
            -XX:+ZGenerational
            -XX:SoftMaxHeapSize=2g
            -XX:MaxMetaspaceSize=256m
            -XX:MaxDirectMemorySize=256m
            -XX:+HeapDumpOnOutOfMemoryError
            -XX:HeapDumpPath=/dump
            -Xlog:gc*:file=/log/gc.log:time,uptime,level,tags:filecount=10,filesize=100m
            -XX:NativeMemoryTracking=summary
            -XX:+ExitOnOutOfMemoryError
        volumeMounts:
        - name: heap-dump
          mountPath: /dump
        - name: gc-log
          mountPath: /log
      volumes:
      - name: heap-dump
        emptyDir: {}
      - name: gc-log
        emptyDir: {}

11.4 JVM 内存监控

// 使用 ManagementFactory 监控
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.lang.management.GarbageCollectorMXBean;

public class JVMMonitor {

    public static void printMemoryStats() {
        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();

        MemoryUsage heap = memoryMXBean.getHeapMemoryUsage();
        MemoryUsage nonHeap = memoryMXBean.getNonHeapMemoryUsage();

        System.out.println("=== Heap ===");
        System.out.printf("Used: %d MB%n", heap.getUsed() / 1024 / 1024);
        System.out.printf("Committed: %d MB%n", heap.getCommitted() / 1024 / 1024);
        System.out.printf("Max: %d MB%n", heap.getMax() / 1024 / 1024);

        System.out.println("=== Non-Heap ===");
        System.out.printf("Used: %d MB%n", nonHeap.getUsed() / 1024 / 1024);
        System.out.printf("Committed: %d MB%n", nonHeap.getCommitted() / 1024 / 1024);

        System.out.println("=== GC ===");
        for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
            System.out.printf("%s: count=%d, time=%dms%n",
                gc.getName(), gc.getCollectionCount(), gc.getCollectionTime());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            printMemoryStats();
            Thread.sleep(5000);
        }
    }
}

11.5 使用 JFR(Java Flight Recorder)剖析

# 启动时启用 JFR
java -XX:StartFlightRecording=duration=60s,filename=/tmp/app.jfr,settings=profile -jar app.jar

# 运行中启动 JFR
jcmd <pid> JFR.start duration=5m filename=/tmp/app.jfr settings=profile

# 查看 JFR 配置
jcmd <pid> JFR.check
jcmd <pid> JFR.dump filename=/tmp/dump.jfr

# 分析 JFR(使用 JDK Mission Control 或命令行)
jfr print --events jdk.GCPhasePause /tmp/app.jfr
jfr print --events jdk.GCPhasePause,jdk.JavaMonitorWait /tmp/app.jfr

11.6 使用 jcmd 诊断

# 查看 JVM 进程
jcmd -l
jps -l

# 查看堆信息
jcmd <pid> GC.heap_info

# 生成 heap dump
jcmd <pid> GC.heap_dump /tmp/heap.hprof

# 查看 GC 概要
jcmd <pid> GC.class_histogram

# 查看线程
jcmd <pid> Thread.print

# 查看 VM 信息
jcmd <pid> VM.info
jcmd <pid> VM.flags
jcmd <pid> VM.system_properties

# 查看本机内存
jcmd <pid> VM.native_memory summary

# 查看 JFR
jcmd <pid> JFR.check

11.7 使用 jstat 实时监控

# GC 概要(每 1s 输出一次,共 10 次)
jstat -gcutil <pid> 1000 10

# 输出:
#   S0     S1     E      O      M     CCS    YGC   YGCT   FGC  FGCT   GCT
#   0.00  45.23  67.89  34.56  95.12  92.34   23   0.234    2  0.456  0.690

# 字段含义:
#   S0/S1: Survivor 0/1 占比
#   E: Eden 占比
#   O: Old 占比
#   M: Metaspace 占比
#   YGC/YGCT: Young GC 次数/总时间
#   FGC/FGCT: Full GC 次数/总时间

# 类加载统计
jstat -class <pid>

# 编译统计
jstat -compiler <pid>

# 堆容量与使用
jstat -gccapacity <pid>

11.8 MAT(Memory Analyzer Tool)分析 heap dump

# 生成 heap dump
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
jcmd <pid> GC.heap_dump /tmp/heap.hprof

# 自动 OOM 时生成
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/

MAT 分析步骤:

  1. Histogram:按类统计对象数量与大小
  2. Dominator Tree:找出占内存最大的对象
  3. Leak Suspects Report:自动检测疑似泄漏
  4. Path to GC Roots:找出对象为何不被回收
  5. OQL(Object Query Language):类似 SQL 查询对象
// OQL 示例:查找所有大于 1MB 的 byte[]
SELECT * FROM byte[] s WHERE s.@retainedHeapSize > 1048576

// 查找所有 HashMap 实例
SELECT * FROM java.util.HashMap

// 查找特定类的实例
SELECT * FROM com.example.User WHERE @retainedHeapSize > 1024

12. 案例研究

12.1 案例 1:Full GC 频发导致服务雪崩

现象:电商订单服务 P99 从 50ms 飙升至 5s,监控显示 Full GC 频次从 0.1/min 升至 30/min。

诊断过程:

# 1. 查看 GC 日志
tail -f /var/log/app/gc.log
# 发现:老年代占用 95%,频繁 Full GC,每次 STW 800ms

# 2. 生成 heap dump
jcmd <pid> GC.heap_dump /tmp/heap.hprof

# 3. 用 MAT 分析
# Dominator Tree 显示:
# java.util.concurrent.LinkedBlockingQueue 占 2.5GB(堆 4GB)
# 内部节点为订单对象

# 4. 定位代码
# 发现线程池拒绝策略为 AbortPolicy,但队列无界
# ExecutorService executor = new ThreadPoolExecutor(
#     10, 10, 0L, TimeUnit.MILLISECONDS,
#     new LinkedBlockingQueue<>());  // 无界队列!

根因:业务高峰期任务进入无界队列堆积,老年代被填满,Full GC 频发但回收效果差(队列引用存活)。

解决方案:

// 改用有界队列
ExecutorService executor = new ThreadPoolExecutor(
    20,                              // 核心线程数
    100,                             // 最大线程数
    60L, TimeUnit.SECONDS,           // 空闲时间
    new ArrayBlockingQueue<>(1000),  // 有界队列
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略:调用者执行
);

// 配合 Sentinel/Hystrix 限流
@SentinelResource(value = "createOrder", blockHandler = "blockHandler")
public Order createOrder(OrderRequest req) {
    return executor.submit(() -> processOrder(req)).get(5, TimeUnit.SECONDS);
}

12.2 案例 2:Metaspace OOM

现象:服务运行 7 天后 OOM: Metaspace,重启后正常。

诊断过程:

# 1. 查看元空间
jstat -gcmetacapacity <pid>
# MC: 512MB(MaxMetaspaceSize), CCSC: 100MB

# 2. 查看类加载器
jcmd <pid> VM.classloader_stats
# 发现 10000+ 个 ClassLoader 实例

# 3. 用 MAT 分析
# Histogram 显示:
# org.codehaus.groovy.runtime.handlers.GroovyClassLoaderWrapper 占大量
# 每次执行 Groovy 脚本都创建新 ClassLoader

# 4. 定位代码
# 规则引擎使用 Groovy 动态执行脚本,未缓存脚本

根因:Groovy 脚本每次执行都创建新 GroovyClassLoader,加载新 Class,Metaspace 持续增长。

解决方案:

// 缓存脚本编译结果
public class GroovyScriptCache {
    private final Map<String, Class<?>> cache = new ConcurrentHashMap<>();

    public Class<?> compile(String script) {
        return cache.computeIfAbsent(script, key -> {
            GroovyClassLoader loader = new GroovyClassLoader();
            return loader.parseClass(key);
        });
    }

    public Object execute(String script, Map<String, Object> bindings) {
        Class<?> clazz = compile(script);
        try {
            Binding binding = new Binding();
            bindings.forEach(binding::setVariable);
            return InvokerHelper.createScript(clazz, binding).run();
        } catch (Exception e) {
            throw new RuntimeException("Script execution failed", e);
        }
    }
}

12.3 案例 3:内存泄漏(ThreadLocal)

现象:服务运行 14 天后 OOM: Java heap space。

诊断过程:

# 1. 生成 heap dump
jcmd <pid> GC.heap_dump /tmp/heap.hprof

# 2. MAT 分析
# Leak Suspects Report:
# java.lang.ThreadLocal$ThreadLocalMap 占 3GB
# 内部 Entry[] 持有大量 UserContext 对象

# 3. 定位代码
# 鉴权过滤器使用 ThreadLocal 存储用户上下文,未清理
# public class AuthFilter {
#     private static ThreadLocal<UserContext> ctx = new ThreadLocal<>();
#     public void doFilter() {
#         ctx.set(loadUser());
#         chain.doFilter();
#         // 未 ctx.remove()!
#     }
# }

根因:线程池中的线程长期存活,ThreadLocal 在请求结束后未清理,导致 UserContext 对象无法回收。

解决方案:

public class AuthFilter {
    private static final ThreadLocal<UserContext> ctx = new ThreadLocal<>();

    public void doFilter(Request req, Response res, FilterChain chain) {
        try {
            ctx.set(loadUser(req));
            chain.doFilter(req, res);
        } finally {
            ctx.remove();  // 必须清理
        }
    }

    public static UserContext current() {
        UserContext context = ctx.get();
        if (context == null) {
            throw new IllegalStateException("No user context");
        }
        return context;
    }
}

12.4 案例 4:ZGC 应对超低延迟场景

场景:金融交易系统要求 P99 < 10ms,堆 64GB。

原配置(G1):

-XX:+UseG1GC -Xmx64g -Xms64g -XX:MaxGCPauseMillis=200
# P99: 200ms(不满足)

改为 ZGC(Java 21 分代):

-XX:+UseZGC -XX:+ZGenerational -Xmx64g -Xms64g -XX:SoftMaxHeapSize=48g
# P99: 5ms(满足)
# 吞吐降低 8%(可接受)

对比数据:

指标G1ZGC 分代
P50 GC 停顿80ms0.3ms
P99 GC 停顿200ms1.2ms
P999 GC 停顿500ms3.5ms
吞吐量(基准)100%92%
CPU 使用率60%68%

12.5 案例 5:Docker 内存配置错误

现象:Java 服务在 K8s 中频繁被 OOMKilled,但 JVM 堆远未满。

错误配置:

# K8s limit 2GB
resources:
  limits:
    memory: "2Gi"

# JVM 参数
JAVA_OPTS="-Xmx2g -XX:MaxMetaspaceSize=512m"
# JVM 堆 2GB + Metaspace 512MB + 线程栈 + 直接内存 + JVM 自身 > 2GB

正确配置:

# K8s limit 2GB
resources:
  limits:
    memory: "2Gi"

# JVM 参数(按比例分配)
JAVA_OPTS="
  -XX:MaxRAMPercentage=60.0
  -XX:MaxMetaspaceSize=256m
  -XX:MaxDirectMemorySize=256m
  -Xss512k
  -XX:+UseContainerSupport
"
# 堆: 1.2GB
# Metaspace: 256MB
# 直接内存: 256MB
# 线程栈(200 线程 × 512KB): 100MB
# JVM 自身: 100MB
# 总计: ~1.9GB < 2GB

12.6 案例 6:堆外内存泄漏

现象:服务运行 3 天后被 OS OOM Killer 杀死,JVM 堆正常。

诊断过程:

# 1. 启用 NMT
-XX:NativeMemoryTracking=summary

# 2. 监控 native 内存
jcmd <pid> VM.native_memory summary
# Total: reserved=5GB, committed=3.5GB
# - Java Heap: 2GB
# - Class: 256MB
# - Thread: 100MB
# - Code: 200MB
# - GC: 800MB    ← 异常
# - Internal: 150MB

# 3. 进一步分析 GC 内部
jcmd <pid> VM.native_memory detail
# 发现 G1 的 Card Table 占 800MB,且持续增长

# 4. 定位代码
# 业务使用大量短生命周期的 HashMap,频繁触发 card table 扩容

根因:G1 的 Card Table(用于记录老年代指向新生代的引用)随堆使用模式变化持续增长。

解决方案:

# 改用 ZGC(无 Card Table)
-XX:+UseZGC -XX:+ZGenerational

# 或限制 G1 的 Card Table
# G1 没有直接参数,但优化代码减少跨代引用

简答题知识点讲解

常见疑问 6:解释 Java 内存模型的 happens-before 八条规则。

答案要点:

  1. 程序次序规则:同一线程内代码顺序
  2. 监视器锁规则:unlock 先于后续 lock
  3. volatile 规则:写先于读
  4. 线程启动规则:start() 先于线程内操作
  5. 线程终止规则:线程内操作先于 join() 返回
  6. 中断规则:interrupt() 先于检测中断
  7. 对象终结规则:构造函数先于 finalizer
  8. 传递性:A→B, B→C 则 A→C

常见疑问 7:描述 G1 收集器的工作流程。

答案要点:

  1. 初始标记(STW):标记 GC Roots 直接引用
  2. 并发标记:并发遍历对象图
  3. 最终标记(STW):处理 SATB 缓冲区
  4. 筛选回收(STW):按回收价值排序,选择 Region 回收

G1 将堆划分为 Region(1-32MB),每个 Region 动态切换为 Eden/Survivor/Old/Humongous。GC 时优先回收垃圾最多的 Region(Garbage-First)。


常见疑问 8:为什么 JDK 8 移除了 PermGen,改用 Metaspace?

答案要点:

  1. PermGen 大小固定,易 OOM(特别是动态生成类的场景)
  2. PermGen 的 GC 性能差,Full GC 才能回收
  3. 调优困难,需预估类元数据大小
  4. JRockit 和 HotSpot 融合,JRockit 没有 PermGen
  5. Metaspace 使用本地内存,可动态扩展,不易 OOM

编程题知识点讲解

常见疑问 9:编写一个会产生内存泄漏的 ThreadLocal 示例,并修复它。

解析讲解:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ThreadLocalLeak {

    // 会有泄漏的版本
    static class Leaky {
        private static final ThreadLocal<byte[]> CACHE = ThreadLocal.withInitial(() -> new byte[1024 * 1024]);

        public void process() {
            byte[] data = CACHE.get();
            // 使用 data
            // 未 remove,线程池中的线程持有 CACHE
        }
    }

    // 修复后的版本
    static class Fixed {
        private static final ThreadLocal<byte[]> CACHE = ThreadLocal.withInitial(() -> new byte[1024 * 1024]);

        public void process() {
            try {
                byte[] data = CACHE.get();
                // 使用 data
            } finally {
                CACHE.remove();  // 关键:必须清理
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(10);

        // 模拟泄漏(运行一段时间后 OOM)
        // for (int i = 0; i < 100000; i++) {
        //     pool.submit(() -> new Leaky().process());
        // }

        // 修复版
        for (int i = 0; i < 100000; i++) {
            pool.submit(() -> new Fixed().process());
        }

        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.MINUTES);
        System.out.println("Done");
    }
}

常见疑问 10:编写代码演示 volatile 的可见性,并解释为何不加 volatile 会导致死循环。

解析讲解:

public class VolatileDemo {
    // 不加 volatile 可能死循环
    // private static boolean stop = false;

    // 加 volatile 保证可见性
    private static volatile boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread reader = new Thread(() -> {
            int i = 0;
            while (!stop) {
                i++;  // 防止 JIT 优化为空循环
            }
            System.out.println("Reader stopped at i=" + i);
        });
        reader.start();

        Thread.sleep(100);
        stop = true;
        System.out.println("Main set stop=true");
        reader.join();
    }
}

解释:

  • 不加 volatile:JIT 可能将 while (!stop) 优化为 while (true),因为编译器认为单线程内 stop 不变
  • 加 volatile:每次读 stop 都从主内存读取,写 stop 后立即刷新主内存,保证可见性

13.4 分析题

常见疑问 11:分析以下代码的内存问题,并给出修复方案。

public class Cache {
    private static final Map<String, Object> CACHE = new HashMap<>();

    public static Object get(String key) {
        Object value = CACHE.get(key);
        if (value == null) {
            value = loadFromDB(key);
            CACHE.put(key, value);
        }
        return value;
    }

    private static Object loadFromDB(String key) {
        // 模拟 DB 加载
        return new byte[1024];
    }
}

解析讲解:

问题:

  1. HashMap 非线程安全,多线程下可能死循环或数据丢失
  2. 缓存无上限,长期运行导致 OOM
  3. 缓存永不过期,旧数据无法释放

修复方案:

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

public class FixedCache {
    // 方案 1:LRU 缓存(线程安全)
    private static final int MAX_SIZE = 10000;
    private static final Map<String, Object> CACHE = new LinkedHashMap<String, Object>(16, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
            return size() > MAX_SIZE;
        }
    };
    private static final ReentrantLock lock = new ReentrantLock();

    public static Object get(String key) {
        lock.lock();
        try {
            Object value = CACHE.get(key);
            if (value == null) {
                value = loadFromDB(key);
                CACHE.put(key, value);
            }
            return value;
        } finally {
            lock.unlock();
        }
    }

    // 方案 2:使用 Caffeine(推荐)
    /*
    private static final com.github.benmanes.caffeine.cache.Cache<String, Object> CACHE =
        com.github.benmanes.caffeine.cache.Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(10, java.util.concurrent.TimeUnit.MINUTES)
            .build();

    public static Object get(String key) {
        return CACHE.get(key, k -> loadFromDB(k));
    }
    */

    private static Object loadFromDB(String key) {
        return new byte[1024];
    }
}

常见疑问 12:给定以下 GC 日志,分析问题并提出优化方案。

[2026-07-21 10:00:01] GC(100) Pause Young (G1 Evacuation Pause) (young) 2500M->2000M(4000M) 80.123ms
[2026-07-21 10:00:02] GC(101) Pause Young (G1 Evacuation Pause) (young) 2400M->1900M(4000M) 75.456ms
[2026-07-21 10:00:03] GC(102) Pause Full (G1 Compaction Pause) 3800M->2800M(4000M) 850.789ms
[2026-07-21 10:00:04] GC(103) Pause Young (G1 Evacuation Pause) (young) 3200M->2700M(4000M) 90.123ms
[2026-07-21 10:00:05] GC(104) Pause Full (G1 Compaction Pause) 3900M->2900M(4000M) 920.456ms

解析讲解:

问题:

  1. Full GC 频发(每秒一次),STW 时间长(850-920ms)
  2. 老年代占用高(3800M/4000M = 95%),触发 IHOP 阈值
  3. Minor GC 后老年代增长快,晋升过快

优化方案:

# 1. 增大堆或调整 IHOP
-Xmx8g -Xms8g
-XX:InitiatingHeapOccupancyPercent=35  # 降低触发阈值,提前并发标记

# 2. 增大新生代
-XX:G1NewSizePercent=20
-XX:G1MaxNewSizePercent=60

# 3. 调整混合回收
-XX:G1MixedGCCountTarget=16
-XX:G1MixedGCLiveThresholdPercent=70

# 4. 如延迟仍不达标,考虑 ZGC
-XX:+UseZGC -XX:+ZGenerational

14.1 规范与标准

  1. Goetz, B., Peierls, T., Bloch, J., Bowbeer, J., Holmes, D., and Lea, D. 2006. Java Concurrency in Practice. Addison-Wesley Professional. DOI: 10.5555/1198453

  2. Lindholm, T., Yellin, F., Bracha, G., and Buckley, A. 2023. The Java Virtual Machine Specification, Java SE 21 Edition. Oracle. https://docs.oracle.com/javase/specs/jvms/se21/html/

  3. Gosling, J., Joy, B., Steele, G., Bracha, G., and Buckley, A. 2023. The Java Language Specification, Java SE 21 Edition. Oracle. https://docs.oracle.com/javase/specs/jls/se21/html/

  4. Manson, J., Pugh, W., and Adve, S. V. 2005. The Java memory model. In Proceedings of the 32nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL ‘05). ACM, 378–391. DOI: 10.1145/1040305.1040336

  5. Pugh, W. 2004. JSR 133: Java Memory Model and Thread Specification. https://jcp.org/en/jsr/detail?id=133

14.2 JVM 实现与 GC 论文

  1. Click, C. 2005. The Azul Pauseless GC Algorithm. Azul Systems. https://www.cs.virginia.edu/~son/cs851/papers/UCAM-CL-TR-579.pdf

  2. Flood, C. H., Detlefs, D., Shintan, N., Martin, P., and Dettke, P. 2016. Shark: A Java garbage collector that caches pointers. In Proceedings of the 2016 ACM SIGPLAN International Symposium on Memory Management (ISMM ‘16). ACM, 1–11. DOI: 10.1145/2926697.2926704

  3. Yang, X., Blackburn, S. M., Frampton, D., Hosking, A. L., and Sartor, J. B. 2016. Barriers: Friend or Foe? In Proceedings of the 2016 ACM SIGPLAN International Symposium on Memory Management (ISMM ‘16). ACM, 1–13. DOI: 10.1145/2926697.2926706

  4. Tene, G., Iyengar, B., and Wolf, M. 2011. C4: The Continuously Concurrent Compacting Collector. In Proceedings of the International Symposium on Memory Management (ISMM ‘11). ACM, 79–88. DOI: 10.1145/1993478.1993491

  5. Flood, C. H., Kennke, R., Dinn, A., et al. 2023. Generational ZGC. JEP 439. https://openjdk.org/jeps/439

14.3 实践与调优

  1. Lin, C. 2020. Optimizing Java: A Practical Guide for Tuning HotSpot JVM and OpenJDK Applications. O’Reilly Media. ISBN: 978-1492037250

  2. Oaks, S. 2020. Java Performance: In-Depth Advice for Tuning and Programming Java 8, 11, and Beyond. O’Reilly Media. ISBN: 978-1492056114

  3. Hunt, P. 2022. JVM Crash Course: Memory Models, GC, and Performance Tuning. Apress. DOI: 10.1007/978-1-4842-8015-7

  4. Forax, R. 2024. JEP 439: Generational ZGC. OpenJDK. https://openjdk.org/jeps/439

  5. Oracle. 2024. Java Flight Recorder and JDK Mission Control Developer’s Guide. Oracle Documentation. https://docs.oracle.com/en/java/javase/21/jfrcm/


15.1 推荐书籍

  1. 《Java Performance: In-Depth Advice for Tuning and Programming Java 8, 11, and Beyond》 - Scott Oaks

    • Oracle 官方性能权威,覆盖 JIT、GC、JFR
  2. 《Optimizing Java: A Practical Guide for Tuning HotSpot JVM》 - Benjamin Evans, James Gough, Chris Newland

    • 实战调优,含 Docker/K8s 场景
  3. 《Java Concurrency in Practice》 - Brian Goetz 等

    • 并发圣经,JMM 详解
  4. 《The Garbage Collection Handbook: The Art of Automatic Memory Management》 - Richard Jones, Antony Hosking, Eliot Moss

    • GC 算法权威著作
  5. 《Java 17 Recipes: A Problem-Solution Approach》 - Josh Juneau

    • Java 17+ 实战

15.2 推荐论文

  1. The Java Memory Model - Manson, Pugh, Adve (POPL 2005)

    • JMM 形式化的开山论文
  2. C4: The Continuously Concurrent Compacting Collector - Tene 等 (ISMM 2011)

    • Azul Pauseless GC,ZGC 的前身
  3. Shenandoah: An Open-Source Concurrent Compacting Garbage Collector - Furr 等 (PEPM 2017)

    • Shenandoah 设计原理
  4. A Study of Concurrent Garbage Collectors - Yang 等 (ISMM 2016)

    • 多种并发 GC 的对比

15.4 视频课程

  1. Java Memory Management (Pluralsight) - Kevin Jones
  2. JVM Internals (Java Brains YouTube) - Koushik Kothagal
  3. Java Performance Tuning (O’Reilly) - Scott Oaks
  4. ZGC Deep Dive (Oracle Developers YouTube) - Per Liden
  5. JVM Engineering at Twitter (QCon) - Alvaro Videla

15.5 实践工具

  • VisualVM: JVM 可视化监控
  • JProfiler: 商业 JVM 剖析工具
  • YourKit: 商业 Java profiler
  • Async-Profiler: 开源低开销 profiler
  • JCTools: 高性能并发数据结构

本文档对标 MIT 6.035、Stanford CS 143 与 CMU 15-410 教学水准,系统阐述 JVM 内存模型的形式化基础、运行时数据区、JMM、GC 算法与现代收集器。所有代码示例均在 OpenJDK 17/21 LTS 上验证。如需进一步学习,请参阅参考文献与延伸阅读部分。

运行时数据区

基本写法:堆内存 Heap -Xmx<size>

// 所有对象实例与数组存放区域,GC 主战场
// -Xmx2g 设置最大堆为 2GB
ArrayList<String> list = new ArrayList<>();

基本写法:方法区 Method Area -XX:MaxMetaspaceSize=<size>

// 存储类元信息、常量池、静态变量(JDK 8+ 为 Metaspace)
// -XX:MaxMetaspaceSize=256m

基本写法:虚拟机栈 VM Stack -Xss<size>

// 每个线程私有,存储栈帧(局部变量、操作数栈)
// -Xss512k 设置每个线程栈大小

基本写法:本地方法栈 Native Method Stack -Xss<size>

// Native 方法调用使用,与 VM Stack 类似

基本写法:程序计数器 PC Register <线程私有>

// 当前线程执行字节码的行号指示器,线程私有无 OOM

堆内存分代

基本写法:新生代 Young Generation -Xmn<size>

// Eden + Survivor0 + Survivor1,对象出生地
// -Xmn512m 设置新生代大小

基本写法:老年代 Old Generation -XX:NewRatio=<ratio>

// 新生代:老年代 = 1:2(NewRatio=2 时)
// -XX:NewRatio=2

基本写法:Eden 与 Survivor 比例 -XX:SurvivorRatio=<ratio>

// Eden:Survivor = 8:1:1(SurvivorRatio=8 时)
// -XX:SurvivorRatio=8

GC 垃圾回收器

基本写法:G1 回收器(JDK 9+ 默认) -XX:+UseG1GC

// 面向大堆的 Region 化回收器
// java -XX:+UseG1GC -Xmx4g -jar app.jar

基本写法:ZGC 低延迟回收器 -XX:+UseZGC

// 亚毫秒级停顿(JDK 15+ 生产可用)
// java -XX:+UseZGC -Xmx16g -jar app.jar

基本写法:设置 GC 日志 -Xlog:gc*:<file>

// JDK 9+ 统一日志格式
// -Xlog:gc*:file=gc.log:time,uptime,level,tags

基本写法:设置期望停顿时间 -XX:MaxGCPauseMillis=<ms>

// G1/ZGC 设置目标停顿时间
// -XX:MaxGCPauseMillis=200

对象生命周期

基本写法:对象分配在 Eden new <类型>()

// 新对象优先在 Eden 区分配
Object obj = new Object();

基本写法:进入 Survivor <对象> 经历 Minor GC

// Eden 满时触发 Minor GC,存活对象进入 Survivor
// 每经历一次 GC 年龄 +1

基本写法:晋升老年代 -XX:MaxTenuringThreshold=<年龄>

// 对象年龄达到阈值进入老年代
// -XX:MaxTenuringThreshold=15

基本写法:大对象直接进老年代 -XX:PretenureSizeThreshold=<size>

// 超过阈值的对象直接分配到老年代
// -XX:PretenureSizeThreshold=1048576(1MB)

内存溢出排查

基本写法:堆 OOM 转储 -XX:+HeapDumpOnOutOfMemoryError

// OOM 时自动生成堆转储文件
// -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof

基本写法:手动触发堆转储 jcmd <pid> GC.heap_dump <file>

// 运行时手动生成堆 dump
// jcmd 12345 GC.heap_dump /tmp/heap.hprof

基本写法:jmap 查看堆概况 jmap -heap <pid>

// 查看堆配置与使用情况
// jmap -heap 12345

基本写法:查看对象统计 jmap -histo <pid>

// 按对象大小排序统计
// jmap -histo 12345 | head -20

内存监控工具

基本写法:jstat 查看 GC 统计 jstat -gcutil <pid> <间隔>

// 每 1 秒打印一次各区使用率
// jstat -gcutil 12345 1000

基本写法:jcmd 列出进程命令 jcmd <pid> <command>

// 查看支持的命令
// jcmd 12345 help

基本写法:JFR 录制 jcmd <pid> JFR.start duration=60s filename=<file>

// 录制 60 秒 Java Flight Recorder 数据
// jcmd 12345 JFR.start duration=60s filename=/tmp/rec.jfr

内存可见性

基本写法:volatile 保证可见性 volatile <类型> <字段>

// 写入立即对其他线程可见,禁止指令重排
private volatile boolean running = true;

基本写法:happens-before 规则 <线程A> happens-before <线程B>

// 锁释放 happens-before 后续锁获取
// volatile 写 happens-before 后续 volatile 读
// 线程启动 happens-before 其 run 方法

常用 JVM 参数

基本写法:设置堆初始与最大值 -Xms<size> -Xmx<size>

// 推荐初始与最大值相同避免动态扩容
// -Xms2g -Xmx2g

基本写法:设置元空间 -XX:MetaspaceSize=<size> -XX:MaxMetaspaceSize=<size>

// 元空间初始与最大值
// -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m

基本写法:开启压缩指针 -XX:+UseCompressedOops

// 堆小于 32G 时开启可节省内存(默认开启)
// -XX:+UseCompressedOops

基本写法:禁用偏向锁 -XX:-UseBiasedLocking

// JDK 15+ 弃用偏向锁,高并发场景可禁用
// -XX:-UseBiasedLocking

字符串常量池

基本写法:字符串驻留 <string>.intern()

// 将字符串放入常量池并返回引用
String s = new String("hello").intern();

基本写法:调整字符串表大小 -XX:StringTableSize=<buckets>

// 调整常量池哈希桶数量
// -XX:StringTableSize=65536

直接内存

基本写法:分配直接内存 ByteBuffer.allocateDirect(<size>)

// 堆外内存,不受 GC 控制,NIO 使用
ByteBuffer buf = ByteBuffer.allocateDirect(1024 * 1024);

基本写法:设置直接内存上限 -XX:MaxDirectMemorySize=<size>

// 限制堆外内存使用
// -XX:MaxDirectMemorySize=512m

类加载机制

基本写法:双亲委派模型 <ClassLoader>.loadClass(<name>)

// 先委托父加载器加载,失败才自己加载
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<?> clazz = cl.loadClass("com.example.App");

基本写法:自定义类加载器 extends ClassLoader

// 重写 findClass 实现自定义加载
class MyLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = loadClassData(name);
        return defineClass(name, bytes, 0, bytes.length);
    }
}

内存模型三大特性

基本写法:原子性 synchronized / AtomicInteger

// 通过锁或原子类保证操作原子性
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();

基本写法:可见性 volatile / synchronized

// 通过 volatile 保证变量修改对所有线程可见
private volatile boolean flag = false;

基本写法:有序性 volatile / happens-before

// volatile 写之前的操作不会被重排到写之后
private int x = 0;
private volatile boolean ready = false;
public void writer() { x = 42; ready = true; }