JVM(Java 虚拟机)

发布于:2025-02-11 ⋅ 阅读:(59) ⋅ 点赞:(0)

Java语言的解释性和编译性(通过JVM 的执行引擎)

 Java 代码(.java 文件)要先使用 javac 编译器编译为 .class 文件(字节码),紧接着再通过JVM 的执行引擎(Execution Engine) 负责处理 Java 字节码并执行,它的主要组成部分包括:

  • 解释器(Interpreter):逐行解释字节码执行,启动快但执行速度较慢。
  • JIT 编译器(Just-In-Time Compiler):将热点字节码编译为本地机器码,提高执行效率。
  • 垃圾回收器(Garbage Collector, GC):管理 Java 堆中的对象回收,提升内存管理效率。

(1)Java 代码先编译成字节码

  • Java 代码(.java 文件)使用 javac 编译器编译为 .class 文件(字节码)。
  • 这种字节码与平台无关,可以在不同的 JVM 上运行。

(2)JVM 先解释执行,再逐步编译为机器码

  • 当 JVM 启动 Java 应用程序时,解释器(Interpreter)逐条解析字节码并执行,优点是启动速度快,但缺点是运行速度慢。
  • JVM 会分析热点代码(执行次数较多的代码),并使用 JIT(即时编译器) 将热点代码直接编译成机器码,提高性能。

(3)JIT 编译器优化热点代码

  • JIT 编译器(如 C1、C2 编译器)会在代码执行时,将热点字节码转换为本地机器码,提升执行速度。
  • 解释执行 + JIT 编译 的混合模式确保 Java 既有较快的启动速度,又能提升长时间运行的性能

总结:
解释器负责启动时快速执行,JIT 编译器负责优化热点代码。 这就是 Java 既有解释语言的灵活性,又有编译语言的高效性的原因。

1. JVM 运行时数据区(Runtime Data Areas)

1.1. 程序计数器(Program Counter Register)

概述

程序计数器(PC Register)是 JVM 中一个小型的内存区域,它的作用是记录当前线程正在执行的字节码指令地址。

特点

  • 线程私有(每个线程都有一个独立的 PC 寄存器)。
  • 存储字节码指令地址,用于指示当前线程下一条要执行的指令。
  • 执行 Java 方法时,PC 计数器存储正在执行的字节码指令地址。
  • 执行本地方法(Native Method)时,PC 计数器值 undefined(未定义)。
  • 该区域是 JVM 唯一不会发生 OutOfMemoryError 的区域

作用

  • 记录当前线程的执行位置(类似于 CPU 的 PC 寄存器)。
  • 线程切换时,保证线程恢复后能继续执行正确的指令。
  • 实现 Java 代码的多线程并发(JVM 通过线程轮流切换,每个线程都有自己的 PC 寄存器)。

1.2. Java 虚拟机栈(JVM Stack)

概述

Java 虚拟机栈(JVM Stack)用于存储 Java 方法执行时的栈帧(Stack Frame),是 Java 方法执行的基础。

特点

  • 线程私有,每个线程有独立的栈。
  • 栈的大小可以通过 -Xss 参数设置(例如 -Xss1M 设置栈大小为 1MB)。
  • 栈的生命周期与线程相同,线程结束时栈也随之销毁。

栈帧(Stack Frame)的组成

每个栈帧对应一个正在执行的方法,包含:

  • 局部变量表(Local Variable Table)

    • 存放方法中的基本数据类型(int、long、float、double 等)对象引用(reference)
    • 局部变量表的大小在编译期确定,运行时不会改变。
    • longdouble 类型占 两个 存储单元,其它数据类型占 一个 存储单元。
  • 操作数栈(Operand Stack)

    • 作为字节码指令的操作数临时存储区,用于方法调用时传递参数、计算临时结果等。
    • JVM 指令基于栈操作(如 iadd 指令会从操作数栈中弹出两个整数相加后再压入栈中)。
  • 动态链接(Dynamic Linking)

    • 指向方法区中的运行时常量池,用于解析方法引用(即方法调用时如何找到方法地址)。
  • 方法返回地址(Return Address)

    • 记录方法执行完毕后,返回到调用者的位置,以便继续执行。

栈的空间大小

  • 栈的大小由 -Xss 参数控制,通常 默认 1MB,可根据需求调整:
    java -Xss512k MyApplication
    
    • 每个线程的栈大小 512KB
    • 栈过大可能导致 StackOverflowError(递归调用过深)。

为什么栈是从高地址向低地址增长?

  • LIFO 机制(后进先出)

    • 栈用于存储方法调用信息,每次调用新方法时,会创建一个栈帧(Stack Frame),压入栈顶
    • 方法执行完后,栈帧被弹出,栈顶回到上一个方法的栈帧。
  • CPU 设计 & 指针运算优化

    • 许多计算机体系结构(如 x86、ARM)都使用“向下增长”的栈
    • 这允许 ESP(栈指针寄存器)直接递减来分配新的栈帧,提高性能。

可能出现的异常

  • StackOverflowError:递归过深导致栈空间耗尽。
  • OutOfMemoryError:JVM 栈的大小动态扩展失败(一般在线程数量过多时)。

1.3. 本地方法栈(Native Method Stack)

概述

本地方法栈(Native Method Stack)与 JVM 栈类似,但它用于存储 Native 方法的执行信息

特点

  • 线程私有,生命周期与线程相同。
  • 主要用于 JNI(Java Native Interface)调用 C/C++ 代码。
  • 可能出现:
    • StackOverflowError
    • OutOfMemoryError

作用

  • 维护 Native 方法调用时的参数、返回地址、Native 方法执行环境等。
  • 例如,调用 System.arraycopy() 方法时,JVM 需要通过本地方法栈进入 C 代码执行内存拷贝。

1.4. 堆(Heap)

概述

堆(Heap)是 JVM 内存中最大的区域,用于存储所有对象实例

特点

  • 线程共享(所有线程都能访问)。
  • GC(垃圾回收器)管理的区域。
  • 堆的大小可通过 -Xms(初始大小)和 -Xmx(最大大小)参数控制。
  • 可能抛出 OutOfMemoryError: Java heap space

