java

JVM 原理与调优基础

By AI-Writer 20 min read

JVM 原理与调优基础

理解 JVM(Java Virtual Machine)是成为高级 Java 开发者的必经之路。JVM 管理内存、加载类、执行字节码,是 Java “一次编写,到处运行”的核心。本文深入讲解 JVM 内存结构、类加载机制、垃圾回收算法以及常用调优工具。

JVM 内存结构

JVM 运行时数据区分为以下几个部分:

plaintext
┌─────────────────────────────────────────────┐
│                   JVM 进程                   │
│  ┌──────────┐  ┌──────────┐  ┌────────────┐ │
│  │ PC Register│  │ VM Stack │  │ Native Stack │ │
│  └──────────┘  └──────────┘  └────────────┘ │
│                  线程私有                    │
│  ┌─────────────────────────────────────┐    │
│  │           Heap(堆)线程共享          │    │
│  │  ┌────────┐              ┌────────┐  │    │
│  │  │ Eden   │ S0  │ S1 │    │ Old Gen │  │    │
│  │  │  伊甸区│ Survivor │    │ 老年代  │  │    │
│  │  └────────┘              └────────┘  │    │
│  └─────────────────────────────────────┘    │
│  ┌──────────────────────────────────────┐   │
│  │     Metaspace(元数据区)线程共享      │   │
│  └──────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

各区域职责

线程私有区域(每个线程独享)

  • PC Register(程序计数器):记录当前线程正在执行的字节码指令地址
  • VM Stack(虚拟机栈):每个线程的方法调用栈,存放栈帧,栈帧包含局部变量表、操作数栈、方法返回地址
  • Native Stack:用于本地方法(C/C++)调用

线程共享区域

  • Heap(堆):存放对象实例和数组,是 GC 的主要管理区域
  • Metaspace(元数据区):Java 8+ 替代 PermGen(永久代),存放类的元信息(Java 17+ 使用 ZGC 后默认无限大)

栈帧结构

java
public class StackDemo {
    public int factorial(int n) {
        if (n <= 1) return 1;
        return n * factorial(n - 1);  // 递归调用
    }
}

// 每次递归调用 factorial() 都会在栈上压入一个新栈帧
// 递归深度过大会导致 StackOverflowError
java
// 栈溢出示例
public class StackOverflow {
    public static void recurse() {
        recurse();  // 无限递归 → StackOverflowError
    }

    public static void main(String[] args) {
        // 默认栈大小约 1MB(可通过 -Xss 参数调整)
        // 如果方法调用链路深(如深层递归),栈会溢出
        recurse();
    }
}

堆内存分配

java
public class HeapDemo {
    public static void main(String[] args) {
        // -Xms256m -Xmx512m:最小堆 256MB,最大堆 512MB

        // 对象在堆上分配
        Object obj = new Object();  // obj 引用在栈上,对象在堆上

        // 数组也是对象
        int[] arr = new int[1_000_000];  // 约 4MB 的堆内存

        // 大对象直接进入老年代(通过 -XX:PretenureSizeThreshold 设置阈值)
        byte[] large = new byte[100 * 1024 * 1024];  // 100MB 的大数组

        // 查看 GC 日志,了解对象分配情况
        // java -Xms256m -Xmx512m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps ...
    }
}

类加载机制

Java 程序启动时,JVM 通过类加载器.class 文件加载到内存中,并执行初始化。

双亲委派模型

类加载器按层级关系工作,先向上委托,再向下加载

plaintext
启动类加载器(Bootstrap ClassLoader)
    └── 扩展类加载器(Extension ClassLoader)
            └── 应用类加载器(Application ClassLoader)
                    └── 自定义类加载器(User ClassLoader)
java
public class ClassLoaderDemo {
    public static void main(String[] args) {
        // 查看类的加载器
        ClassLoader cl = String.class.getClassLoader();
        System.out.println(cl);  // null → Bootstrap ClassLoader(Java 核心类)

        ClassLoader cl2 = ClassLoaderDemo.class.getClassLoader();
        System.out.println(cl2);  // AppClassLoader(应用类加载器)

        System.out.println(ClassLoaderDemo.class.getClassLoader().getParent());  // PlatformClassLoader
    }
}

类加载过程

java
public class ClassLoadingProcess {
    // 1. 加载(Loading):读取 .class 文件,生成 Class 对象
    // 2. 链接(Linking)
    //    - 验证(Verification):检查字节码格式是否正确
    //    - 准备(Preparation):为静态变量分配内存,赋默认值(0/null)
    //    - 解析(Resolution):将符号引用替换为直接引用
    // 3. 初始化(Initialization):执行静态代码块,给静态变量赋初值

    static int value = 10;  // 准备阶段=0,初始化阶段=10

    static {
        value = 20;  // 静态代码块在初始化阶段执行
    }

