一、JVM 概述

1.1 什么是 JVM

JVM(Java Virtual Machine,Java 虚拟机)是运行 Java 字节码的虚拟计算机,它是 Java 语言实现 “一次编写,到处运行”(Write Once, Run Anywhere)核心特性的关键组件。Java 源代码经编译器(javac)编译成字节码文件(.class)后,JVM 负责将字节码解释或编译为本地机器指令,最终在操作系统上执行。

JVM 屏蔽了不同操作系统的底层差异,使得同一份字节码能在 Windows、Linux、macOS 等不同平台上运行,无需针对特定平台修改代码。

1.2 JVM 的核心作用

  • 字节码执行:通过解释器(Interpreter)逐行解释字节码,或通过即时编译器(JIT,Just-In-Time Compiler)将热点代码(频繁执行的代码)编译为本地机器码,提升执行效率。
  • 内存管理:自动负责对象的内存分配(如在堆中创建对象)和垃圾回收(Garbage Collection,GC),避免手动管理内存导致的内存泄漏、野指针等问题。
  • 安全检查:在类加载过程中通过 “类加载器双亲委派模型” 防止恶意类篡改,同时对字节码进行验证(如语法检查、类型安全检查),保障程序运行安全。
  • 线程管理:映射 Java 线程到操作系统原生线程,管理线程的生命周期(创建、运行、阻塞、终止),并通过内置的同步机制(如 synchronized)实现线程安全。

二、JVM 架构组成

JVM 架构主要分为五大核心模块,各模块协同工作完成字节码的执行和资源管理,具体结构如下:

模块 核心功能
类加载子系统 负责加载 .class 文件到内存,完成 “加载、验证、准备、解析、初始化”5 个阶段
运行时数据区 存储程序运行过程中的所有数据(如对象、变量、线程栈等),是 JVM 内存核心
执行引擎 执行字节码,包含解释器、JIT 编译器、垃圾回收器(GC)
本地方法接口(JNI) 衔接 Java 代码与本地 C/C++ 代码,允许调用操作系统原生方法(如文件操作)
本地方法库 存放 JNI 调用的本地方法实现,是操作系统底层功能的 Java 封装

三、类加载子系统

类加载子系统负责将磁盘上的 .class 文件加载到 JVM 运行时数据区,整个过程分为加载、验证、准备、解析、初始化5 个阶段,只有经过这 5 个阶段,类才能被使用。

3.1 类加载的 5 个阶段

  1. 加载(Loading)

    • 核心任务:通过类的全限定名(如 java.lang.String)找到对应的 .class 文件,读取文件字节流,将其转换为运行时数据区中的 “类元数据”(存储在方法区),并在堆中创建一个 java.lang.Class 对象,作为访问类元数据的入口。
    • 示例:通过 ClassLoader.loadClass("com.example.User") 触发加载。
  2. 验证(Verification)

    • 核心任务:确保 .class 文件的字节流符合 JVM 规范,防止恶意字节码(如篡改指令、破坏类型安全)导致 JVM 崩溃。
    • 主要验证内容:文件格式验证(如魔数是否为 0xCAFEBABE)、元数据验证(如类是否有父类、是否实现接口)、字节码验证(如指令合法性、类型转换安全)、符号引用验证(如引用的类 / 方法是否存在)。
  3. 准备(Preparation)

    • 核心任务:为类的静态变量(static 修饰)分配内存,并设置默认初始值(而非代码中定义的初始值)。
    • 示例:public static int num = 10; 在准备阶段,num 会被分配内存并设置为默认值 0,而非 10;10 的赋值会在 “初始化” 阶段完成。
    • 注意:如果静态变量被 final 修饰(如 public static final int NUM = 10;),则在准备阶段会直接设置为 10(编译时已确定值,存储在常量池中)。
  4. 解析(Resolution)

    • 核心任务:将类元数据中的 “符号引用”(如 com.example.User 是字符串形式的引用)转换为 “直接引用”(如内存地址),建立类、方法、字段与内存地址的关联。
    • 触发时机:部分解析可能在 “初始化” 后延迟执行(如动态绑定的方法调用),而非严格在准备阶段后执行。
  5. 初始化(Initialization)

    • 核心任务:执行类的静态代码块(static {})和静态变量的赋值语句,完成类的初始化。这是类加载的最后阶段,只有初始化完成后,类才能被实例化(创建对象)或调用静态方法 / 变量。
    • 初始化触发条件(主动使用场景):
      • 创建类的实例(如 new User());
      • 调用类的静态方法(如 User.staticMethod());
      • 访问类的静态变量(如 int n = User.num);
      • 反射调用(如 Class.forName("com.example.User"));
      • 初始化子类时,父类会先初始化(子类依赖父类)。