堆的分代

  • 新生代(Young Generation)
    • Eden 区(对象最初分配在这里)。
    • Survivor 0(S0)、Survivor 1(S1)(少量存活对象在两者之间交替存储)。
  • 老年代(Old Generation)
    • 长期存活对象存放这里,GC 频率较低。

垃圾回收

  • Minor GC(新生代 GC):采用复制算法(对象生命周期短,适合快速回收)清理 Eden 和 Survivor 区。
  • Major GC / Full GC:清理整个堆,采用标记-整理算法(对象生命周期长)清理老年代。
  •  Major GC主要针对老年代进行清理,Full GC对整个堆内存(包括年轻代、老年代以及元空间)进行回收。

堆的空间大小

  • 堆的大小可以通过 JVM 参数设置

    • -Xms堆的初始大小(默认通常是 1/64 物理内存)
    • -Xmx堆的最大大小(默认通常是 1/4 物理内存)
    • 例如:
      java -Xms256m -Xmx1024m MyApplication
      
      • 最小堆内存 256MB
      • 最大堆内存 1024MB
  • 默认情况下,堆的大小会随着 GC 调整,但不能超过 -Xmx 设定的上限。

为什么堆是从低地址向高地址增长?

  • 堆是动态分配的,大小不固定

    • JVM 通过 -Xms(最小堆)和 -Xmx(最大堆)设置堆的大小。
    • 由于对象的创建是动态的,JVM 需要扩展堆的大小,通常向高地址扩展。
  • 操作系统内存管理

    • 在 C 语言的 malloc()JVMnew 语义中,分配的堆空间通常从低地址向高地址增长
    • 堆的增长方向使得堆的可扩展性更好,能动态调整大小。

1.5. 方法区(Method Area,又称元空间 Metaspace)

概述

存储类的元数据(方法信息、静态变量、运行时常量池)。关于类的信息存储在这里。

JDK 7 及以前

  • 方法区是堆中的永久代(PermGen)
  • -XX:PermSize-XX:MaxPermSize 限制。

JDK 8 及以后

  • 方法区改为单独的元空间(Metaspace)
  • 使用本地内存,不再受堆大小限制。
  • -XX:MetaspaceSize 控制其大小。

1.6 运行时常量池(在方法区中)字符串常量池(在堆中) 

常量池类型 存储位置(JDK 6 及以前) 存储位置(JDK 7+) 存储内容 作用
类文件常量池(Class File Constant Pool) .class 文件 .class 文件 字面量(数值、字符串)、符号引用 编译时生成,在运行时加载到运行时常量池
运行时常量池(Runtime Constant Pool) 方法区(永久代 PermGen) 方法区(元空间 Metaspace) 从类文件加载的常量(字面量、符号引用)、运行时生成的常量(String.intern() 动态解析符号引用、存储运行时常量
字符串常量池(String Pool) 方法区(永久代 PermGen) 堆(Heap) String 字面量、intern() 方法存入的字符串 优化字符串存储,减少内存占用
常量类型 说明
字面量 编译时生成的常量,如 final 修饰的常量、字符串字面量、数值(int、float、double、long)等。
符号引用 类名、字段名、方法名的符号引用(未解析为具体地址),用于支持动态链接。
方法引用 方法的符号引用,如方法的名称、描述符等。

1.7 总结

内存区域 线程私有/共享 主要作用 可能抛出的异常
程序计数器 线程私有 记录当前线程执行的字节码地址
JVM 栈 线程私有 存储局部变量表、操作数栈 StackOverflowErrorOutOfMemoryError
本地方法栈 线程私有 执行 Native 方法 StackOverflowErrorOutOfMemoryError
线程共享 存储对象实例 OutOfMemoryError: Java heap space
方法区(元空间) 线程共享 存储类信息、静态变量 OutOfMemoryError: Metaspace
直接内存 线程共享 用于高效 I/O(如 NIO) OutOfMemoryError: Direct Buffer Memory

2. 类加载机制

2.1 类加载器

(1)启动类加载器(Bootstrap ClassLoader)

  • 负责加载JDK 核心类库rt.jarcharsets.jar 等)。
  • C++ 代码实现,无法直接获取其实例(即 null)。
  • 只能加载JDK 自带的核心类库,无法加载用户定义的 .class 文件。