    public static void main(String[] args) {
        System.out.println(value);  // 20
    }
}

垃圾回收(GC)

GC 是 JVM 最重要的自动内存管理机制——自动回收不再使用的对象,防止内存泄漏。

如何判断对象可回收

引用计数法(有缺陷,无法处理循环引用):

java
// 引用计数:对象每多一个引用,计数器+1;引用失效-1
// 问题:objA → objB → objA 循环引用时,两个对象计数器都是1,无法回收

可达性分析算法(GC Roots)(主流):

从一组根对象(GC Roots)出发,通过引用链遍历所有可达对象,其余对象即为可回收垃圾:

java
// GC Roots 包括:
// - 虚拟机栈(栈帧中的本地变量表)中引用的对象
// - 方法区中类静态属性引用的对象
// - 方法区中常量引用的对象
// - 本地方法栈中 JNI 引用的对象
// - 虚拟机内部的引用(ClassLoader、异常对象等)

public class GCrootsDemo {
    static class A { B b; }  // A 持有 B 的引用
    static class B { A a; }  // B 持有 A 的引用,形成循环引用

    public static void main(String[] args) {
        A a = new A();  // a 是 GC Root
        B b = new B();   // b 是 GC Root
        a.b = b;
        b.a = a;

        a = null;   // a 引用断开
        b = null;   // b 引用断开
        // 此时 A 和 B 循环引用,但没有任何 GC Root 可达
        // → 可被 GC 回收
    }
}

分代回收理论

不同生命周期的对象适合不同的回收策略:

plaintext
┌─────────────────────────────────────────┐
│ Heap                                    │
│  ┌───────────────┐  ┌───────────────┐  │
│  │ Young Gen     │  │ Old Gen       │  │
│  │               │  │               │  │
│  │  ┌───┐ ┌───┐  │  │               │  │
│  │  │Eden│S0│S1│  │  │ 大对象/长寿命 │  │
│  │  └───┘ └───┘  │  │               │  │
│  │  8:1:1       │  │               │  │
│  └───────────────┘  └───────────────┘  │
│                                         │
│  Minor GC(频繁)    Major/Full GC     │
│  回收 Young Gen     回收 Old Gen + Metaspace│
└─────────────────────────────────────────┘

垃圾回收算法

标记-清除(Mark-Sweep):先标记所有存活对象,再清除垃圾。缺点是产生内存碎片。

复制(Copying):将内存分为两半,每次只用一半;存活对象复制到另一半,然后清空当前半。缺点是只能使用一半内存。

标记-整理(Mark-Compact):标记存活对象,将它们移向一端,然后清理边界外的内存。解决碎片问题,但移动成本高。

主流 GC 收集器

收集器算法适用场景特点
Serial GC复制(Young)+ 标记整理(Old)单核/客户端简单高效,Stop The World
Parallel GC(吞吐量优先)复制(Young)+ 标记整理(Old)多核/批处理吞吐量最高,Stop The World
CMS GC初始标记→并发标记→重新标记→并发清除低延迟需求已废弃,Java 14 移除
G1 GC(默认)标记整理服务端(Java 9+默认)可预测延迟,区域化回收
ZGC着色指针 + 读屏障超大堆(TB级)亚毫秒级停顿
Shenandoah GC着色指针 + 读屏障低延迟非 JDK 官方(Red Hat)

G1 GC 详解

G1(Garbage First) 是 Java 9+ 的默认 GC,将堆划分为多个大小相等的 Region,并发执行回收:

bash
# G1 GC 调优参数
java -Xms4g -Xmx4g \         # 堆大小 4GB
     -XX:MaxGCPauseMillis=200 \  # 目标最大停顿时间 200ms
     -XX:+UseG1GC \              # 使用 G1 GC
     -XX:G1HeapRegionSize=4m \  # Region 大小(1/2/4/8/16/32 MB)
     -XX:InitiatingHeapOccupancyPercent=45 \  # 堆占用 45% 时触发 Mixed GC
     -XX:G1NewSizePercent=5 \    # Young Gen 最小比例 5%
     -XX:G1MaxNewSizePercent=60 \ # Young Gen 最大比例 60%
     -XX:+PrintGCDetails \       # 打印详细 GC 日志
     -XX:+PrintGCDateStamps \    # 打印时间戳
     -Xlog:gc*:file=gc.log \     # 输出到文件
     MyApp

ZGC:亚毫秒级停顿

ZGC 是 Java 11+ 实验特性、Java 15+ 正式版,支持 TB 级堆内存的同时,将停顿时间控制在亚毫秒级

bash
# ZGC 调优参数
java -Xms16g -Xmx16g \
     -XX:+UseZGC \
     -XX:MaxGCPauseMillis=1 \    # 目标最大停顿 1ms
     -XX:+ZCollectionInterval=60 \  # 每 60 秒触发一次 GC
     MyApp