3.2 类加载器与双亲委派模型

类加载器(ClassLoader)是实现 “加载” 阶段的核心组件,JVM 内置了 3 种类加载器,且遵循双亲委派模型加载类,确保类加载的安全性和唯一性。

3.2.1 内置类加载器
  • 启动类加载器(Bootstrap ClassLoader)

    • 负责加载 JVM 核心类库(如 java.langjava.util 包下的类),这些类存放在 JRE 的 lib 目录(如 rt.jar)。
    • 特点:由 C/C++ 实现,不属于 Java 类(无法通过 getClass() 获取其对象),返回 null
  • 扩展类加载器(Extension ClassLoader)

    • 负责加载 JRE 扩展目录(lib/ext)下的类库,如 javax 包下的部分类。
    • 实现类:sun.misc.Launcher$ExtClassLoader
  • 应用程序类加载器(Application ClassLoader)

    • 负责加载用户编写的类(即项目 classpath 下的 .class 文件),是默认的类加载器。
    • 实现类:sun.misc.Launcher$AppClassLoader,可通过 ClassLoader.getSystemClassLoader() 获取。

  • 自定义类加载器(Custom ClassLoader)

    • 自定义类加载器是开发者通过继承 java.lang.ClassLoader 类实现的类加载器,用于满足特定场景下的类加载需求。其核心作用是突破 JVM 内置类加载器的固定加载路径限制,灵活加载来自自定义来源的类(打破双亲委派机制)。
    • 特点:自定义类加载器通过继承ClassLoader实现,可从自定义来源(如加密文件、网络)加载类,遵循双亲委派模型,兼具灵活性与安全性,常用于类隔离、加密保护等场景
    • 自定义类加载器deom
public class CustomClassLoader extends ClassLoader {
    // 构造方法:指定父加载器(默认父加载器为应用程序类加载器)
    public CustomClassLoader(ClassLoader parent) {
        super(parent);
    }

    // 重写findClass方法:实现自定义加载逻辑
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 1. 从自定义来源读取类字节码(如加密文件、网络)
            byte[] classData = loadClassBytes(name);
            if (classData == null) {
                throw new ClassNotFoundException("类未找到:" + name);
            }
            // 2. 将字节码转换为Class对象(由JVM实现,不可重写)
            return defineClass(name, classData, 0, classData.length);
        } catch (Exception e) {
            throw new ClassNotFoundException("加载类失败:" + name, e);
        }
    }

    // 自定义方法:从特定来源加载类字节码
    private byte[] loadClassBytes(String className) throws IOException {
        // 示例:从自定义目录加载(实际可扩展为网络、加密文件等)
        String path = className.replace('.', '/') + ".class";
        // 读取字节码(若为加密文件,此处需添加解密逻辑)
        try (InputStream is = new FileInputStream("/custom/classes/" + path)) {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            return bos.toByteArray();
        }
    }
}

3.2.2 双亲委派模型
  • 核心规则:当一个类加载器需要加载类时,会先委托给父加载器加载,只有父加载器无法加载(找不到对应的 .class 文件)时,才由当前加载器自己加载。
  • 委派顺序:应用程序类加载器 → 扩展类加载器 → 启动类加载器(顶层)。
  • 作用:
    1. 防止类重复加载:如 java.lang.String 只能由启动类加载器加载,避免用户自定义同名类篡改核心类。
    2. 保障安全:禁止用户编写恶意类(如 java.lang.String)替换 JVM 核心类,因为父加载器(启动类加载器)已加载核心类,子加载器不会重复加载。