主要加载的类包括:

  • java.lang.*(如 StringIntegerSystem
  • java.util.*(如 ArrayListHashMap
  • java.io.*(如 FileInputStream
  • java.nio.*java.net.*

(2)扩展类加载器(ExtClassLoader)

  • 负责加载 lib/ext/ 目录下的扩展类库(如 javax.crypto.*)。
  • Java 代码实现,可通过 ClassLoader.getSystemClassLoader().getParent() 获取。
  • JDK 9 以后,扩展类加载器被移除,改为平台类加载器(PlatformClassLoader)

主要加载的类包括:

  • javax.crypto.*(加密库)
  • javax.sound.*(声音处理库)
  • javax.imageio.*(图像处理库)

(3)应用类加载器(AppClassLoader/ SystemClassLoader)

  • 默认的类加载器:如果你没有手动指定类加载器,默认由它加载。
  • 可以通过 ClassLoader.getSystemClassLoader() 获取到它
  • 支持动态加载 JAR 包:当你添加 JAR 依赖(如 Spring Boot 依赖的 JAR),它会动态加载这些类。

 主要加载的类包括:

  • com.example.MyClass
  • org.springframework.*
  • 任何放在 classpath 下的 .class 文件

(4)自定义类加载器 

  • 默认类加载器仅加载 classpath 下的类,如果需要从网络、数据库、加密文件中加载 .class 文件,必须使用自定义类加载器。 
  • 默认的 AppClassLoader 共享 classpath,如果多个模块的类存在相同的包名,可能会发生类冲突
  • 防止 Java 反编译:Java .class 文件容易被反编译,我们可以加密 .class 文件,并使用自定义类加载器在运行时解密。

如何自定义类加载器?

  • 方式 1:继承 ClassLoader。Java 提供了 ClassLoader 抽象类,允许我们创建自己的类加载器。

  • 方式 2:继承 URLClassLoader。如果 .class 文件存放在 JAR 或远程服务器上,我们可以继承 URLClassLoader 来动态加载类。

2.2 双亲委派机制(Parent Delegation Model)

1. 什么是双亲委派机制?

双亲委派机制 是指 类加载器在加载一个类时,先委托其父类加载器加载,只有当父类加载器无法加载该类时,子类加载器才会尝试自己加载

2. 双亲委派机制的工作流程

  1. 当一个 ClassLoader 需要加载一个类时,它不会自己直接加载,而是先委托给父类加载器
  2. 父类加载器递归向上委托,最终到 Bootstrap ClassLoader(顶层)。
  3. 如果 Bootstrap ClassLoader 无法加载该类(即不是核心类库),那么父类加载器会逐层返回,直到应用类加载器(AppClassLoader)。
  4. 如果所有的父类加载器都无法加载该类,那么当前类加载器才会自己尝试加载

3. 为什么要使用双亲委派机制?

保证 Java 运行时的安全性

  • 避免核心 API 被篡改java.lang.String 始终由 Bootstrap ClassLoader 加载)。
  • 防止类的重复加载类冲突

提高类加载的效率

  • 先尝试加载已经加载过的类,避免重复加载。

2.3 类加载过程

类加载的五个阶段

阶段 说明
加载(Loading) 读取 .class 文件,将字节码转换为 Class 对象。
验证(Verification) 检查字节码是否合法,防止恶意代码执行。
准备(Preparation) 分配静态变量的内存,初始化默认值(不赋具体值)。
解析(Resolution) 将符号引用转换为直接引用(方法地址、变量地址)。
初始化(Initialization) 执行 static 代码块,赋值静态变量。

2.3.1. 加载(Loading)

步骤:

  • 读取 .class 文件(从硬盘、网络、JAR 包等加载类的字节码)。
  • 转换字节码为 JVM 识别的 Class 对象,存入方法区。
  • 在堆(Heap)中创建 Class 对象,表示该类的运行时信息。

示例

Class<?> clazz = Class.forName("java.lang.String");
  • Class.forName() 方法会触发类加载

2.3.2. 连接(Linking)

(1)验证(Verification)

  • 检查 .class 文件格式是否正确,防止恶意代码(字节码验证)。
  • 例如,检查字节码指令是否合法,是否会破坏 JVM 运行。

(2)准备(Preparation)

  • 为类的静态变量 分配内存,并设置默认值(如 int 变量默认值为 0)。
  • 例如:
public static int a = 10;  // 在准备阶段 a=0,在初始化阶段才变成10

(3)解析(Resolution)

  • 符号引用(如 java.lang.String)转换为直接引用(JVM 内存地址)。

2.3.3. 初始化(Initialization)

  • 执行 <clinit>() 静态代码块,初始化静态变量(准备阶段是为静态变量赋默认值,而这里是要设置你所定义的值)。
  • 只有第一次使用该类时才执行初始化,确保类只初始化一次。

示例

class Example {
    static {
        System.out.println("Static block executed");
    }

    public static int value = 10;
}

public class Test {
    public static void main(String[] args) {
        System.out.println(Example.value);
    }
}

3. Java 对象的创建过程

当 Java 代码执行 new 关键字创建对象时,JVM 需要完成以下步骤:

步骤 1:检查类是否已被加载

  • JVM 先检查目标类的元信息是否已加载到方法区(元空间 Metaspace)
    • 如果 类未加载,JVM 先通过 类加载器(ClassLoader) 加载 .class 文件,并完成 类加载、验证、准备、解析、初始化 过程(即 类的五个生命周期阶段)。
    • 如果 类已加载,跳过此步骤。

步骤 2:为对象分配内存

JVM 在堆(Heap)中为新对象分配内存,分配策略取决于 内存是否连续

  • 指针碰撞(Bump the Pointer)(内存连续时):

    • 堆内存按顺序分配,JVM 仅需将指针移动到新的可用地址
    • 适用于 使用 GC 压缩后 的堆。
  • 空闲列表(Free List)(内存不连续时):

    • 维护空闲内存块列表,找到合适的内存块进行分配。
    • 适用于 堆内存碎片较多 的情况。
  • 线程私有分配缓冲区(TLAB, Thread Local Allocation Buffer)

    • JVM 允许每个线程在堆中新建私有缓存区域,提高对象分配效率,减少同步锁竞争。

步骤 3:初始化对象的默认值

  • JVM 将对象字段初始化为默认值(不调用构造方法)。
class Example {
    int x;    // 默认值 0
    boolean y; // 默认值 false
    String s; // 默认值 null
}

这里需要注意,类加载过程中也会有对变量赋默认值的操作,但二者是不同的,类加载过程中的是为类的静态变量赋默认值,而这里是对对象的属性进行赋默认值。 

步骤 4:设置对象的元数据

  • Mark Word(标记字段)

    • 存储 哈希码、GC 状态、锁信息
    • 在对象加锁、GC 过程中会被修改。
  • Class Pointer(类指针)

    • 指向对象所属的类元信息(方法区中的 Class 对象)。
    • 通过此指针可以找到对象的类型信息

步骤 5:执行构造方法

JVM 调用 构造方法 <init>(),执行初始化逻辑:

class Example {
    int num;
    Example() {
        num = 10;
        System.out.println("Constructor executed!");
    }
}

public class Test {
    public static void main(String[] args) {
        Example obj = new Example();
    }
}
  • JVM 调用构造方法num = 10

这里需要注意,类加载过程中的赋值操作与这里不同,类加载过程中只是单纯为类的静态变量赋值,而这里是调用构造函数对对象的属性进行赋值。


4. Java 对象的内存分配

4.1 对象的内存结构

Java 采用 堆 + 栈 的模式进行对象的内存管理。

存储位置 存储内容
堆(Heap) 对象本身(实例变量、数组)
栈(Stack) 对象引用(局部变量表)
方法区(Method Area) 类的元信息(方法、静态变量)
方法区:
A 类的静态变量 b = 20
A 类的方法信息

栈:
obj1 -> 指向堆中的 A 对象
obj2 -> 指向另一个 A 对象

堆:
obj1 的实例变量 a = 10
obj2 的实例变量 a = 10

4.2 具体对象内存分配示例 

class Person {
    static String species = "Human";  // 静态变量(方法区)
    String name;   // 实例变量(堆)
    int age;       // 实例变量(堆)

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void sayHello() {
        System.out.println("Hello, my name is " + name);
    }
}

public class MemoryDemo {
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 25);  // 在堆中创建对象
        Person p2 = new Person("Bob", 30);    // 在堆中创建另一个对象

        p1.sayHello();
        p2.sayHello();
    }
}

 直观的内存示意图