java
// ZGC 适合的场景:
// - 堆内存 8GB 以上
// - 对停顿时间敏感(如 Web 应用、交易系统)
// - 吞吐量要求不是极端高

// ZGC 的代价:
// - 吞吐量略低于 G1(约 5-15%)
// - 更高的内存开销(着色指针占用)
// - 不支持类卸载(Java 21+ 已支持)

常用调优参数

bash
# 内存参数
java -Xms256m -Xmx256m      # 固定堆大小(生产环境推荐:-Xms=-Xmx)
java -Xss2m                  # 每个线程栈大小(默认 1MB)
java -XX:MetaspaceSize=256m  # 元数据区初始大小
java -XX:MaxMetaspaceSize=512m # 元数据区最大大小

# GC 参数
java -XX:+UseG1GC            # 使用 G1 GC
java -XX:+UseZGC             # 使用 ZGC(Java 15+)
java -XX:MaxGCPauseMillis=100  # 最大 GC 停顿目标(仅 G1)
java -XX:+PrintGC            # 打印 GC 概要信息
java -XX:+PrintGCDetails     # 打印详细 GC 信息

# OOM 时导出堆转储
java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/heap.hprof \
     MyApp

诊断工具

jstat:GC 统计

bash
# 每 1000ms 输出一次 GC 统计,共输出 10 次
jstat -gcutil <pid> 1000 10

# 输出:
#   S0    S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
#  0.00  12.45  45.23  67.89  94.12  91.23    123   5.678    12   2.345   8.023

# 字段说明:
# S0/S1:Survivor 区使用率
# E:Eden 区使用率(高且频繁 → Minor GC 频繁)
# O:Old 区使用率(高且上升 → 对象晋升过快)
# YGC:Young GC 次数(过多 → Minor GC 太频繁)
# FGCT:Full GC 总耗时(过长 → Old 区回收成本高)

Arthas:线上问题诊断

Arthas(阿尔萨斯)是阿里巴巴开源的 Java 诊断工具,无需重启 JVM 即可实时诊断:

bash
# 启动 Arthas
java -jar arthas-boot.jar

# 常用命令
dashboard              # 查看 JVM 概览(线程、内存、GC)
thread                 # 查看所有线程状态
thread <n>             # 查看指定线程的堆栈
thread -b              # 查找阻塞其他线程的线程
heapdump /tmp/heap.hprof  # 导出堆转储(排查 OOM)

# 监控方法调用
watch com.example.UserService login '{params, returnObj}' -x 3

# 反编译类
jad com.example.UserService

# 查看方法耗时
profiler start --event cpu
profiler stop --format html

jmap:堆内存分析

bash
# 查看堆摘要
jmap -heap <pid>

# 导出堆转储(用于 MAT/JProfiler 分析)
jmap -dump:format=b,file=/tmp/heap.hprof <pid>

# 统计类实例数量(有助于发现内存泄漏)
jmap -histo <pid> | head -30

小结

  • JVM 内存分为线程私有(PC Register / VM Stack / Native Stack)和线程共享(Heap / Metaspace)区域
  • 是 GC 的主要管理区域,采用分代回收策略(Young Gen → Old Gen)
  • 类加载遵循双亲委派模型,避免类的重复加载和核心类的篡改
  • G1 GC 是 Java 9+ 默认收集器,适合大多数服务端场景;ZGC 适合超大堆和超低延迟需求
  • jstatArthasjmap 是日常诊断的三大工具
  • GC 调优的核心思路:先观察,后调整——通过 GC 日志定位瓶颈,再针对性调整参数

JVM 是 Java 后端工程的核心基础设施。掌握本文内容后,你对 Java 程序的运行机制将有一个系统性的认识,能够更好地进行性能分析和问题排查。

至此,Java 完整学习路线已全部覆盖——从基础语法到高级主题,期待你将这些知识运用到实际项目中!

#java #JVM #内存结构 #类加载 #垃圾回收 #GC #G1GC #ZGC #Arthas #性能调优

评论

A

Written by

AI-Writer

Related Articles

java
#6

Java 17+ 新特性精讲

全面讲解 Java 17 LTS 的核心新特性:Records、Sealed Classes、Pattern Matching for instanceof、Text Blocks、Switch Expressions,附详细代码示例

Read More
java
#12

JVM 原理与调优基础

深入讲解 JVM 内存结构(堆/栈/方法区)、类加载机制、垃圾回收算法(GC),以及 Java 17+ G1GC、ZGC、常用调优参数和诊断工具

Read More
java
#2

Java 基础语法

深入讲解 Java 的数据类型、运算符、控制流程(if/for/while/switch)、方法定义与重载、数组声明与遍历

Read More