四、运行时数据区

运行时数据区是 JVM 存储程序运行数据的区域,根据 JVM 规范,分为程序计数器、虚拟机栈、本地方法栈、堆、方法区5 个部分,其中前 3 个区域为 “线程私有”(每个线程对应一份),后 2 个区域为 “线程共享”(所有线程共用)。

4.1 线程私有区域(线程创建时初始化,线程销毁时回收)

4.1.1 程序计数器(Program Counter Register)
  • 作用:记录当前线程执行的字节码指令的行号(即下一条要执行的指令地址),确保线程切换后能恢复到正确的执行位置。
  • 特点
    • 是 JVM 中唯一没有 OutOfMemoryError(OOM)异常的区域,因为它只存储指令地址,内存大小固定。
    • 若线程执行的是 Java 方法,计数器存储字节码指令地址;若执行的是本地方法(JNI 方法),计数器值为 undefined
4.1.2 虚拟机栈(VM Stack)
  • 作用:存储线程执行 Java 方法时的 “栈帧”(Stack Frame),每个方法调用对应一个栈帧的入栈,方法执行完成后栈帧出栈。
  • 栈帧组成
    • 局部变量表:存储方法的局部变量(如参数、临时变量),内存大小在编译时确定。
    • 操作数栈:作为方法执行的临时数据栈(如计算 a + b 时,先将 ab 压栈,再执行加法指令)。
    • 动态链接:指向方法区中当前方法的符号引用,用于将符号引用转换为直接引用。
    • 方法返回地址:记录方法执行完成后,返回给调用者的指令地址。
  • 异常
    • StackOverflowError:线程请求的栈深度超过虚拟机栈的最大深度(如递归调用无终止条件)。
    • OutOfMemoryError:虚拟机栈可动态扩展时,扩展内存失败(如创建大量线程导致栈内存耗尽)。
4.1.3 本地方法栈(Native Method Stack)
  • 作用:与虚拟机栈类似,但专门用于存储线程执行本地方法(JNI 方法,如 System.currentTimeMillis())时的栈帧。
  • 异常:与虚拟机栈一致,可能抛出 StackOverflowError 或 OutOfMemoryError

4.2 线程共享区域(JVM 启动时初始化,JVM 关闭时回收)

4.2.1 堆(Heap)

  • 作用:存储 Java 程序中所有对象实例(如 new User() 创建的对象)和数组,是 JVM 内存中最大的区域,也是垃圾回收(GC)的主要区域。
  • 内存划分(逻辑划分,方便 GC)
    • 年轻代(Young Generation):存储新创建的对象,分为 Eden 区、From Survivor 区、To Survivor 区(比例通常为 8:1:1)。
      • Eden 区:新对象优先在 Eden 区分配内存,当 Eden 区满时触发 “Minor GC”(年轻代 GC),存活的对象进入 Survivor 区。
      • Survivor 区:From 和 To 区交替使用,每次 Minor GC 后,存活对象在两个 Survivor 区之间转移,当对象年龄达到阈值(默认 15)时,进入老年代。
    • 老年代(Old Generation):存储存活时间长的对象(如缓存对象、单例对象),当老年代满时触发 “Major GC”(老年代 GC,也叫 Full GC),Full GC 执行效率远低于 Minor GC。
    • 元空间(Metaspace,JDK 8+):替代 JDK 7 及之前的 “永久代”,存储类元数据(如类结构、方法信息、常量池),元空间使用本地内存(而非 JVM 堆内存),默认无大小限制(可通过参数配置)。
  • 异常OutOfMemoryError: Java heap space(堆内存不足,如创建大量对象且无法被 GC 回收)。