方法区(存储类信息 + 静态变量)
-------------------------------------------------
| 类名:Person                                   
| 静态变量:species = "Human"                   
| 方法:sayHello()                              
-------------------------------------------------

栈(存储局部变量/对象引用)
---------------------------------
| main() 方法的栈帧                          
| p1 -> 指向 堆中的对象 1                     
| p2 -> 指向 堆中的对象 2                     
---------------------------------

堆(存储对象实例)
-------------------------------------
| Person 对象 1 (p1)                     
| name = "Alice"                          
| age = 25                                
-------------------------------------
| Person 对象 2 (p2)                     
| name = "Bob"                            
| age = 30                                
-------------------------------------

4.3 对象的内存分配策略

JVM 根据对象的生命周期和大小,决定其分配的位置:

  • 新生代(Young Generation)

    • 大部分对象先在 Eden 区分配,如果对象存活过 GC,则进入 Survivor 区。
    • 新生代 GC(Minor GC)频繁执行,但速度快。
  • 老年代(Old Generation)

    • 生命周期长的对象 会晋升到老年代(如缓存对象)。
    • 大对象无法放入新生代,直接进入老年代。
  • 栈上分配(逃逸分析)

    • JVM 可能优化对象分配,将短生命周期对象存放在栈上,减少 GC 压力。
    • 需要开启 -XX:+DoEscapeAnalysis

5. JVM 垃圾回收机制(GC) 

5.1 为什么需要垃圾回收?

Java 采用自动内存管理

  • 在 C 语言中,开发者需要手动申请和释放内存(malloc() / free()),容易导致 内存泄漏(Memory Leak)悬空指针(Dangling Pointer)
  • 在 Java 中JVM 通过 GC 自动回收不再使用的对象,避免手动管理内存的复杂性。

解决对象生命周期管理问题

  • Java 采用 堆(Heap)存储对象,但对象的生命周期不同:
    • 短生命周期对象(局部变量、循环创建的对象)
    • 长生命周期对象(缓存、全局对象)
    • 永久对象(static 变量)
  • GC 需要智能回收短生命周期对象,并优化长期存活对象的管理

5.2 判断对象是否需要垃圾回收的两种方法

5.2.1. 引用计数法(已淘汰)

  • 原理:每个对象有一个引用计数器引用 +1,解除引用 -1,当引用计数变为 0,说明对象可被回收。
  • 缺陷:由于无法处理循环引用(两个对象相互引用,但不再使用)的问题,现已淘汰。

5.2.2 可达性分析法

基本原理

可达性分析法是基于图遍历(Graph Traversal)的方式进行垃圾对象检测:

  1. GC Roots(垃圾回收根对象) 作为起点(根节点)。
  2. 从 GC Roots 开始遍历所有可以访问到的对象,标记为存活。
  3. 未被遍历到的对象 被认为是不可达(Garbage),可以被回收。

什么是 GC Roots(垃圾回收根对象) / GC Roots 的来源

在可达性分析中,JVM 会选择一组特殊的对象作为根对象(GC Roots),从这些根开始查找所有可达对象

GC Roots 类型 存储位置 示例
栈帧中的局部变量 栈(Stack) 方法内的局部变量 Object obj = new Object();
静态变量(Static) 方法区(Metaspace) static Object obj = new Object();
常量池中的引用 方法区(Metaspace) String s = "Hello";
JNI(Native 方法)引用的对象 本地方法栈(Native Stack) 通过 JNI 访问的 Java 对象
线程对象 线程管理区 运行中的线程对象 Thread.currentThread()

5.3 垃圾回收算法

JVM 采用不同的 GC 算法来优化垃圾回收,主要包括:

GC 算法 原理 优缺点
标记-清除(Mark-Sweep) 标记存活对象 → 清除未标记对象 产生内存碎片,影响分配效率
复制(Copying) 复制存活对象到新区域,清空旧区域 内存利用率低(50% 内存浪费)
标记-整理(Mark-Compact) 标记存活对象 → 移动对象(向一端移动) → 回收未使用空间 解决碎片问题,性能较高
分代回收(Generational GC) 新生代 采用复制算法老年代 采用标记整理算法 适用于大规模应用
  • 新生代(Young Generation)

    • 采用复制算法(对象生命周期短,适合快速回收)。
    • 包括 Eden、Survivor 0、Survivor 1
    • Minor GC 发生在新生代,速度快。
  • 老年代(Old Generation)

    • 采用标记-整理算法(对象生命周期长)。
    • Major GC / Full GC 发生在老年代,通常比 Minor GC 慢。

5.4 常见垃圾回收器

GC 新生代算法 老年代算法 适用场景
Serial GC 复制 标记整理 单线程,适用于小型应用
Parallel GC 复制 标记整理 多线程高吞吐量
CMS GC 标记清除 标记清除 低延迟,适用于 Web 应用
G1 GC Region 化管理 Region 化管理 大内存应用,JDK 9+ 推荐
ZGC 并发 并发 超低延迟,JDK 11+

STW(Stop-The-World)的概念

STW(Stop-The-World) 是指 JVM 在执行 GC 时,会暂停所有应用线程,以便垃圾回收器安全地回收对象。这意味着:

  • 所有应用线程停止执行,等待 GC 完成后再继续运行。
  • STW 发生时,Java 代码暂停执行,系统响应变慢,可能导致卡顿。

5.4.1 CMS GC(Concurrent Mark-Sweep)

CMS(Concurrent Mark-Sweep)是 JDK 1.4 引入的 低延迟 GC,适用于Web 服务器、金融系统等低停顿时间应用

(1)CMS GC 的核心特点

最小化 STW(低延迟),适用于交互式应用。
并发执行 GC,不影响应用线程运行。
"标记-清除" 算法,回收时不会整理堆内存(容易产生内存碎片)。
垃圾碎片问题严重,可能导致 Full GC(STW 变长)。
CPU 资源开销大,GC 线程与应用线程竞争 CPU 资源。


(2)CMS GC 的工作原理