4.2.2 方法区(Method Area)
  • 作用:存储线程共享的类元数据(类结构、方法信息、字段信息)、常量池(如字符串常量、数字常量)、静态变量、即时编译器编译后的代码(JIT 缓存)。
  • 演变
    • JDK 7 及之前:方法区实现为 “永久代”(PermGen),属于堆的一部分,有固定大小限制,容易出现 OutOfMemoryError: PermGen space
    • JDK 8 及之后:方法区实现为 “元空间”(Metaspace),使用本地内存,默认无大小限制(可通过 MetaspaceSizeMaxMetaspaceSize 配置),避免了永久代的内存溢出问题。
  • 常量池
    • 运行时常量池:是方法区的一部分,存储类编译后的常量(如 String s = "abc" 中的 "abc" 会存入常量池),支持动态常量(如 String s = new String("abc") 会在堆中创建对象,同时常量池若没有 "abc" 则会添加)。

五、垃圾回收(GC)

垃圾回收(Garbage Collection)是 JVM 自动回收堆中 “无用对象”(不再被引用的对象)内存的过程,无需手动释放内存,是 JVM 内存管理的核心机制。

5.1 垃圾判定算法(如何判断对象是 “垃圾”)

5.1.1 引用计数法(Reference Counting)
  • 原理:为每个对象添加一个 “引用计数器”,当对象被引用时计数器加 1,引用失效时计数器减 1;当计数器为 0 时,判定为垃圾对象。
  • 优点:实现简单,判定效率高。
  • 缺点:无法解决 “循环引用” 问题(如 A 引用 B,B 引用 A,两者均无其他引用,但计数器均为 1,无法被回收),因此 JVM 未采用此算法。
5.1.2 可达性分析算法(Reachability Analysis)
  • 原理:以 “GC Roots”(根对象)为起点,遍历对象引用链(如从 GC Roots 出发能找到的对象为 “可达对象”,找不到的为 “不可达对象”,不可达对象即为垃圾对象)。
  • GC Roots 包括
    • 虚拟机栈中局部变量表引用的对象(如当前执行方法的参数、临时变量);
    • 本地方法栈中引用的对象(JNI 方法引用的对象);
    • 方法区中静态变量引用的对象(如 static User user = new User());
    • 方法区中常量引用的对象(如 final User user = new User());
    • JVM 内部引用(如 Class 对象、异常对象 Throwable)。
  • 优点:解决了循环引用问题,是 JVM 采用的核心垃圾判定算法。

5.2 垃圾回收算法(如何回收垃圾对象)

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

  • 步骤
    1. 标记:通过可达性分析,标记所有可达对象(非垃圾),未标记的为垃圾对象。
    2. 清除:遍历堆内存,回收所有未标记的垃圾对象,释放内存。
  • 优点:实现简单,无需移动对象。
  • 缺点
    • 产生内存碎片(回收后的内存不连续,后续大对象可能无法分配连续内存);
    • 标记和清除过程效率低(需遍历整个堆)。
5.2.2 复制算法(Copying)

  • 步骤
    1. 将堆内存划分为两个大小相等的区域(如 From Survivor 和 To Survivor),每次只使用其中一个区域(From)。
    2. 标记 From 区中的可达对象,将其复制到 To 区,且按顺序排列(避免内存碎片)。
    3. 清空 From 区,交换 From 和 To 的角色,下次 GC 重复上述过程。
  • 优点:无内存碎片,回收效率高(只需复制可达对象)。
  • 缺点:内存利用率低(仅使用一半内存),适合对象存活率低的区域(如年轻代的 Eden 区和 Survivor 区,Minor GC 采用此算法)。
5.2.3 标记 - 整理(压缩)算法(Mark-Compact)

  • 步骤
    1. 标记:与标记 - 清除算法一致,标记所有可达对象。
    2. 整理:将所有可达对象向堆内存的一端移动,按顺序排列,然后清空移动后另一端的垃圾对象内存。
  • 优点:无内存碎片,内存利用率高(无需划分两个区域)。
  • 缺点:增加了对象移动的开销,适合对象存活率高的区域(如老年代,Major GC 采用此算法)。
5.2.4 分代收集算法(Generational Collection)

  • 原理:根据对象存活时间的不同,将堆分为年轻代和老年代,针对不同代的特点采用不同的 GC 算法:
    • 年轻代(对象存活率低):采用复制算法(Minor GC),效率高。
    • 老年代(对象存活率高):采用标记 - 清除算法标记 - 整理算法(Major GC/Full GC),平衡内存碎片和效率。
  • 特点:结合了不同算法的优点,是当前所有 JVM 垃圾收集器的核心思想。

5.3 常见垃圾收集器

垃圾收集器是 GC 算法的具体实现,不同收集器针对 “吞吐量”“延迟”“内存占用” 等指标做了不同优化,适用于不同业务场景(如高并发服务侧重低延迟,后台任务侧重高吞吐量)。以下是 JDK 中主流的垃圾收集器:

收集器类型 适用代际 核心特点 适用场景
Serial 收集器 年轻代 - 单线程收集(GC 时暂停所有用户线程,即 STW,Stop The World)- 实现简单、内存占用少 客户端应用(如桌面程序)、单核 CPU 环境
ParNew 收集器 年轻代 - Serial 的多线程版本(GC 时使用多线程回收,减少 STW 时间)- 可与 CMS 收集器配合使用 服务端应用(如 Tomcat),多核 CPU 环境
Parallel Scavenge 收集器 年轻代 - 多线程收集,侧重 “吞吐量优先”(吞吐量 = 用户线程执行时间 / 总时间)- 可自动调节 GC 参数(如 -XX:+UseAdaptiveSizePolicy) 后台计算任务(如数据批处理),追求高吞吐量
Serial Old 收集器 老年代 - Serial 的老年代版本,单线程收集,基于 “标记 - 整理算法”- STW 时间较长 客户端应用、与 Parallel Scavenge 配合使用
Parallel Old 收集器 老年代 - Parallel Scavenge 的老年代版本,多线程收集,基于 “标记 - 整理算法”- 侧重吞吐量优先 与 Parallel Scavenge 搭配,适用于高吞吐量场景
CMS 收集器(Concurrent Mark Sweep) 老年代 - 基于 “标记 - 清除算法”,追求 “低延迟”(GC 线程与用户线程并发执行,减少 STW 时间)- 执行流程:初始标记 → 并发标记 → 重新标记 → 并发清除 高并发服务(如电商支付、金融交易),侧重低延迟
G1 收集器(Garbage-First) 全代(年轻代 + 老年代) - 基于 “区域划分”(将堆分为多个大小相等的 Region),兼顾吞吐量与低延迟- 优先回收垃圾占比高的 Region(Garbage-First 含义)- 基于 “标记 - 整理算法”,无内存碎片 JDK 9+ 默认收集器,适用于大内存(如 8G+)服务
ZGC 收集器 全代 - 低延迟(STW 时间控制在 10ms 以内),支持 TB 级内存- 基于 “着色指针” 和 “读屏障” 技术,并发执行大部分 GC 步骤 超大内存、低延迟要求极高的场景(如大型分布式系统)
Shenandoah 收集器 全代 - 与 ZGC 目标类似,低延迟、支持大内存- 基于 “连接矩阵” 和 “写屏障” 技术,不依赖特定 JVM 实现 OpenJDK 专属,适用于低延迟、大内存场景

5.4 GC 相关参数配置

实际开发中,需根据业务场景配置 JVM GC 参数,优化性能。以下是常用的核心参数:

参数格式 作用说明 示例
-Xms<size> 设置堆初始内存大小(与 -XX:InitialHeapSize 等价) -Xms2g(初始堆内存 2GB)
-Xmx<size> 设置堆最大内存大小(与 -XX:MaxHeapSize 等价) -Xmx4g(最大堆内存 4GB)
-Xmn<size> 设置年轻代内存大小(Eden + Survivor),剩余为老年代 -Xmn1g(年轻代 1GB,老年代 3GB,若 -Xmx4g
-XX:SurvivorRatio=<ratio> 设置 Eden 区与单个 Survivor 区的比例(默认 8,即 Eden:From:To = 8:1:1) -XX:SurvivorRatio=4(Eden:From:To = 4:1:1)
-XX:MaxTenuringThreshold=<age> 设置对象进入老年代的最大年龄(默认 15,即对象在 Survivor 区存活 15 次 Minor GC 后进入老年代) -XX:MaxTenuringThreshold=10
-XX:+UseG1GC 指定使用 G1 收集器(JDK 9+ 默认,JDK 8 需手动指定) -XX:+UseG1GC
-XX:+UseConcMarkSweepGC 指定使用 CMS 收集器(JDK 9 后已废弃,推荐 G1) -XX:+UseConcMarkSweepGC
-XX:MetaspaceSize=<size> 设置元空间初始大小(JDK 8+,替代永久代) -XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=<size> 设置元空间最大大小(默认无限制,建议配置避免元空间溢出) -XX:MaxMetaspaceSize=256m
-XX:+PrintGCDetails 打印 GC 详细日志(包含回收区域、时间、内存变化等) -XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError 当发生 OOM 时,自动生成堆转储文件(.hprof),用于分析内存泄漏 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./oom.hprof

六、JVM 内存溢出与排查

6.1 常见内存溢出(OOM)类型

JVM 运行时可能因内存分配不足或资源泄漏导致 OOM,不同区域的 OOM 表现不同:

  1. 堆内存溢出(Java heap space)

    • 原因:创建大量对象且无法被 GC 回收(如内存泄漏),或堆内存配置过小。
    • 示例:循环创建对象并放入集合,且集合未被释放:

      java

      运行

      List<Object> list = new ArrayList<>();
      while (true) {
          list.add(new Object()); // 对象持续被 list 引用,无法 GC,最终 OOM
      }
      
    • 排查工具:JVisualVM、MAT(Memory Analyzer Tool),分析堆转储文件(.hprof)定位泄漏对象。
  2. 虚拟机栈 / 本地方法栈溢出(StackOverflowError)

    • 原因:线程栈深度超过 JVM 限制(如递归调用无终止条件),或栈内存配置过小(-Xss 参数)。
    • 示例:无限递归调用:

      java

      运行

      public void infiniteRecursion() {
          infiniteRecursion(); // 每次调用都会创建新栈帧,栈深度不断增加,最终 StackOverflowError
      }
      
    • 排查:检查递归逻辑是否有终止条件,或增大栈内存(如 -Xss256k)。
  3. 元空间溢出(Metaspace OOM)

    • 原因:加载的类过多(如动态生成大量类、依赖包过大),且元空间最大大小未限制。
    • 场景:使用 Spring、MyBatis 等框架时,动态代理生成大量代理类;或频繁使用反射创建类。
    • 排查:配置 -XX:MaxMetaspaceSize 限制元空间大小,或分析类加载情况(如通过 jmap -clstats <pid> 查看类加载统计)。
  4. 直接内存溢出(Direct buffer memory)

    • 原因:使用 NIO 的 DirectByteBuffer 分配直接内存(不受堆内存限制,受物理内存限制),且未及时释放。
    • 示例:频繁分配直接内存且不回收:

      java

      运行

      while (true) {
          ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 分配 1MB 直接内存,不释放
      }
      
    • 排查:通过 -XX:MaxDirectMemorySize 限制直接内存大小,或检查 DirectByteBuffer 的使用是否有泄漏。

6.2 JVM 故障排查工具

日常开发中,需借助工具定位 JVM 性能问题(如 OOM、GC 频繁、CPU 过高),常用工具如下:

工具名称 核心功能 使用场景
jps(Java Process Status) 列出当前运行的 Java 进程(PID、主类名) 快速查找目标 Java 进程的 PID
jstat(Java Statistics Monitoring) 实时监控 JVM 统计信息(如 GC 次数、堆内存使用、类加载数量) 查看 GC 频率、堆内存变化,判断是否有 GC 频繁问题
jmap(Java Memory Map) 生成堆转储文件(.hprof)、查看堆内存使用情况、查看类加载统计 分析内存泄漏、查看对象分布(如 jmap -dump:format=b,file=heap.hprof <pid>
jstack(Java Stack Trace) 生成线程栈快照(查看线程状态、调用栈) 定位线程死锁、CPU 过高的线程(如找到阻塞线程的调用栈)
jhat(Java Heap Analysis Tool) 分析 jmap 生成的堆转储文件,提供 Web 界面查看对象引用关系 简单分析堆内存泄漏(推荐使用更强大的 MAT 工具)
MAT(Memory Analyzer Tool) 专业堆内存分析工具,支持查找内存泄漏、分析对象引用链、计算对象大小 深度分析 OOM 问题,定位泄漏源头
JVisualVM 集成 jps、jstat、jmap、jstack 功能,提供图形化界面,支持监控、分析、 profiling 日常 JVM 监控与简单故障排查(JDK 自带,位于 JDK/bin 目录)

七、JVM 性能优化核心思路

JVM 优化的目标是减少 GC 频率、缩短 STW 时间、避免 OOM、提升应用响应速度,核心思路围绕 “内存配置”“GC 选择”“代码优化” 三个维度展开:

7.1 内存配置优化

  1. 堆内存配置(-Xms 与 -Xmx 一致)

    • 建议将 -Xms(初始堆)与 -Xmx(最大堆)设置为相同值,避免 JVM 频繁调整堆内存大小(减少性能开销)。
    • 堆内存大小建议:根据物理内存确定,如物理内存为 8GB 时,堆内存可设置为 -Xms4g -Xmx4g(避免占用过多物理内存导致系统资源不足)。
  2. 年轻代与老年代比例

    • 年轻代(-Xmn)建议占堆内存的 1/3 ~ 1/2:年轻代过小会导致 Minor GC 频繁,过大则会减少老年代内存,可能导致 Major GC 频繁。
    • 示例:堆内存为 4GB 时,年轻代可设置为 -Xmn1.5g(老年代 2.5GB)。
  3. 元空间配置

    • 必须配置 -XX:MaxMetaspaceSize(如 -XX:MaxMetaspaceSize=256m),避免元空间无限膨胀导致 OOM。
    • 初始元空间(-XX:MetaspaceSize)建议设置为默认值或根据依赖包大小调整(如 128m)。

7.2 GC 收集器选择

  1. 低延迟场景(如电商、金融)

    • 优先选择 G1 收集器(JDK 9+ 默认),或 ZGC/Shenandoah(超大内存场景),通过 -XX:+UseG1GC 启用。
    • 配置 G1 相关参数:-XX:MaxGCPauseMillis=200(目标 STW 时间 200ms)、-XX:InitiatingHeapOccupancyPercent=45(堆内存占用 45% 时触发 G1 GC)。
  2. 高吞吐量场景(如后台批处理)

    • 选择 Parallel Scavenge + Parallel Old 组合(JDK 8 默认),无需手动指定,或通过 -XX:+UseParallelGC 启用。
    • 配置吞吐量参数:-XX:GCTimeRatio=19(目标吞吐量 95%,即 GC 时间占比不超过 5%)。
  3. 客户端 / 轻量场景

    • 选择 Serial + Serial Old 组合(默认),无需额外配置,适合单核 CPU 或桌面应用。

7.3 代码层面优化

  1. 避免内存泄漏

    • 集合类泄漏:使用 ArrayListHashMap 等集合时,若不再使用,需手动清空引用(如 list.clear() 或 list = null),避免集合持有大量无效对象。
    • 静态集合风险:避免使用 static List 存储大量对象(静态集合生命周期与 JVM 一致,对象无法被 GC)。
    • 资源未关闭:IO 流、数据库连接、Socket 等资源需在 finally 中关闭,或使用 try-with-resources 语法(JDK 7+)。
  2. 减少对象创建

    • 复用对象:使用对象池(如 Apache Commons Pool)复用频繁创建的对象(如数据库连接、线程)。
    • 避免频繁创建短生命周期对象:如在循环中创建 String(可使用 StringBuilder 拼接)、临时对象。
  3. 合理使用引用类型

    • 根据对象重要性选择引用类型,避免不必要的强引用导致对象无法回收:
      • 强引用(默认):对象被强引用时,GC 不会回收(如 Object obj = new Object())。
      • 软引用(SoftReference):内存不足时 GC 才会回收,适合缓存(如图片缓存)。
      • 弱引用(WeakReference):GC 触发时立即回收,适合临时关联(如 WeakHashMap)。
      • 虚引用(PhantomReference):仅用于跟踪对象回收,无实际引用意义。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