1️⃣ CMS GC 的堆内存结构

  • 采用 "分代回收"(Generational GC)策略
    • 新生代(Young Generation):使用 "复制" 算法进行垃圾回收(Minor GC)。
    • 老年代(Old Generation):使用 "标记-清除" 算法进行垃圾回收(Major GC)。
    • 方法区。

2️⃣ CMS GC 的垃圾回收流程

CMS GC 的核心思想是:并发执行垃圾回收,尽可能减少 STW 时间

这里的并发执行指的是和应用线程并发执行,不用暂停应用线程也能进行垃圾回收。

垃圾回收流程如下:

  1. 初始标记(Initial Mark,STW):标记 GC Roots 直接关联的对象,STW 时间短。

  2. 并发标记(Concurrent Marking):应用程序运行的同时,遍历对象图,标记可达对象。

  3. 重新标记(Remark,STW):由于并发标记时,可能有对象状态发生变化,因此需要再次 STW,重新标记存活对象

  4. 并发清除(Concurrent Sweep):应用程序运行的同时,并发清除垃圾对象,释放内存。

  5. Full GC(当 CMS GC 失败时触发,STW 时间长):由于 CMS 不进行内存整理(Compaction),可能导致碎片化问题。当大对象无法分配到连续空间时,触发 Full GC(可能造成严重 STW(通常几百毫秒到几秒))。


(3)CMS GC 的垃圾碎片问题

为什么 CMS GC 会产生垃圾碎片?

  • CMS 采用"标记-清除"算法,不进行内存整理,导致老年代中存在很多不连续的空闲内存(碎片)。
  • 大对象需要分配时,如果没有足够的连续空间,JVM 可能触发 Full GC 进行内存整理(STW 时间长)。

解决方案:

  • 参数优化

    • -XX:+UseCMSCompactAtFullCollection(在 Full GC 后进行整理)。
    • -XX:CMSFullGCsBeforeCompaction=3(每 3 次 Full GC 后执行一次内存整理)。
  • 改用 G1 GC

    • G1 GC 通过 Region 化管理和混合回收,可以避免碎片化问题

5.4.2 G1 GC(Garbage First)

G1 GC(Garbage First GC)是 JDK 7u4 引入,并在 JDK 9 成为 默认 GC
适用于大内存应用(4GB 以上),相比 CMS GC 减少了碎片化问题,提供更可预测的 GC 停顿时间

(1)G1 GC 的核心特点

Region(分区化管理),动态调整新生代和老年代比例。
可预测的 GC 停顿时间(-XX:MaxGCPauseMillis)。
并发执行回收,减少 STW 停顿时间。
自动整理内存(不会产生碎片化问题)。
相比 CMS,CPU 开销更高。
吞吐量略低于 Parallel GC。


(2)G1 GC 的工作原理

1️⃣ G1 GC 的堆内存结构

  • 不同于 CMS GC 的 "分代管理",G1 GC 采用 "Region(分区)管理"
    • Eden(新生代)
    • Survivor(新生代)
    • Old(老年代)
    • Humongous(存放大对象)
    • Free(未使用的 Region)

2️⃣ G1 GC 的垃圾回收流程

  1. 年轻代 GC(Minor GC,STW):复制存活对象到 Survivor 或老年代,清空 Eden。

  2. 并发标记(Concurrent Marking,避免 STW):识别老年代中垃圾最多的 Region,准备回收。

  3. 混合回收(Mixed GC,减少 Full GC):同时回收年轻代和部分老年代,减少老年代空间不足问题。

  4. Full GC(极少发生):只有当 G1 GC 失败时才会触发 Full GC。


(3)G1 GC 避免垃圾碎片
  • 通过 Region 化管理对象,回收垃圾最多的 Region,避免碎片化问题。
  • 当需要整理时,可以逐步迁移存活对象,减少 STW 时间

5.4.3 CMS GC vs G1 GC 对比

JDK 9默认使用G1 GC

对比项 CMS GC G1 GC
适用场景 低延迟应用(Web 服务器) 大内存应用(4GB+)
回收策略 标记-清除,不整理内存 Region 化管理,减少碎片
STW 时间 可能较长(Full GC) 可预测(-XX:MaxGCPauseMillis
碎片化问题 可能严重,影响 Full GC 频率 通过 Region 避免碎片
吞吐量 较高,但 Full GC 影响较大 较稳定,整体吞吐量较优
Full GC 触发 碎片化严重时容易触发 极少发生

5.5 GC不仅会对堆进行GC还会对方法区GC

  • 堆(Heap)GC:

    • 主要回收 Java 对象(实例)。
    • 频繁触发 GC(Minor GC、Major GC、Full GC)
  • 方法区(Method Area)GC:

    • 主要回收 类的元数据、常量池、JIT 编译后的代码缓存
    • 较少触发 GC(通常在类卸载时进行)。

方法区 GC 主要回收哪些内容?

(1)废弃的常量

  • 字符串常量池(String Pool)
  • 运行时常量池中的数值、类名、方法名、字段名

(2)无用的类

类的卸载(Class Unloading) 发生在以下条件都满足时:

  1. 该类的所有实例都被 GC 回收(即堆中不再存在该类的对象)。
  2. 加载该类的 ClassLoader 本身已经被回收
  3. 该类没有被静态变量(static)引用

注意:JVM 默认不会主动卸载类,通常只有在动态加载和卸载 ClassLoader 时才会发生(如 Web 服务器动态部署)


(3)JIT 编译缓存

JVM 的 JIT 编译器(Just-In-Time Compiler) 会将热点代码编译成本地机器码并缓存到 代码缓存(Code Cache)。当缓存空间不足时,JVM 可能会触发 GC 清除不常用的编译代码。


方法区 GC 触发时机

  • 动态代理、反射、CGLIB 生成的类较多时(如 Spring 框架)。
  • 大量的字符串常量、方法名、字段名 存入常量池。
  • 频繁卸载 ClassLoader(如 Web 服务器重新加载 WAR 包)。
  • JIT 编译器缓存过多代码(如长时间运行的大型 Java 程序)。

6. JVM 内存泄漏

6.1 什么是 JVM 内存泄漏?

在 JVM 中,内存泄漏(Memory Leak) 指的是程序运行过程中,不再使用的对象仍然被引用,导致 GC 无法回收它们,进而导致堆内存(Heap)不断膨胀,最终可能触发 OutOfMemoryError(OOM)

尽管 Java 有 垃圾回收机制(GC),但如果对象仍然被可达引用(Reachable),即使程序不再使用它们,GC 也不会回收这些对象。这就形成了内存泄漏


6.2 内存泄漏的表现

1. 堆内存持续增长

  • JVM 运行时间越长,内存占用越高,甚至 OOM
  • GC 频率升高,但老年代(Old Generation)对象无法释放

2. 应用性能下降

  • 内存占用增加,导致频繁 GC
  • 应用响应时间变慢,甚至崩溃

3. OutOfMemoryError: Java heap space

  • 堆空间耗尽,程序崩溃
  • 发生在 大量对象未释放大对象占用过多内存 的情况下。

6.3 JVM 内存泄漏的常见原因

Java 内存泄漏的根本原因是 无用对象仍然被引用,GC 无法回收它们。常见的几种情况如下:


6.3.1 静态集合类 / 静态变量导致的内存泄漏

原因

  • 静态变量(static)属于类,生命周期与 JVM 一致,不会被 GC 自动回收。
  • 若静态变量持有大量对象引用,即使对象本身不再使用,也不会被回收,从而造成 堆积

示例

import java.util.*;

public class StaticCollectionLeak {
    private static final List<byte[]> memoryLeakList = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            byte[] largeObject = new byte[1024 * 1024]; // 1MB
            memoryLeakList.add(largeObject);
        }
    }
}

问题memoryLeakListstatic,导致所有对象即使不再需要,仍然不会被 GC 回收。 

解决方案

  • 避免静态集合存储大量对象
  • 使用 WeakReferenceSoftReference
  • 手动调用集合的clear()方法清理

6.3.2 监听器 & 观察者模式导致的泄漏

原因

  • 监听器(Listener)或观察者模式(Observer)会使对象之间形成强引用,即使对象不再使用,监听器仍然会保持对它的引用,导致 GC 无法回收。

示例

import java.util.ArrayList;
import java.util.List;

class EventSource {
    private final List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener listener) {
        listeners.add(listener);
    }
}

interface EventListener {
    void onEvent();
}

public class ListenerLeak {
    public static void main(String[] args) {
        EventSource eventSource = new EventSource();
        EventListener listener = () -> System.out.println("Event received!");
        eventSource.addListener(listener);
    }
}

问题

  • listeners 集合会一直持有 EventListener 对象的引用,即使它们不再被使用,导致 GC 不能回收它们。

解决方案

  • 使用 WeakReference 弱引用
private final List<WeakReference<EventListener>> listeners = new ArrayList<>();

6.3.3 线程本地变量(ThreadLocal)泄漏

原因

  • ThreadLocal 绑定的变量存储在线程的 ThreadLocalMap,但如果不手动清理,线程池复用线程时可能会导致数据泄漏。

示例

public class ThreadLocalLeak {
    private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            threadLocal.set(new byte[10 * 1024 * 1024]); // 10MB
        });
        thread.start();
    }
}

问题:线程执行完后,ThreadLocal 变量没有被清理,导致占用 10MB 内存无法释放。

解决方案

  • finally 语句中手动清理 ThreadLocal 变量
try {
    threadLocal.set(new byte[10 * 1024 * 1024]);
} finally {
    threadLocal.remove();
}

6.3.4 内部类 & 匿名类导致的泄漏

原因

  • 非静态内部类匿名类 持有对外部类的隐式引用,如果外部类仍然存活,内部类不会被 GC 回收。

示例

public class InnerClassLeak {
    private byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB

    public void createAnonymousClass() {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println(largeArray.length);
            }
        };
        new Thread(task).start();
    }

    public static void main(String[] args) {
        new InnerClassLeak().createAnonymousClass();
    }
}

问题

task 作为匿名内部类会持有 InnerClassLeak 的引用?

在 Java 中,非静态内部类和匿名类会隐式地持有外部类实例的引用,这就是 隐式引用(Implicit Reference)。这意味着:

  • 匿名内部类 taskrun() 方法内部,访问了 largeArrayInnerClassLeak 的成员变量)。
  • 由于 largeArrayInnerClassLeak 的实例变量,所以 匿名类 task 需要持有 InnerClassLeak 的引用,才能访问 largeArray
  • 这种 外部类的隐式引用 可能会导致 InnerClassLeak 对象无法被 GC 回收,从而导致内存泄漏

解决方案

  • 使用静态内部类,避免隐式引用
public class InnerClassLeak {
    private byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB

    public void createStaticInnerClass() {
        new Thread(new StaticTask(largeArray)).start(); // 直接传递 largeArray
    }

    // 变为静态内部类
    private static class StaticTask implements Runnable {
        private final byte[] arrayRef;

        StaticTask(byte[] arrayRef) {
            this.arrayRef = arrayRef;
        }

        @Override
        public void run() {
            System.out.println(arrayRef.length);  // 访问传入的参数,而不是外部类变量
        }
    }

    public static void main(String[] args) {
        new InnerClassLeak().createStaticInnerClass();
    }
}

为什么静态内部类可以避免内存泄漏?

  • 静态内部类不会持有外部类 InnerClassLeak 的隐式引用

    • StaticTask 使用 static 修饰后,就不再与 InnerClassLeak 绑定,它变成了独立的类
    • StaticTask 不会自动持有 InnerClassLeak 的实例引用
  • 显式传递 largeArray,避免隐式引用

    new Thread(new StaticTask(largeArray)).start();
    
    • 我们显式地将 largeArray 传递给 StaticTask 构造方法,这样 StaticTask 只持有 largeArray 的引用,而不是 InnerClassLeak 的整个实例。
    • 即使 InnerClassLeak 被 GC 回收,StaticTask 仍然可以正常运行
方案 是否会导致内存泄漏? 原因
匿名内部类 可能会泄漏 持有外部类 InnerClassLeak 的隐式引用,导致 largeArray 无法回收
静态内部类 不会泄漏 不再持有 InnerClassLeak 的引用,只持有 largeArray,可以安全回收

最佳实践

  • 避免匿名类访问外部类的实例变量,否则可能会无意间创建隐式引用,导致对象不能被 GC
  • 如果必须使用内部类,建议使用 static 内部类,并通过构造方法传递所需数据,避免隐式引用外部类。

6.3.5 数据库连接 / IO 资源未关闭

原因

  • 数据库连接(JDBC)、文件流、Socket 连接未正确关闭,导致资源泄漏,最终耗尽可用内存。
  • GC 无法自动回收这些外部资源

示例

public class ConnectionLeak {
    public static void main(String[] args) throws Exception {
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "password");
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT * FROM users");

        while (rs.next()) {
            System.out.println(rs.getString("name"));
        }
        // 🚨 资源未关闭,泄漏
    }
}

解决方案

  • 使用 try-with-resources
    try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "password");
         Statement stmt = conn.createStatement();
         ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
        
        while (rs.next()) {
            System.out.println(rs.getString("name"));
        }
    }
    
    try-with-resources 确保所有资源自动关闭!

6.4 如何检测 JVM 内存泄漏?

(1) 使用 jmap 查看堆内存

jmap -histo:live <pid>
  • 分析大对象占用情况,找出无法被回收的对象。

(2)使用 jconsole 监控 JVM 内存

  • 实时观察堆内存使用趋势,发现是否存在不断增长且无法释放的对象

(3)使用 VisualVM 进行 Heap Dump 分析

jmap -dump:format=b,file=heapdump.hprof <pid>
  • 导入 VisualVM,分析对象引用关系,找出无法被 GC 回收的对象

(4)使用 MAT(Memory Analyzer Tool)

  • MAT(Eclipse Memory Analyzer) 可以分析 .hprof 文件,找出GC Root 保持的对象,定位泄漏点。

7. JVM 内存溢出(OutOfMemoryError, OOM) 

7.1. 什么是 JVM 内存溢出?

JVM 内存溢出(OutOfMemoryError,简称 OOM) 是指 JVM 试图分配内存,但由于内存不足或内存无法回收,导致 JVM 运行失败并抛出 java.lang.OutOfMemoryError 异常

JVM 主要的内存区域

  • 堆(Heap):存储对象实例。
  • 栈(Stack):存储方法调用帧、局部变量。
  • 方法区(Method Area)(JDK 8+ 称为 Metaspace):存储类元数据、方法、静态变量等。
  • 直接内存(Direct Memory):NIO 直接分配的操作系统内存。

7.2. 常见的 OOM 类型

JVM 内存溢出 通常发生在以下几种区域

OOM 错误类型 发生区域 主要原因
java.lang.OutOfMemoryError: Java heap space 堆(Heap) - 对象过多,无法回收,导致堆空间耗尽(如集合无限增长,缓存未清理)。
- 单个大对象分配失败(如一次性分配超大数组)。
java.lang.OutOfMemoryError: GC overhead limit exceeded 堆(Heap) - GC 频繁运行但每次回收内存极少,导致 CPU 资源被大量消耗。
java.lang.OutOfMemoryError: Metaspace 方法区(Metaspace) - 类加载过多(如 Spring 频繁创建代理类,动态类加载)。
- 类无法卸载(如自定义 ClassLoader 造成内存泄漏)。
java.lang.StackOverflowError 栈(Stack) - 方法递归过深,导致栈帧溢出(如无限递归)。
- 每个线程栈空间不足,导致溢出。
java.lang.OutOfMemoryError: unable to create new native thread 本地内存(OS 线程数) - 线程创建过多,超出 OS 允许的最大线程数(如无限创建 new Thread())。
- 每个线程栈大小过大,导致系统无法分配新线程。
java.lang.OutOfMemoryError: Direct buffer memory 直接内存(Direct Memory) - NIO 直接缓冲区 (ByteBuffer.allocateDirect()) 分配过多,超过 MaxDirectMemorySize 限制。
java.lang.OutOfMemoryError: Swap space 操作系统 Swap 交换空间 - 应用分配内存过多,导致 OS 交换空间耗尽(一般在物理内存不足时发生)。
java.lang.OutOfMemoryError: Requested array size exceeds VM limit 堆(Heap) - 试图分配超大数组(如 new int[Integer.MAX_VALUE])。
java.lang.OutOfMemoryError: Compressed class space 方法区(Metaspace, Class Space) - JVM 运行时加载类过多,超出 CompressedClassSpaceSize 限制。

7.3 各种 JVM 内存溢出情况

7.3.1. 堆内存溢出(java.lang.OutOfMemoryError: Java heap space)

原因

  • 创建对象过多,堆空间不断增长,无法回收(如:缓存未清理、集合不断增长)。
  • 单个大对象分配失败(如:一次性分配一个超大数组)。
  • 内存泄漏,无用对象仍然被引用,GC 无法清理。

示例

import java.util.ArrayList;
import java.util.List;

public class HeapOOM {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[10 * 1024 * 1024]); // 每次分配 10MB
        }
    }
}

解决方案

增大堆空间(适用于对象确实需要更多内存的情况):

java -Xms2g -Xmx4g HeapOOM

优化 GC 策略

java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 HeapOOM

检测内存泄漏

  • 使用 jmap
jmap -histo:live <pid>
  • 使用 Heap Dump
jmap -dump:format=b,file=heapdump.hprof <pid>
  • 使用 VisualVMMAT(Memory Analyzer Tool)分析 heapdump.hprof

7.3.2. 栈内存溢出(StackOverflowError 或 Stack Space OOM)

原因

  • 递归调用过深,导致栈帧不断压入,最终超过栈空间大小。
  • 创建大量线程,导致 JVM 线程栈空间耗尽。

示例 1:递归调用导致 StackOverflowError

public class StackOverflowDemo {
    public void recursiveMethod() {
        recursiveMethod(); // 无限递归
    }

    public static void main(String[] args) {
        new StackOverflowDemo().recursiveMethod();
    }
}

示例 2:创建大量线程导致 OutOfMemoryError: Unable to create new native thread

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

public class ThreadOOM {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(1000);
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executor.execute(() -> {
                while (true) {}  // 每个线程执行无限循环
            });
        }
    }
}

解决方案

减少递归深度增大栈空间

java -Xss1m StackOverflowDemo

控制线程池大小

ExecutorService executor = Executors.newFixedThreadPool(100);

7.3.3. 方法区/元空间溢出(Metaspace OOM)

原因

  • 大量动态生成的类(如:大量使用 CGLIBJavassist 动态代理)。
  • Spring Boot 等框架频繁加载新类,导致 Metaspace 过满。
  • 应用长时间运行,但类卸载不及时,导致 Metaspace 持续增长。

示例

import javassist.ClassPool;

public class MetaspaceOOM {
    public static void main(String[] args) throws Exception {
        ClassPool classPool = ClassPool.getDefault();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            classPool.makeClass("com.example.GeneratedClass" + i).toClass();
        }
    }
}

解决方案

增加 Metaspace 大小

java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m MetaspaceOOM

减少动态生成的类


7.3.4. GC 过载导致 OOM(java.lang.OutOfMemoryError: GC Overhead limit exceeded)

原因

  • GC 运行时间过长,超过 98% CPU,但回收的内存不足 2%,JVM 触发此 OOM 保护机制。
  • 堆内存不足,导致 GC 频繁执行,但对象回收效果不佳。

示例

import java.util.HashMap;
import java.util.Map;

public class GCOverheadOOM {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        int i = 0;
        while (true) {
            map.put(i, "OOM Test " + i++); // 不断填充 HashMap
        }
    }
}

解决方案

增大堆空间,减少 GC 触发

java -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 GCOverheadOOM

使用 -XX:-UseGCOverheadLimit 关闭 GC 限制

java -Xmx4g -XX:-UseGCOverheadLimit GCOverheadOOM

7.3.5. 直接内存溢出(Direct Buffer Memory OOM)

原因

  • NIO ByteBuffer 分配过多,导致 Direct Memory 耗尽。
  • JVM 直接内存上限太低,无法满足 ByteBuffer.allocateDirect() 分配请求。

示例

import java.nio.ByteBuffer;

public class DirectMemoryOOM {
    public static void main(String[] args) {
        while (true) {
            ByteBuffer.allocateDirect(1024 * 1024); // 每次申请 1MB 直接内存
        }
    }
}

解决方案

增大直接内存

java -XX:MaxDirectMemorySize=512m DirectMemoryOOM

避免无限制分配

ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
buffer.clear(); // 复用 Buffer,避免反复分配

8. JVM 常见参数及其作用

在 JVM 运行 Java 应用时,我们可以使用 JVM 参数 来控制内存分配、垃圾回收(GC)策略、性能优化等。本文将详细介绍 JVM 常见参数的分类、作用、以及如何设置这些参数。


8.1 JVM 参数设置方法

在生产环境中,JVM 参数通常通过以下方式进行设置:

(1)直接通过 java 命令行设置

适用于 独立 Java 应用、测试环境,例如:

java -Xms2g -Xmx4g -XX:+UseG1GC -jar myapp.jar

(2)在 JAVA_OPTSJAVA_TOOL_OPTIONS 环境变量中设置

适用于 Web 服务器(Tomcat、Spring Boot、微服务)

export JAVA_OPTS="-Xms2g -Xmx4g -XX:+UseG1GC"

(3)在 Docker 容器中设置

容器化部署时,一般通过环境变量 JAVA_OPTS 传递:

docker run -e "JAVA_OPTS=-Xmx4g -XX:+UseG1GC" my-java-app

(4)在 Kubernetes(K8s)中设置

对于 K8s 部署的 Java 应用,可以在 Deployment 配置文件中设置:

env:
  - name: JAVA_OPTS
    value: "-Xms2g -Xmx4g -XX:+UseG1GC"

8.2 常用 JVM 参数及生产环境实践

8.2.1 内存管理参数

作用:控制 JVM 的 堆(Heap)、栈(Stack)、方法区(Metaspace) 大小,影响 GC 频率和性能。

参数 作用 生产环境建议
-Xms<size> 初始堆大小(默认 1/64 物理内存) 设置与 -Xmx 相同,避免运行时扩展
-Xmx<size> 最大堆大小(默认 1/4 物理内存) 根据可用内存大小设置,如 -Xmx4g
-XX:NewRatio=n 新生代:老年代 比例(默认 2,即 1:2 推荐 NewRatio=2,适用于吞吐量型应用
-XX:SurvivorRatio=n Eden:Survivor 比例(默认 8:1:1 保持默认 SurvivorRatio=8
-Xss<size> 每个线程的栈大小(默认 1MB) 适用于高并发应用,如 -Xss512k 减少栈内存占用
-XX:MetaspaceSize=256m JDK 8+ 方法区大小 推荐 256m
-XX:MaxMetaspaceSize=512m 元空间最大值 防止 Metaspace OOM,推荐 512m

8.2.2 GC(垃圾回收)策略

作用:选择合适的 GC 机制,降低 STW(Stop-The-World) 停顿时间,提高吞吐量。

参数 作用 生产环境建议
-XX:+UseSerialGC 单线程 GC(适用于小型应用) 不推荐用于生产环境
-XX:+UseParallelGC 多线程吞吐量 GC 适用于批处理任务、Kafka、Spark
-XX:+UseG1GC 低延迟 GC(默认) 适用于 Web 服务器 / 微服务
-XX:+UseZGC 超低延迟 GC(JDK 11+) 适用于金融、超大堆(TB 级)应用
-XX:MaxGCPauseMillis=200 最大 GC 停顿时间 适用于 G1 GC,控制 STW 时长

8.2.3 JIT(Just-In-Time 编译)优化

作用:优化 JIT 编译,提高 Java 代码执行性能。

参数 作用 生产环境建议
-XX:+TieredCompilation 分层 JIT 编译 默认启用,适用于高并发应用
-XX:+PrintCompilation 打印 JIT 编译方法 调试时启用

8.2.4 线程管理

作用:控制并发线程数,提高 CPU 资源利用率。

参数 作用 生产环境建议
-XX:ParallelGCThreads=<n> GC 并行线程数 推荐 CPU 核心数 / 2
-XX:ConcGCThreads=<n> G1 / ZGC 并发线程数 适用于低延迟应用

8.2.5 监控与日志

作用:启用 GC 日志,监控应用运行状态。

参数 作用 生产环境建议
-XX:+HeapDumpOnOutOfMemoryError OOM 生成 Heap Dump 强烈建议启用
-XX:HeapDumpPath=<path> Heap Dump 存储路径 推荐 /var/logs/heapdump.hprof
-XX:+PrintGCDetails 打印 GC 详情 生产环境推荐
-Xloggc:/var/logs/gc.log GC 日志存储路径 用于 GC 监控分析