Java JVM 篇常见面试题

一、JVM 基础概念

JVM 是什么?它的作用是什么?

1. JVM 是什么?

JVM 全称 Java Virtual Machine ,即 Java 虚拟机

它是一个运行 Java 字节码的虚拟计算机 ,负责将 .class 文件中的 字节码 转换为 机器指令,并在不同操作系统上运行。

简单来说:

  • Java 源代码.java) → 编译器(javac)字节码文件(.classJVM 执行
  • JVM 是 Java 的核心,保证了 一次编写,处处运行(Write Once, Run Anywhere)。

2. JVM 的作用

JVM 在 Java 运行过程中承担了很多重要职责,可以总结为以下几个方面:

(1)跨平台运行

  • Java 程序编译后生成的是 字节码,而不是直接的机器码。
  • 不同平台只需要安装对应的 JVM,就能运行同样的字节码文件。
  • 这就是 Java 的 平台无关性

(2)内存管理

  • JVM 会自动管理内存,包括:
    • 内存分配(对象创建时分配内存)
    • 垃圾回收(GC 自动回收不再使用的对象)
  • 这让开发者不必手动释放内存,减少内存泄漏风险。

(3)类加载机制

  • JVM 通过 类加载器(ClassLoader) 动态加载 .class 文件到内存。
  • 支持按需加载、延迟加载、热替换等功能。

(4)字节码执行

  • JVM 内部有一个 执行引擎(Execution Engine) ,负责解释执行字节码或通过 JIT(Just-In-Time)编译优化为机器码,提高运行效率。

(5)安全性

  • JVM 会在类加载和字节码执行时进行 安全检查,防止恶意代码破坏系统。
  • 例如:字节码验证、访问权限控制、沙箱机制。

3. JVM 的主要组成结构

JVM 内部可以分为几个核心模块:

模块 作用
类加载器(ClassLoader) 负责加载 .class 文件到内存
运行时数据区(Runtime Data Area) JVM 的内存结构,存放类信息、对象、方法等
执行引擎(Execution Engine) 解释执行字节码或通过 JIT 编译为机器码
本地方法接口(JNI) 调用本地操作系统方法或 C/C++ 库
垃圾回收器(GC) 自动回收不再使用的对象内存

4. JVM 内存结构(运行时数据区)

JVM 在运行时会划分不同的内存区域:

内存区域 作用
方法区(Method Area) 存放类信息、常量、静态变量、JIT 编译后的代码
堆(Heap) 存放所有对象实例,是 GC 的主要管理区域
虚拟机栈(VM Stack) 存放方法调用的局部变量、操作数栈等
本地方法栈(Native Method Stack) 为本地方法服务
程序计数器(PC Register) 记录当前线程执行的字节码行号

5. JVM 的工作流程

  1. 编译阶段 :Java 源代码 → 编译器 → 字节码文件(.class
  2. 类加载阶段:类加载器将字节码加载到 JVM 内存
  3. 执行阶段
    • 解释执行字节码(Interpreter)
    • 或 JIT 编译为机器码(提高性能)
  4. 内存管理与垃圾回收:自动分配与回收对象内存

6. 总结

  • JVM 是 Java 的核心运行环境,负责字节码执行、内存管理、类加载、安全检查等。
  • 它的最大作用是:
    1. 跨平台运行(一次编写,处处运行)
    2. 自动内存管理(GC)
    3. 安全性保障
    4. 性能优化(JIT 编译)

JVM、JRE、JDK 的区别?

1. 三者的定义

JVM(Java Virtual Machine)

  • Java 虚拟机,是 Java 程序的运行环境。
  • 负责将 字节码(.class 文件) 转换为机器码并执行。
  • 提供 跨平台能力内存管理(GC)安全检查 等功能。
  • 核心作用:运行 Java 程序。

JRE(Java Runtime Environment)

  • Java 运行时环境 ,包含 JVM + 核心类库(Java API)。
  • 提供运行 Java 程序所需的全部环境,但不包含编译工具
  • 核心作用:让你可以运行 Java 程序,但不能编译。

JDK(Java Development Kit)

  • Java 开发工具包 ,包含 JRE + 开发工具(javac 编译器、调试工具、文档生成工具等)。
  • 用于开发、编译、调试 Java 程序。
  • 核心作用:让你可以开发和运行 Java 程序。

2. 三者的关系

可以用一个简单的包含关系来理解:

复制代码
JDK = JRE + 开发工具
JRE = JVM + 核心类库

3. 对比表格

名称 全称 主要组成 作用 是否能运行 Java 程序 是否能开发 Java 程序
JVM Java Virtual Machine 虚拟机执行引擎 运行字节码
JRE Java Runtime Environment JVM + 核心类库 提供运行环境
JDK Java Development Kit JRE + 开发工具 开发、编译、运行 Java 程序

4. 举例理解

  • JVM:相当于一台虚拟的计算机,只能执行 Java 字节码。
  • JRE:相当于这台虚拟计算机 + 必备的系统文件,让它能正常运行 Java 程序。
  • JDK:相当于这台虚拟计算机 + 系统文件 + 开发工具,让你可以写代码、编译、运行。

5. 面试高频总结

  • JVM:运行 Java 程序的虚拟机。
  • JRE:运行 Java 程序的环境(包含 JVM 和类库)。
  • JDK:开发 Java 程序的工具包(包含 JRE 和开发工具)。
  • 关系:JDK ⊃ JRE ⊃ JVM。

Java 程序的执行过程是什么?

1. Java 程序执行的整体流程

Java 程序的执行过程可以分为 编译阶段运行阶段 两大部分:

  1. 编译阶段 (由 javac 完成)

    • Java 源代码文件.java)编译成 字节码文件.class)。
    • 字节码是一种 平台无关的中间代码,不能直接在操作系统上运行。
  2. 运行阶段(由 JVM 完成)

    • JVM 通过 类加载器.class 文件加载到内存。
    • 字节码验证器 检查字节码的安全性。
    • 执行引擎(解释器 / JIT 编译器)将字节码转换为机器码并执行。
    • JVM 负责 内存分配垃圾回收(GC)

2. 详细执行步骤

① 编写源代码

  • 开发者使用 Java 语言编写 .java 文件。

② 编译成字节码

  • 使用 javac 编译器:

    bash 复制代码
    javac HelloWorld.java
  • 生成 .class 文件(字节码)。

③ 类加载(Class Loading)

  • JVM 的 类加载器(ClassLoader).class 文件加载到内存。
  • 类加载过程分为:
    1. 加载(Loading) :读取 .class 文件。
    2. 链接(Linking):验证字节码、准备静态变量、解析符号引用。
    3. 初始化(Initialization):执行类的静态初始化块和静态变量赋值。

④ 字节码验证

  • JVM 的 字节码验证器 检查代码的合法性和安全性,防止恶意代码破坏系统。

⑤ 执行引擎运行字节码

  • 解释器(Interpreter):逐行解释字节码为机器码,启动快但运行慢。
  • JIT 编译器(Just-In-Time Compiler):将热点代码编译为机器码,提高运行速度。

⑥ 内存管理与垃圾回收

  • JVM 自动分配对象内存(在堆中)。
  • GC 自动回收不再使用的对象,避免内存泄漏。

3. Java 程序执行流程图

复制代码
[编写源代码] → [javac 编译] → [字节码文件 .class]
       ↓
[类加载器加载类] → [字节码验证] → [执行引擎运行字节码]
       ↓
[机器码执行] → [内存管理 & 垃圾回收]

4. 总结

  • 编译阶段:Java 源代码 → 字节码(平台无关)
  • 运行阶段:JVM 将字节码 → 机器码(平台相关)
  • 核心环节:
    1. 类加载器(加载字节码)
    2. 字节码验证器(安全检查)
    3. 执行引擎(解释执行 / JIT 编译)
    4. 内存管理 & GC(自动管理内存)

JVM 的跨平台原理是什么?

1. 先说结论

JVM 的跨平台原理 就是:
Java 源代码先编译成平台无关的字节码(Bytecode),再由不同平台的 JVM 将字节码翻译成对应平台的机器码执行。

这样,Java 程序只需要编写一次,就能在不同操作系统上运行(Write Once, Run Anywhere)。


2. 原理拆解

跨平台的关键在于 字节码 + 各平台的 JVM 实现

① 编译成字节码(平台无关)

  • Java 源代码(.java)通过 javac 编译器 编译成 字节码文件(.class
  • 字节码是一种 中间语言,不依赖具体的硬件和操作系统。

② JVM 解释执行字节码(平台相关)

  • 不同操作系统(Windows、Linux、macOS)都有各自的 JVM 实现
  • JVM 会将字节码 解释执行JIT 编译成该平台的机器码。
  • 因为 JVM 屏蔽了底层差异,所以同一个字节码文件可以在不同平台运行。

3. 跨平台的核心机制

  1. 统一的字节码格式
    • Java 编译器生成的 .class 文件在任何平台上都是相同的。
  2. 不同平台的 JVM 实现
    • 每个平台的 JVM 都能识别字节码,并将其转换为该平台的机器码。
  3. Java API 的平台适配
    • Java 标准库(Java API)对底层系统调用进行了封装,开发者不需要关心操作系统差异。

4. 流程图

复制代码
Java 源代码 (.java)
        ↓ javac 编译
字节码文件 (.class)  ←—— 平台无关
        ↓ JVM(不同平台实现)
机器码(平台相关) → 执行程序

5. 举例说明

假设你写了一个 HelloWorld.java

  • 在 Windows 上:用 Windows 版 JVM 运行 .class 文件 → 转成 Windows 机器码 → 执行。
  • 在 Linux 上:用 Linux 版 JVM 运行同一个 .class 文件 → 转成 Linux 机器码 → 执行。
  • 源代码和字节码完全相同,只是 JVM 的底层实现不同。

6. 面试高频总结

  • 跨平台的本质:字节码统一,JVM 适配不同平台。
  • 一句话回答:Java 通过将源代码编译成平台无关的字节码,由不同平台的 JVM 将字节码转换为平台相关的机器码,从而实现跨平台运行。

JVM 的主要组成部分有哪些?

1. JVM 的主要组成部分

JVM 的核心结构可以分为 类加载子系统、运行时数据区、执行引擎、垃圾回收系统、JNI 接口 五大模块。


① 类加载子系统(Class Loader Subsystem)

  • 作用 :负责将 .class 字节码文件加载到 JVM 内存中。
  • 过程
    1. 加载(Loading):读取字节码文件。
    2. 链接(Linking):验证字节码、准备静态变量、解析符号引用。
    3. 初始化(Initialization):执行类的静态初始化块和静态变量赋值。
  • 特点:支持按需加载、延迟加载、热替换。

② 运行时数据区(Runtime Data Area)

  • JVM 在运行时会划分不同的内存区域,用来存储类信息、对象、方法调用等数据。
内存区域 作用
方法区(Method Area) 存放类信息、常量、静态变量、JIT 编译后的代码
堆(Heap) 存放所有对象实例,是 GC 的主要管理区域
虚拟机栈(VM Stack) 存放方法调用的局部变量、操作数栈等
本地方法栈(Native Method Stack) 为本地方法服务
程序计数器(PC Register) 记录当前线程执行的字节码行号

③ 执行引擎(Execution Engine)

  • 作用:负责执行字节码。
  • 组成
    • 解释器(Interpreter):逐行解释字节码为机器码,启动快但运行慢。
    • JIT 编译器(Just-In-Time Compiler):将热点代码编译为机器码,提高运行速度。
  • 特点:解释器启动快,JIT 编译器运行快,二者结合使用。

④ 垃圾回收系统(Garbage Collector, GC)

  • 作用:自动回收不再使用的对象内存。
  • 特点
    • 避免内存泄漏。
    • 常见算法:标记-清除、标记-整理、复制算法、分代收集。
  • 好处:开发者无需手动释放内存。

⑤ 本地方法接口(Java Native Interface, JNI)

  • 作用:让 Java 调用本地操作系统方法或 C/C++ 库。
  • 特点
    • 提供与底层平台交互的能力。
    • 常用于调用系统 API 或高性能的本地库。

2. JVM 组成结构图

复制代码
┌───────────────────────────────┐
│         类加载子系统           │
├───────────────────────────────┤
│       运行时数据区(内存)     │
│  ┌───────────────┐            │
│  │ 方法区         │            │
│  │ 堆             │            │
│  │ 虚拟机栈       │            │
│  │ 本地方法栈     │            │
│  │ 程序计数器     │            │
│  └───────────────┘            │
├───────────────────────────────┤
│         执行引擎               │
│  ┌───────────────┐            │
│  │ 解释器         │            │
│  │ JIT 编译器     │            │
│  └───────────────┘            │
├───────────────────────────────┤
│         垃圾回收系统           │
├───────────────────────────────┤
│         本地方法接口           │
└───────────────────────────────┘

3. 面试高频总结

  • 类加载子系统 :加载 .class 文件到内存。
  • 运行时数据区:JVM 的内存结构(方法区、堆、栈、PC 寄存器、本地方法栈)。
  • 执行引擎:解释执行字节码或 JIT 编译为机器码。
  • 垃圾回收系统:自动回收内存。
  • JNI 接口:调用本地方法。

JVM 的运行时数据区有哪些?

1. 什么是运行时数据区?

  • 运行时数据区(Runtime Data Area) 是 JVM 在运行 Java 程序时分配的内存区域,用于存储类信息、对象、方法调用、局部变量等数据。
  • 这些区域有的 线程共享 ,有的 线程私有

2. JVM 运行时数据区的组成

JVM 运行时数据区主要包括以下几个部分:

内存区域 线程共享/私有 主要作用
方法区(Method Area) 共享 存放类信息、常量、静态变量、JIT 编译后的代码
堆(Heap) 共享 存放所有对象实例,是 GC 的主要管理区域
虚拟机栈(VM Stack) 私有 存放方法调用的局部变量、操作数栈、方法出口等
本地方法栈(Native Method Stack) 私有 为本地方法(C/C++)服务
程序计数器(PC Register) 私有 记录当前线程执行的字节码行号

① 方法区(Method Area)

  • 线程共享
  • 存放:
    • 类的结构信息(类名、父类、接口、字段、方法等)
    • 运行时常量池(字符串常量、符号引用等)
    • 静态变量
    • JIT 编译后的代码
  • 在 JDK 8 之前,方法区的实现是 永久代(PermGen) ;JDK 8 之后改为 元空间(Metaspace),存储在本地内存中。

② 堆(Heap)

  • 线程共享
  • 存放所有对象实例和数组。
  • 垃圾回收(GC) 的主要区域。
  • 一般分为:
    • 新生代(Young Generation):Eden 区 + Survivor 区(S0、S1)
    • 老年代(Old Generation)
  • GC 主要在堆中进行。

③ 虚拟机栈(VM Stack)

  • 线程私有
  • 每个方法在执行时会创建一个 栈帧(Stack Frame) ,用于存储:
    • 局部变量表(方法参数、局部变量)
    • 操作数栈
    • 动态链接
    • 方法返回地址
  • 方法执行结束后,栈帧被销毁。

④ 本地方法栈(Native Method Stack)

  • 线程私有
  • 为 JVM 调用的本地方法(Native Method)服务。
  • 存储本地方法的局部变量、操作数栈等。

⑤ 程序计数器(PC Register)

  • 线程私有
  • 记录当前线程执行的字节码指令地址(行号)。
  • 如果执行的是本地方法,PC 寄存器的值是 未定义(Undefined)

3. 运行时数据区结构图

复制代码
┌───────────────────────────────┐
│        线程共享(所有线程可见) │
│  ┌─────────────────────────┐  │
│  │ 方法区(Method Area)    │  │
│  │ 堆(Heap)               │  │
│  └─────────────────────────┘  │
├───────────────────────────────┤
│        线程私有(每个线程独立) │
│  ┌─────────────────────────┐  │
│  │ 虚拟机栈(VM Stack)     │  │
│  │ 本地方法栈(Native Stack)│ │
│  │ 程序计数器(PC Register)│ │
│  └─────────────────────────┘  │
└───────────────────────────────┘

4. 面试高频总结

  • 线程共享:方法区、堆。
  • 线程私有:虚拟机栈、本地方法栈、程序计数器。
  • GC 主要作用于堆,方法区也可能被回收(如废弃常量、无用类)。
  • JDK 8 之后:永久代被移除,方法区由元空间实现。

JVM 的内存模型与 Java 内存模型(JMM)的区别?

1. JVM 内存模型

  • 定义 :描述 JVM 在运行 Java 程序时的内存结构划分
  • 关注点:程序运行时,数据在 JVM 内部是如何存储和管理的。
  • 核心内容
    • 方法区(Method Area)
    • 堆(Heap)
    • 虚拟机栈(VM Stack)
    • 本地方法栈(Native Method Stack)
    • 程序计数器(PC Register)
  • 用途:用于理解对象存储位置、GC 作用范围、内存分配策略等。

2. Java 内存模型(JMM)

  • 定义 :描述 Java 多线程程序中,线程如何通过内存进行交互
  • 关注点:线程之间如何共享变量、如何保证可见性、有序性和原子性。
  • 核心内容
    • 主内存(Main Memory):存储所有共享变量。
    • 工作内存(Working Memory):每个线程的私有内存,存储主内存变量的副本。
    • 内存交互规则 :定义了变量在主内存和工作内存之间的读写操作(如 readloadstorewrite)。
  • 用途 :用于理解并发编程中的内存可见性问题、volatilesynchronizedfinal 等关键字的作用。

3. 核心区别

对比项 JVM 内存模型 Java 内存模型(JMM)
定义 JVM 运行时的内存结构划分 多线程间的内存交互规范
关注点 程序运行时数据的存储位置和管理 线程间共享变量的可见性、有序性、原子性
范围 JVM 内部实现细节 Java 语言层面的并发语义
主要内容 方法区、堆、虚拟机栈、本地方法栈、PC 寄存器 主内存、工作内存、内存交互规则
应用场景 GC 调优、内存溢出分析、对象分配优化 并发编程、线程安全、内存可见性问题
是否与硬件相关 偏向 JVM 实现,与硬件关系不大 与 CPU 缓存、内存一致性模型密切相关

4. 形象理解

  • JVM 内存模型:像是 JVM 的"房间布局图",告诉你数据在 JVM 内部的哪个房间(区域)存放。
  • Java 内存模型(JMM):像是"快递传输规则",告诉你不同线程之间如何把数据从一个房间送到另一个房间,并保证送到的数据是最新的。

5. 面试高频总结

  • JVM 内存模型:运行时内存结构(方法区、堆、栈等)。
  • JMM:并发编程的内存交互规则(主内存、工作内存、可见性、有序性、原子性)。
  • 一句话区分:JVM 内存模型是"存储结构",JMM 是"访问规则"。

JVM 的指令集是基于栈还是寄存器?为什么?

1. 什么是寄存器?

  • 寄存器(Register)CPU 内部 的一种 速度最快的存储单元
  • 它用来 暂时存放 CPU 正在处理的数据、指令地址等信息。
  • 因为寄存器在 CPU 内部,所以访问速度比内存(RAM)快很多。

2. 寄存器的特点

  1. 位置:在 CPU 芯片内部。
  2. 容量:很小(通常只有几十到几百个寄存器,每个寄存器大小一般是 32 位或 64 位)。
  3. 速度:最快的存储器,比缓存(Cache)和内存(RAM)都快。
  4. 用途:存放运算过程中的数据、指令地址、状态信息等。

3. 寄存器的主要类型

类型 作用
通用寄存器 存放运算数据(如加法、乘法的操作数)
地址寄存器 存放内存地址
指令寄存器(IR) 存放当前正在执行的指令
程序计数器(PC) 存放下一条要执行的指令地址
状态寄存器 存放运算结果的状态(如进位、溢出、零标志)

4. 为什么寄存器很快?

  • 寄存器直接在 CPU 内部,数据不需要通过总线传输。
  • 访问寄存器的延迟几乎为 0 个 CPU 时钟周期,而访问内存可能需要几十甚至上百个时钟周期。

5. 形象类比

你可以把 寄存器 想象成:

  • CPU 的手里正在握着的工具(随时可用,速度最快)。
  • 内存 就像是旁边的工具箱,需要伸手去拿,速度会慢一些。

一句话总结
寄存器是 CPU 内部的超高速存储单元,用来暂存正在处理的数据和指令地址,速度比内存快很多,但数量很少。

JVM 的指令集是 基于栈(Stack-Based) 的,而不是基于寄存器的。

1. 结论

  • JVM 指令集是基于栈的(Stack-Based Instruction Set)
  • 这意味着 JVM 的大部分指令操作数都来自 操作数栈,而不是 CPU 寄存器。

2. 为什么选择基于栈?

① 跨平台性

  • 不同 CPU 架构的寄存器数量和名称都不同(x86、ARM、RISC-V 等)。
  • 如果 JVM 基于寄存器,那么字节码就会依赖具体硬件,失去跨平台能力。
  • 基于栈的指令集不依赖硬件寄存器,字节码可以在任何平台的 JVM 上运行。

② 字节码更紧凑

  • 基于栈的指令集不需要显式指定操作数位置(寄存器编号),只需从栈顶取值。
  • 这样字节码更短、更紧凑,便于传输和存储。

③ 实现简单

  • 栈结构简单,指令只需操作栈顶元素。
  • JVM 的解释器实现更容易,不需要复杂的寄存器分配算法。

④ 安全性

  • 栈操作天然有边界检查(栈溢出检测),更容易保证字节码的安全性。
  • 这符合 JVM 的设计目标:安全、可移植、可验证

3. 基于栈 vs 基于寄存器的对比

特性 基于栈(JVM) 基于寄存器(如 x86)
跨平台性 高,不依赖硬件寄存器 低,依赖具体 CPU 架构
指令长度 短,紧凑 长,需要指定寄存器编号
实现复杂度 低,操作栈顶即可 高,需要寄存器分配和管理
执行速度 相对慢(多一次栈操作) 快(直接寄存器操作)
安全性 高,易于验证字节码 较低,需额外安全检查

4. 示例

假设我们要计算 $1 + 2$

基于栈的字节码(JVM)

复制代码
iconst_1    // 压入常量 1
iconst_2    // 压入常量 2
iadd        // 弹出两个数相加,结果压回栈顶

基于寄存器的指令(x86)

复制代码
mov eax, 1  // 将 1 放入寄存器 eax
add eax, 2  // eax 加 2

JVM 不需要关心寄存器名称,只操作栈顶数据,因此更容易跨平台。


5. 面试高频总结

  • JVM 指令集是基于栈的,不是基于寄存器。
  • 原因
    1. 跨平台性(不依赖硬件寄存器)
    2. 字节码更紧凑
    3. 实现简单
    4. 安全性高
  • 缺点:执行速度比寄存器架构稍慢,但可以通过 JIT 编译优化。

JVM 的启动过程是怎样的?

1. JVM 启动的整体流程

当你在命令行执行

bash 复制代码
java MyClass

时,JVM 会经历以下几个阶段:

  1. 启动类加载器加载启动类
  2. 初始化运行时数据区
  3. 加载并初始化主类(包含 main 方法的类)
  4. 执行 main 方法
  5. 程序运行直到结束或被终止

2. 详细步骤

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

  • JVM 由操作系统加载到内存后,会先启动 Bootstrap ClassLoader
  • 这个类加载器负责加载 核心类库rt.jar 或模块化后的 java.base)。
  • 包括:
    • java.lang.*(如 ObjectStringThread
    • java.util.*
    • 其他 JVM 必需的基础类。

② 初始化运行时数据区

  • JVM 会创建并初始化 运行时数据区
    • 方法区(存放类信息、常量、静态变量)
    • (存放对象实例)
    • 虚拟机栈(每个线程一个)
    • 本地方法栈
    • 程序计数器
  • 这一步相当于为程序运行准备好"内存环境"。

③ 加载并初始化主类

  • JVM 使用 应用类加载器(Application ClassLoader) 加载你指定的主类(MyClass)。
  • 类加载过程:
    1. 加载(Loading) :读取 .class 文件字节码。
    2. 链接(Linking)
      • 验证(Verify):检查字节码合法性。
      • 准备(Prepare):为静态变量分配内存。
      • 解析(Resolve):将符号引用替换为直接引用。
    3. 初始化(Initialization):执行静态初始化块和静态变量赋值。

④ 执行 main 方法

  • JVM 创建一个主线程(Main Thread)。
  • 主线程的虚拟机栈中压入 main 方法的栈帧。
  • 执行引擎(解释器 + JIT 编译器)开始执行字节码。

⑤ 程序运行与结束

  • 程序运行过程中:
    • JVM 负责内存分配和垃圾回收。
    • 多线程由 JVM 调度。
  • main 方法执行完毕,且所有非守护线程结束时,JVM 退出。

3. JVM 启动过程示意图

复制代码
[操作系统加载 JVM]
        ↓
[Bootstrap ClassLoader 加载核心类库]
        ↓
[初始化运行时数据区]
        ↓
[Application ClassLoader 加载主类]
        ↓
[类加载过程:加载 → 链接 → 初始化]
        ↓
[创建主线程,执行 main 方法]
        ↓
[程序运行,GC 管理内存]
        ↓
[所有非守护线程结束 → JVM 退出]

4. 面试高频总结

  • 启动类加载器先加载核心类库。
  • 运行时数据区初始化,为程序运行准备内存。
  • 主类加载经过加载、链接、初始化三个阶段。
  • 执行引擎负责运行字节码。
  • JVM 退出条件:所有非守护线程结束。

JVM 的双亲委派模型是什么?

1. 什么是双亲委派模型?

  • 双亲委派模型(Parent Delegation Model) 是 JVM 类加载器的一种工作机制。
  • 核心思想
    当一个类加载器接到类加载请求时,它不会自己先加载 ,而是先把请求交给父类加载器去处理,父类加载器如果能加载,就直接返回;如果不能加载,才由当前加载器自己去加载。

2. 类加载器层次结构

JVM 中的类加载器有一个层级关系:

复制代码
Bootstrap ClassLoader(启动类加载器)
        ↑
ExtClassLoader(扩展类加载器)
        ↑
AppClassLoader(应用类加载器)
        ↑
自定义类加载器(Custom ClassLoader)
  • Bootstrap ClassLoader
    • 由 C++ 实现(不是 Java 类),负责加载 核心类库java.lang.*java.util.* 等)。
  • ExtClassLoader
    • 加载 JAVA_HOME/lib/ext 目录下的扩展类库。
  • AppClassLoader
    • 加载应用程序的 classpath 下的类。
  • 自定义类加载器
    • 由开发者自己实现,通常继承 ClassLoader

3. 工作流程

双亲委派模型的类加载流程如下:

  1. 收到类加载请求
  2. 先委托父类加载器去加载。
  3. 父类加载器继续向上委托,直到 Bootstrap ClassLoader。
  4. 如果父类加载器能加载,直接返回类。
  5. 如果父类加载器不能加载,由当前加载器自己加载。

4. 为什么要用双亲委派?

① 避免类的重复加载

  • 保证同一个类在 JVM 中只有一个版本(由同一个类加载器加载)。
  • 例如:java.lang.String 始终由 Bootstrap ClassLoader 加载,避免被恶意替换。

② 保证核心类的安全性

  • 防止用户自己写一个 java.lang.Objectjava.lang.String 来替换系统核心类。

③ 提高加载效率

  • 父类加载器通常已经加载过很多类,直接复用可以减少重复工作。

5. 示例

假设我们要加载 java.lang.String

  1. AppClassLoader 收到请求 → 委托 ExtClassLoader。
  2. ExtClassLoader 收到请求 → 委托 Bootstrap ClassLoader。
  3. Bootstrap ClassLoader 在核心类库中找到 String.class → 返回。
  4. AppClassLoader 直接使用父类加载器返回的类。

6. 面试高频总结

  • 定义:类加载器收到请求先委托父类加载器,父类加载器无法加载时才自己加载。
  • 好处
    1. 避免类重复加载
    2. 保证核心类安全
    3. 提高加载效率
  • 缺点
    • 不灵活,某些情况下需要打破双亲委派(如 SPI、热部署、插件机制)。

二、类加载机制

类加载的过程包括哪几个阶段?

加载、验证、准备、解析、初始化分别做什么?

什么是双亲委派模型?为什么要使用它?

如何打破双亲委派模型?

1. 为什么要打破双亲委派模型?

双亲委派模型的好处是安全、避免重复加载,但它也有一个限制

如果父类加载器已经加载了某个类,子类加载器就无法加载同名类。

在一些场景下,我们希望由子类加载器优先加载,而不是父类加载器,例如:

  • SPI(Service Provider Interface)机制
    父类加载器加载接口,子类加载器加载实现类。
  • 热部署 / 热替换
    需要重新加载同名类。
  • 插件化系统
    不同插件可能有相同类名,但需要隔离加载。

2. 打破双亲委派的常见方法

方法 1:自定义类加载器并重写 loadClass 方法

  • 默认 ClassLoaderloadClass 方法是先委托父类加载器。
  • 如果我们不调用 super.loadClass ,而是直接调用 findClass,就可以让当前类加载器优先加载。

示例代码:

java 复制代码
public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 1. 核心类库还是走父类加载器,保证安全
        if (name.startsWith("java.")) {
            return super.loadClass(name, resolve);
        }

        // 2. 自己先尝试加载
        try {
            byte[] data = loadClassData(name); // 从文件或网络读取字节码
            Class<?> clazz = defineClass(name, data, 0, data.length);
            if (resolve) {
                resolveClass(clazz);
            }
            return clazz;
        } catch (Exception e) {
            // 3. 如果自己加载失败,再交给父类加载器
            return super.loadClass(name, resolve);
        }
    }

    private byte[] loadClassData(String name) {
        // 读取字节码的逻辑
        return new byte[0];
    }
}

关键点

  • 不直接调用 super.loadClass,而是先自己加载。
  • 核心类库(java.*)依然交给父类加载器,避免安全问题。

方法 2:线程上下文类加载器(Thread Context ClassLoader)

  • JDK 提供了 Thread.currentThread().setContextClassLoader() 方法,可以临时替换当前线程的类加载器
  • 常用于 SPI 机制
    • 父类加载器加载接口。
    • 子类加载器(通过线程上下文类加载器)加载实现类。

示例:

java 复制代码
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(new MyClassLoader());

方法 3:OSGi / 模块化框架

  • OSGi 等模块化框架会为每个模块创建独立的类加载器,并且不遵循传统的双亲委派 ,而是通过模块依赖关系来决定类加载顺序。
  • 这种方式可以让不同模块加载同名类而互不干扰。

3. 典型应用场景

场景 打破方式 原因
SPI 机制 线程上下文类加载器 父类加载器加载接口,子类加载器加载实现类
热部署 自定义类加载器 重新加载同名类
插件化系统 模块化类加载器(OSGi) 不同插件隔离加载
Tomcat / Web 容器 WebAppClassLoader 每个 Web 应用独立类加载器,避免类冲突

4. 面试高频总结

  • 打破双亲委派的核心:让子类加载器优先加载,而不是父类加载器。
  • 常用方法
    1. 重写 loadClass (不调用 super.loadClass
    2. 线程上下文类加载器(SPI)
    3. 模块化类加载器(OSGi、Tomcat)
  • 注意安全性:核心类库仍应由 Bootstrap ClassLoader 加载。

类加载器的种类有哪些?

JVM 中的类加载器种类 ,这个知识点在面试和调优中都很常见,而且和 双亲委派模型打破双亲委派 等问题紧密相关。


1. JVM 内置的类加载器种类

在 Java 8 及之前(非模块化),JVM 主要有三种内置类加载器

类加载器 作用 加载范围
Bootstrap ClassLoader(启动类加载器) JVM 最顶层的类加载器,由 C++ 实现,不是 Java 类 加载 核心类库JAVA_HOME/lib 下的 rt.jar 或模块化后的 java.base
Extension ClassLoader(扩展类加载器) Java 实现,父类是 Bootstrap ClassLoader 加载 JAVA_HOME/lib/ext 目录下的类库
Application ClassLoader(系统类加载器) Java 实现,父类是 Extension ClassLoader 加载应用程序 classpath 下的类

2. Java 9 之后的变化(模块化系统)

  • Java 9 引入 模块化(Jigsaw) ,类加载器结构有所调整:
    • Bootstrap ClassLoader 仍然存在,加载 java.base 模块。
    • Platform ClassLoader 取代了 Extension ClassLoader,加载平台模块(如 java.sqljava.xml)。
    • Application ClassLoader 仍然负责加载应用模块和类路径下的类。

3. 自定义类加载器

除了 JVM 内置的类加载器,我们还可以自己实现类加载器

  • 继承 ClassLoaderURLClassLoader
  • 通过重写 findClassloadClass 方法实现自定义加载逻辑。
  • 常用于:
    • 热部署(重新加载同名类)
    • 插件化系统(不同插件隔离)
    • 网络加载类(从远程服务器加载字节码)

4. 类加载器的层次关系(Java 8 之前)

复制代码
Bootstrap ClassLoader(启动类加载器)
        ↑
Extension ClassLoader(扩展类加载器)
        ↑
Application ClassLoader(系统类加载器)
        ↑
Custom ClassLoader(自定义类加载器)

5. 面试高频总结

  • Java 8 及之前
    1. Bootstrap ClassLoader(核心类库)
    2. Extension ClassLoader(扩展类库)
    3. Application ClassLoader(应用类)
    4. Custom ClassLoader(自定义)
  • Java 9 及之后
    1. Bootstrap ClassLoader
    2. Platform ClassLoader(平台模块)
    3. Application ClassLoader
    4. Custom ClassLoader
  • 关系:双亲委派模型形成树状结构,父类加载器优先加载。

Bootstrap ClassLoader、ExtClassLoader、AppClassLoader 的区别?

1. Bootstrap ClassLoader

  • 作用 :负责加载 Java 核心类库 ,例如 rt.jar 中的类(java.lang.*java.util.* 等)。
  • 加载路径%JAVA_HOME%/lib 目录下的 .jar.class 文件。
  • 实现方式 :由 JVM 内部的 C/C++ 代码实现,不是 Java 类,因此在 Java 中无法直接获取它的对象。
  • 特点
    • 是所有类加载器的顶层父加载器
    • 在 Java 中调用 getParent() 时,如果父加载器是它,会返回 null
    • 主要保证核心类的安全性,防止被用户代码替换。

2. ExtClassLoader(扩展类加载器)

  • 作用 :负责加载 扩展类库 ,例如 %JAVA_HOME%/lib/ext 目录下的 .jar 文件。
  • 加载路径%JAVA_HOME%/lib/ext 或由系统属性 java.ext.dirs 指定的路径。
  • 实现方式 :由 Java 编写,类名通常是 sun.misc.Launcher$ExtClassLoader
  • 特点
    • 父加载器Bootstrap ClassLoader
    • 用于加载非核心但属于 JDK 扩展的类库。
    • 在双亲委派机制下,如果核心类加载器无法加载某个类,才会由它来加载。

3. AppClassLoader(系统类加载器)

  • 作用 :负责加载 应用程序类路径(classpath)下的类和资源。
  • 加载路径 :由系统属性 java.class.path 指定的路径(通常是我们项目的 bintarget/classes 目录以及依赖的 jar)。
  • 实现方式 :由 Java 编写,类名通常是 sun.misc.Launcher$AppClassLoader
  • 特点
    • 父加载器ExtClassLoader
    • 是我们在 Java 程序中最常用的类加载器。
    • 调用 ClassLoader.getSystemClassLoader() 返回的就是它。

4. 三者关系(双亲委派机制)

它们之间的关系可以用一条链表示:

复制代码
Bootstrap ClassLoader
        ↑
   ExtClassLoader
        ↑
   AppClassLoader

双亲委派机制

  1. 当一个类加载器接到加载请求时,先委托给父加载器
  2. 父加载器如果能加载,就直接返回。
  3. 父加载器不能加载时,才由当前加载器自己尝试加载。

5. 对比表格

类加载器 主要作用 加载路径 实现方式 父加载器
Bootstrap ClassLoader 加载 Java 核心类库 %JAVA_HOME%/lib JVM 内部 C/C++ 实现 无(返回 null
ExtClassLoader 加载扩展类库 %JAVA_HOME%/lib/extjava.ext.dirs Java 实现 Bootstrap ClassLoader
AppClassLoader 加载应用程序类路径下的类 java.class.path Java 实现 ExtClassLoader

总结

  • Bootstrap ClassLoader → 核心类库。
  • ExtClassLoader → JDK 扩展类库。
  • AppClassLoader → 应用程序类和第三方依赖。
  • 它们遵循 双亲委派机制,保证核心类的安全性和类加载的有序性。

自定义类加载器的实现方法?

1. 自定义类加载器的实现步骤

在 Java 中,自定义类加载器通常有两种方式:

  1. 继承 ClassLoader(推荐)
  2. 继承 URLClassLoader(适合加载指定路径的 jar/目录)

核心方法

  • findClass(String name)
    负责根据类名找到字节码并返回 Class 对象。
  • defineClass(String name, byte[] b, int off, int len)
    将字节数组转换为 Class 对象。
  • loadClass(String name)
    继承自 ClassLoader,实现了双亲委派机制,一般不重写,除非要改变加载逻辑。

2. 实现思路

  • 读取 .class 文件的字节码(可以从文件系统、网络、加密文件等来源)。
  • 调用 defineClass() 将字节码转换为 Class 对象。
  • 使用 loadClass()findClass() 来加载类。

3. 示例代码

下面是一个简单的自定义类加载器,从指定目录加载 .class 文件:

java 复制代码
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class MyClassLoader extends ClassLoader {

    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 将类名转换为文件路径
            String fileName = classPath + "/" + name.replace('.', '/') + ".class";
            byte[] classBytes = Files.readAllBytes(Paths.get(fileName));

            // 将字节码转换为 Class 对象
            return defineClass(name, classBytes, 0, classBytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException("无法加载类: " + name, e);
        }
    }

    public static void main(String[] args) throws Exception {
        // 假设 Hello.class 存放在 D:/classes 目录
        MyClassLoader loader = new MyClassLoader("D:/classes");

        // 使用自定义类加载器加载类
        Class<?> clazz = loader.loadClass("com.example.Hello");

        // 创建对象并调用方法
        Object obj = clazz.getDeclaredConstructor().newInstance();
        clazz.getMethod("sayHello").invoke(obj);
    }
}

4. 运行流程

  1. 编译目标类 (例如 Hello.java)并放到 D:/classes/com/example/Hello.class
  2. 运行 MyClassLoader,它会:
    • 先走双亲委派机制,父加载器找不到时调用 findClass()
    • 从指定目录读取字节码。
    • 调用 defineClass() 转换为 Class 对象。
  3. 通过反射调用方法。

5. 注意事项

  • 如果要打破双亲委派机制 (例如热加载、插件系统),可以重写 loadClass() 方法。
  • 自定义类加载器可以实现类隔离(不同加载器加载同名类会被视为不同类)。
  • 在实际项目中,可以结合加密/解密来保护字节码安全。

类加载器之间的关系是什么?

类的主动引用与被动引用的区别?

什么时候会触发类的初始化?


三、JVM 内存结构

JVM 的运行时数据区有哪些? 程序计数器的作用是什么? Java 虚拟机栈的作用是什么? 本地方法栈的作用是什么? 堆的作用是什么? 方法区(元空间)的作用是什么? 运行时常量池是什么?

1. JVM 运行时数据区概览

Java 虚拟机在运行 Java 程序时,会将内存划分为几个不同的区域,每个区域有不同的用途。主要包括:

  1. 程序计数器(Program Counter Register)
  2. Java 虚拟机栈(Java Virtual Machine Stack)
  3. 本地方法栈(Native Method Stack)
  4. 堆(Heap)
  5. 方法区(Method Area / 元空间 Metaspace)
  6. 运行时常量池(Runtime Constant Pool)

它们的关系可以简单表示为:

​​​​​​​┌─────────────────────────────┐
│ 程序计数器 │
│ Java 虚拟机栈 │
│ 本地方法栈 │
│ 堆 │
│ 方法区(元空间) │
│ 运行时常量池 │
└─────────────────────────────┘


2. 各部分详细说明

(1) 程序计数器

  • 作用:记录当前线程正在执行的字节码指令的地址(行号)。
  • 特点
    • 每个线程都有一个独立的程序计数器(线程私有)。
    • 在执行 Java 方法时,计数器记录的是字节码指令地址;执行 Native 方法时,计数器值为 undefined
  • 用途
    • 支持线程切换后能恢复到正确的执行位置。
    • 是实现 多线程 的关键。

(2) Java 虚拟机栈

  • 作用 :存储 Java 方法执行时的栈帧(Stack Frame)。
  • 栈帧内容
    • 局部变量表(Local Variables)
    • 操作数栈(Operand Stack)
    • 动态链接(Dynamic Linking)
    • 方法返回地址(Return Address)
  • 特点
    • 线程私有。
    • 生命周期与线程相同。
  • 用途
    • 方法调用时创建栈帧,方法结束时销毁栈帧。
    • 用于存储方法的局部变量和操作数。

(3) 本地方法栈

  • 作用 :为 JVM 调用 Native 方法(非 Java 方法)服务。
  • 特点
    • 线程私有。
    • 存储的是 Native 方法的执行状态。
  • 用途
    • 当 Java 调用 C/C++ 编写的本地方法时,本地方法栈会保存相关信息。

(4) 堆

  • 作用 :存储对象实例数组
  • 特点
    • 所有线程共享。
    • 是垃圾回收(GC)的主要区域。
  • 用途
    • 创建对象时,内存分配在堆中。
    • GC 会回收不再使用的对象。

(5) 方法区(元空间 Metaspace)

  • 作用:存储类的结构信息(类元数据)、常量、静态变量、JIT 编译后的代码等。
  • 特点
    • 所有线程共享。
    • JDK 8 之前方法区在堆中(永久代 PermGen),JDK 8 之后改为 元空间,使用本地内存。
  • 用途
    • 保存类的元信息(类名、方法、字段等)。
    • 存储静态变量和常量。

(6) 运行时常量池

  • 作用 :存储类加载后解析的字面量符号引用
  • 特点
    • 属于方法区的一部分。
    • 每个类都有自己的运行时常量池。
  • 用途
    • 支持动态链接。
    • 存储编译期生成的常量(如字符串字面量、final 常量)。

3. 对比表格

区域 线程共享 主要存储内容 生命周期
程序计数器 当前线程执行的字节码指令地址 与线程相同
Java 虚拟机栈 方法栈帧(局部变量、操作数栈等) 与线程相同
本地方法栈 Native 方法调用信息 与线程相同
对象实例、数组 JVM 启动到关闭
方法区 / 元空间 类元数据、静态变量、JIT 代码 JVM 启动到关闭
运行时常量池 字面量、符号引用 JVM 启动到关闭

总结

  • 程序计数器 → 记录当前线程执行位置。
  • Java 虚拟机栈 → 存储方法调用的局部变量和操作数。
  • 本地方法栈 → 支持 Native 方法执行。
  • → 存储对象实例,GC 主要区域。
  • 方法区 / 元空间 → 存储类元数据、静态变量。
  • 运行时常量池 → 存储字面量和符号引用,支持动态链接。

直接内存是什么?

1. 什么是直接内存

直接内存 并不是 JVM 运行时数据区的一部分,它是操作系统的内存 ,由 Java 程序通过 Native 方法直接向操作系统申请的内存空间。

它的分配和回收不受 JVM 堆的限制,而是由 操作系统 管理。

在 Java 中,直接内存通常通过 java.nio 包中的 ByteBuffer.allocateDirect() 来申请。


2. 直接内存的特点

  • 不在 JVM 堆中,因此不会被 GC 直接管理。
  • 操作系统分配和释放。
  • 访问速度比普通堆内存快(尤其在 I/O 场景),因为它可以减少数据在 JVM 堆和操作系统内核缓冲区之间的复制
  • 容量受限于系统内存和 -XX:MaxDirectMemorySize 参数。

3. 为什么需要直接内存

在传统 I/O 中,数据需要:

  1. 从磁盘读到操作系统内核缓冲区。
  2. 再从内核缓冲区复制到 JVM 堆内存。
  3. 程序处理数据。

这种双重拷贝 会降低性能。

直接内存可以让 JVM 直接使用操作系统的内存缓冲区,减少一次复制,提高效率。


4. 使用场景

  • 高性能 I/O(NIO)
  • 网络通信框架(Netty、MINA)
  • 大数据处理(Hadoop、Spark)
  • 需要频繁与操作系统交互的场景

5. 示例代码

java 复制代码
import java.nio.ByteBuffer;

public class DirectMemoryDemo {
    public static void main(String[] args) {
        // 分配 100MB 直接内存
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 100);
        System.out.println("直接内存容量: " + buffer.capacity() / 1024 / 1024 + "MB");

        // 写入数据
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put((byte) i);
        }

        // 读取数据
        buffer.flip();
        while (buffer.hasRemaining()) {
            buffer.get();
        }
    }
}

说明

  • allocateDirect() 会向操作系统申请内存。
  • 释放由 JVM 的 Cleaner 机制触发,但不是立即释放。

6. 注意事项

  • 内存溢出风险 :直接内存不受堆大小限制,但受 -XX:MaxDirectMemorySize 控制,超出会抛出 OutOfMemoryError
  • 释放延迟:虽然不在堆中,但释放依赖 GC 触发 Cleaner,可能会延迟。
  • 调优参数
    • -XX:MaxDirectMemorySize:最大直接内存大小。
    • 如果不设置,默认与堆大小相同。

总结

  • 直接内存是操作系统分配的内存,不在 JVM 堆中。
  • 主要用于 高性能 I/O,减少数据复制,提高速度。
  • 需要注意内存管理和释放延迟问题。

堆与栈的区别?

1. 概念区别

  • 堆(Heap)

    • 存储对象实例数组
    • 所有线程共享。
    • 由 **垃圾回收器(GC)**管理。
  • 栈(Stack)

    • 在 JVM 中指 Java 虚拟机栈 ,存储方法调用的栈帧(局部变量、操作数等)。
    • 每个线程独有(线程私有)。
    • 生命周期与线程相同,方法结束时栈帧自动销毁。

2. 详细对比表

对比项 堆(Heap) 栈(Stack)
存储内容 对象实例、数组 方法栈帧(局部变量、操作数栈、返回地址等)
线程共享 否(线程私有)
生命周期 JVM 启动到关闭 与线程相同
管理方式 由 GC 自动回收 方法结束时自动释放
访问速度 相对较慢(需 GC 管理) 快(直接内存地址访问)
内存分配 动态分配(运行时创建对象) 静态分配(方法调用时分配栈帧)
溢出错误 OutOfMemoryError(堆空间不足) StackOverflowError(栈深度过大)
作用 存储程序运行中需要长期存在的对象 存储方法调用过程中的临时数据

3. 运行过程中的关系

  1. 当你在 Java 中写:

    java 复制代码
    Person p = new Person();
    • new Person() 创建的对象存放在 中。
    • 变量 p 存放在 的局部变量表中,保存的是对象在堆中的引用地址。
  2. 方法调用时:

    • JVM 在 中为该方法创建一个栈帧。
    • 栈帧中可能包含指向堆中对象的引用。

4. 图示关系

复制代码
线程1栈(私有)       线程2栈(私有)
┌─────────────┐     ┌─────────────┐
│ 栈帧:方法A  │     │ 栈帧:方法X  │
│ 局部变量p → ──────→ 堆(共享)
└─────────────┘     └─────────────┘
                      ┌─────────────┐
                      │ Person对象   │
                      └─────────────┘
  • :存放方法调用信息和对象引用。
  • :存放对象本身,所有线程共享。

总结

  • → 存储对象实例,线程共享,GC 管理,生命周期长。
  • → 存储方法调用信息和局部变量,线程私有,方法结束自动释放,生命周期短。
  • 栈中保存的是对象引用,对象本身在堆中。

对象的创建过程是什么?

1. 概览

在 Java 中,当我们写下:

java 复制代码
Person p = new Person();

JVM 会经历 类加载 → 内存分配 → 初始化 → 引用赋值 等多个阶段,最终得到一个可用的对象。


2. 详细过程

(1) 类加载检查

  • 当第一次使用某个类时,JVM 会检查该类是否已经被加载、链接和初始化。
  • 如果没有:
    1. 加载(Loading) :通过类加载器读取 .class 文件字节码。
    2. 链接(Linking)
      • 验证(Verify):检查字节码合法性。
      • 准备(Prepare):为静态变量分配内存并赋默认值。
      • 解析(Resolve):将符号引用替换为直接引用。
    3. 初始化(Initialization) :执行类的 <clinit> 方法(静态初始化块、静态变量赋值)。

(2) 分配内存

  • JVM 在 中为新对象分配内存空间,大小由类的实例变量决定。
  • 分配方式:
    • 指针碰撞(Pointer Bump):如果堆是规整的,直接移动指针分配。
    • 空闲列表(Free List):如果堆有碎片,查找可用块分配。
  • 线程安全:
    • 使用 CAS + 失败重试TLAB(Thread Local Allocation Buffer) 来保证并发安全。

(3) 设置默认值

  • 分配的内存会先清零(保证对象的实例变量有默认值)。
  • 例如:
    • int0
    • booleanfalse
    • Objectnull

(4) 执行 <init> 构造方法

  • JVM 调用对象的构造方法,执行类中定义的初始化逻辑。
  • 过程:
    1. 调用父类构造方法(super())。
    2. 按声明顺序初始化实例变量。
    3. 执行构造方法体中的代码。

(5) 返回对象引用

  • 构造完成后,JVM 将对象的引用地址返回给调用者。
  • 引用存放在 的局部变量表中,对象本身在 中。

3. 流程图

复制代码
源码:new Person()
        ↓
类加载检查(ClassLoader)
        ↓
堆内存分配(Heap)
        ↓
默认值初始化(0/null/false)
        ↓
执行构造方法 <init>
        ↓
返回对象引用(存放在栈中)

4. 对象创建的 5 个关键步骤总结

  1. 类加载检查 → 确保类已加载、链接、初始化。
  2. 分配内存 → 在堆中为对象分配空间。
  3. 默认值初始化 → 保证实例变量有默认值。
  4. 执行构造方法 → 完成实例变量赋值和初始化逻辑。
  5. 返回引用 → 将对象地址赋给变量。

总结

  • 对象创建不仅仅是 new,背后涉及 类加载机制内存分配策略初始化流程
  • 堆中存放对象本身,栈中存放对象引用。
  • JVM 会通过 TLABCAS 保证多线程下的分配效率与安全。

四、垃圾回收(GC)机制

什么是垃圾回收?为什么需要垃圾回收? 如何判断对象是否可回收?

1. 什么是垃圾回收(GC)

垃圾回收 是指 JVM 自动释放不再被使用的对象所占用的内存空间的过程。

它的主要目标是:

  • 回收无用对象,释放堆内存。
  • 避免内存泄漏(Memory Leak)。
  • 提高内存利用率

在 Java 中,内存管理由 JVM 自动完成,程序员不需要手动释放对象(不像 C/C++ 需要 free()delete)。


2. 为什么需要垃圾回收

(1) 防止内存泄漏

  • 如果对象不再使用但仍占用内存,会导致堆空间不足,最终抛出 OutOfMemoryError

(2) 提高开发效率

  • 程序员无需手动管理内存,减少错误风险。

(3) 保证程序稳定性

  • 自动释放无用对象,避免因内存不足导致程序崩溃。

3. 如何判断对象是否可回收

JVM 主要通过**可达性分析(Reachability Analysis)**来判断对象是否可回收。

(1) 可达性分析算法

  • 从一组称为 GC Roots 的对象出发,沿着引用链向下搜索。
  • 如果某个对象不可从 GC Roots 到达 ,则认为它是不可达对象,可以被回收。

GC Roots 常见类型

  1. 虚拟机栈中引用的对象(局部变量)。
  2. 方法区中类的静态引用对象。
  3. 方法区中常量引用对象。
  4. 本地方法栈中 JNI 引用的对象。

(2) 引用类型与回收

Java 中的引用类型会影响 GC 判断:

  • 强引用(Strong Reference):不会被回收。
  • 软引用(Soft Reference):内存不足时回收。
  • 弱引用(Weak Reference):下一次 GC 必回收。
  • 虚引用(Phantom Reference):仅用于跟踪对象回收状态。

(3) 对象的"死亡"过程

  1. 第一次标记:对象不可达。
  2. 是否覆盖 finalize() 方法
    • 如果覆盖且未执行过,则放入 F-Queue 队列,等待执行 finalize()
    • 执行后,如果对象重新与 GC Roots 建立联系,则"复活"。
  3. 第二次标记:如果仍不可达,则回收。

4. 流程图

复制代码
GC Roots
   ↓
可达性分析
   ↓
不可达对象
   ↓
是否执行 finalize()
   ↓
复活? → 是:继续存活
         否:回收

5. 总结表格

问题 说明
什么是 GC 自动释放不再使用的对象所占内存
为什么需要 GC 防止内存泄漏、提高开发效率、保证稳定性
判断可回收的方式 可达性分析算法,从 GC Roots 出发
GC Roots 类型 栈中引用、静态引用、常量引用、JNI 引用
引用类型影响 强引用不回收,软引用内存不足时回收,弱引用必回收,虚引用仅跟踪

总结

  • 垃圾回收是 JVM 自动管理内存的核心机制。
  • 可达性分析是判断对象是否可回收的主要算法。
  • 引用类型会影响对象的回收时机。
  • 对象在被回收前可能会通过 finalize() 方法"复活"。

引用计数法与可达性分析的区别?

1. 引用计数法(Reference Counting)

(1) 原理

  • 在对象中维护一个引用计数器
  • 每当有一个地方引用它时,计数器 +1。
  • 每当引用失效时,计数器 -1。
  • 当计数器为 0 时,说明对象不再被使用,可以回收。

(2) 优点

  • 实现简单,判断效率高(计数为 0 立即回收)。
  • 回收过程分散进行,不会出现长时间的停顿(Stop-The-World)。

(3) 缺点

  • 无法处理循环引用

    java 复制代码
    class Node {
        Node next;
    }
    Node a = new Node();
    Node b = new Node();
    a.next = b;
    b.next = a; // 循环引用
    a = null;
    b = null; // 计数器不为 0,无法回收
  • 需要额外的存储空间维护计数器。

  • 每次引用变化都要更新计数器,性能开销大。

(4) 在 JVM 中的应用

  • 主流 JVM(HotSpot)不使用引用计数法作为主要 GC 判断算法,但在一些场景(如 Python、Objective-C、COM 组件)中仍被使用。

2. 可达性分析(Reachability Analysis)

(1) 原理

  • 从一组称为 GC Roots 的对象出发,沿着引用链向下搜索。
  • 如果某个对象不可从 GC Roots 到达,则认为它是不可达对象,可以回收。

(2) GC Roots 常见类型

  1. 虚拟机栈中引用的对象(局部变量)。
  2. 方法区中类的静态引用对象。
  3. 方法区中常量引用对象。
  4. 本地方法栈中 JNI 引用的对象。

(3) 优点

  • 能正确处理循环引用问题。
  • 更适合现代 GC 算法(分代收集、标记-清除、标记-整理等)。

(4) 缺点

  • 需要从 GC Roots 遍历整个引用图,耗时比引用计数法长。
  • 在执行分析时,通常需要 Stop-The-World 暂停用户线程。

(5) 在 JVM 中的应用

  • HotSpot JVM 采用可达性分析作为主要的 GC 判定算法

3. 对比表

对比项 引用计数法 可达性分析
原理 维护引用计数,计数为 0 即回收 从 GC Roots 出发,遍历引用链
循环引用 无法处理 可以处理
实现复杂度 简单 相对复杂
性能 引用变化时立即更新计数,开销大 需要遍历对象图,耗时集中
回收时机 计数为 0 立即回收 GC 触发时统一回收
是否需要 STW 不需要 通常需要
JVM 应用 不作为主要算法 HotSpot 主要使用

4. 图示理解

引用计数法循环引用问题

复制代码
a → b
↑   ↓
└───┘
a 和 b 互相引用,即使外部引用断开,计数器仍 > 0,无法回收

可达性分析

复制代码
GC Roots
   ↓
  对象A → 对象B → 对象C
         ↘ 对象D
不可达对象:从 GC Roots 无法到达的对象(即使它们互相引用)

5. 总结

  • 引用计数法:简单高效,但无法解决循环引用问题,因此不适合作为 JVM 的主要 GC 判定算法。
  • 可达性分析:能解决循环引用问题,是现代 JVM(如 HotSpot)的主流选择,但需要 STW 暂停用户线程。

强引用、软引用、弱引用、虚引用的区别?

1. 强引用(Strong Reference)

概念

  • Java 中最常见的引用类型。
  • 只要对象有强引用存在,GC 永远不会回收它
  • 即使内存不足,也不会回收,宁愿抛出 OutOfMemoryError

特点

  • 默认的引用类型,例如:

    java 复制代码
    Object obj = new Object();
  • 对象生命周期与引用变量一致。

示例

java 复制代码
Object obj = new Object(); // 强引用
obj = null; // 断开引用,GC 才能回收

适用场景

  • 普通对象的引用,确保对象在使用期间不被回收。

2. 软引用(Soft Reference)

概念

  • 在内存不足时,GC 会回收软引用指向的对象。
  • 适合做缓存,在内存充足时保留对象,内存紧张时释放。

特点

  • 比强引用更容易被回收。
  • 不会导致 OutOfMemoryError(除非内存不足且软引用对象已被回收)。

示例

java 复制代码
import java.lang.ref.SoftReference;

public class SoftRefDemo {
    public static void main(String[] args) {
        SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024 * 10]); // 10MB
        System.out.println("Before GC: " + softRef.get());

        System.gc(); // 触发 GC
        System.out.println("After GC: " + softRef.get()); // 可能仍存在

        // 模拟内存不足
        try {
            byte[] bigData = new byte[1024 * 1024 * 100]; // 100MB
        } catch (Throwable e) {
            System.out.println("After OOM: " + softRef.get()); // 可能为 null
        }
    }
}

适用场景

  • 图片缓存、数据缓存等,内存不足时自动释放。

3. 弱引用(Weak Reference)

概念

  • GC 发现弱引用对象后立即回收,不管内存是否充足。
  • 生命周期非常短。

特点

  • 适合存储一些不重要的对象,随时可能被回收。
  • 常用于 WeakHashMap(键使用弱引用)。

示例

java 复制代码
import java.lang.ref.WeakReference;

public class WeakRefDemo {
    public static void main(String[] args) {
        WeakReference<Object> weakRef = new WeakReference<>(new Object());
        System.out.println("Before GC: " + weakRef.get());

        System.gc(); // 触发 GC
        System.out.println("After GC: " + weakRef.get()); // 一定为 null
    }
}

适用场景

  • 缓存中存放不重要数据。
  • 避免内存泄漏(如监听器、回调对象)。

4. 虚引用(Phantom Reference)

概念

  • 最弱的引用类型,对象几乎不能通过虚引用访问。
  • 主要用于跟踪对象被回收的状态
  • 必须与 引用队列(ReferenceQueue) 配合使用。

特点

  • get() 方法永远返回 null
  • 用于在对象被 GC 回收前执行一些清理操作(类似 finalize())。

示例

java 复制代码
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomRefDemo {
    public static void main(String[] args) {
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);

        System.out.println("phantomRef.get(): " + phantomRef.get()); // 永远为 null

        System.gc();
        System.out.println("From queue: " + queue.poll()); // 对象被回收后,引用进入队列
    }
}

适用场景

  • 管理堆外内存(如直接内存)。
  • 在对象回收前做资源释放。

5. 对比表

引用类型 GC 回收时机 是否可通过 get() 获取对象 适用场景
强引用 永不回收(除非断开引用) 普通对象引用
软引用 内存不足时回收 缓存(图片、数据)
弱引用 下一次 GC 必回收 不重要数据、避免内存泄漏
虚引用 回收前进入引用队列 跟踪对象回收、资源释放

总结

  • 强引用:最常用,保证对象不被回收。
  • 软引用:适合缓存,内存不足时释放。
  • 弱引用:随时可能被回收,适合存放不重要数据。
  • 虚引用:用于跟踪对象回收状态,不能直接访问对象。

垃圾回收的主要算法有哪些?

1. 垃圾回收的主要算法概览

在 JVM 中,常见的 GC 算法主要有:

  1. 标记-清除(Mark-Sweep)
  2. 复制(Copying)
  3. 标记-整理(Mark-Compact)
  4. 分代收集(Generational Collection)

2. 标记-清除(Mark-Sweep)算法

原理

  • 第一阶段:标记
    从 GC Roots 出发,标记所有可达对象。
  • 第二阶段:清除
    遍历堆内存,回收未被标记的对象。

流程

复制代码
GC Roots → 可达性分析 → 标记存活对象 → 清除未标记对象

优点

  • 实现简单。
  • 不需要移动对象。

缺点

  • 内存碎片化:清除后内存不连续,可能导致大对象无法分配。
  • 回收速度较慢。

示例

java 复制代码
// 模拟标记-清除
List<Object> heap = new ArrayList<>();
Object live = new Object(); // 存活对象
heap.add(live);
heap.add(new Object()); // 垃圾对象

// 标记阶段
Set<Object> marked = new HashSet<>();
marked.add(live);

// 清除阶段
heap.removeIf(obj -> !marked.contains(obj));

3. 复制(Copying)算法

原理

  • 将内存分为两块(From 和 To)。
  • GC 时只遍历 From 区,将存活对象复制到 To 区,然后清空 From 区。

流程

复制代码
From 区 → 标记存活对象 → 复制到 To 区 → 清空 From 区 → 交换角色

优点

  • 无碎片化:复制后内存连续。
  • 分配速度快(指针碰撞)。

缺点

  • 需要额外的内存空间(只能使用一半内存)。
  • 复制成本高(如果存活对象多)。

示例

java 复制代码
List<Object> from = new ArrayList<>();
List<Object> to = new ArrayList<>();

Object live = new Object();
from.add(live);
from.add(new Object()); // 垃圾

// 复制阶段
for (Object obj : from) {
    if (obj == live) {
        to.add(obj);
    }
}
from.clear(); // 清空 From

4. 标记-整理(Mark-Compact)算法

原理

  • 类似标记-清除,但在清除阶段会移动存活对象,让它们在内存中连续排列。
  • 解决了标记-清除的碎片化问题。

流程

复制代码
GC Roots → 标记存活对象 → 移动存活对象 → 清理剩余空间

优点

  • 无碎片化。
  • 不需要额外内存空间。

缺点

  • 移动对象成本高(需要更新引用)。

示例

java 复制代码
List<Object> heap = new ArrayList<>();
Object live1 = new Object();
Object live2 = new Object();
heap.add(live1);
heap.add(new Object()); // 垃圾
heap.add(live2);

// 标记阶段
List<Object> marked = Arrays.asList(live1, live2);

// 整理阶段
heap.clear();
heap.addAll(marked); // 移动到连续空间

5. 分代收集(Generational Collection)算法

原理

  • 根据对象生命周期的不同,将堆分为:
    • 新生代(Young Generation) :对象存活率低,使用复制算法
    • 老年代(Old Generation) :对象存活率高,使用标记-整理算法
  • 新生代 GC 称为 Minor GC ,老年代 GC 称为 Major GCFull GC

流程

复制代码
新生代 → 复制算法 → 存活对象晋升到老年代
老年代 → 标记-整理算法

优点

  • 针对不同对象生命周期优化回收策略。
  • 提高 GC 效率。

缺点

  • 需要维护多块内存区域。
  • 老年代 GC 停顿时间长。

示例

java 复制代码
List<Object> youngGen = new ArrayList<>();
List<Object> oldGen = new ArrayList<>();

Object shortLife = new Object();
Object longLife = new Object();

youngGen.add(shortLife);
youngGen.add(longLife);

// Minor GC
List<Object> survivors = new ArrayList<>();
survivors.add(longLife); // 存活对象晋升
oldGen.addAll(survivors);
youngGen.clear();

6. 对比表

算法 原理 优点 缺点 适用场景
标记-清除 标记存活对象,清除未标记对象 实现简单 内存碎片化 老年代(存活率高)
复制 存活对象复制到另一块内存 无碎片化,分配快 占用双倍内存 新生代(存活率低)
标记-整理 标记存活对象并移动 无碎片化 移动成本高 老年代
分代收集 新生代复制,老年代整理 高效,针对性强 老年代停顿长 JVM 默认策略

总结

  • 标记-清除:简单但有碎片。
  • 复制:无碎片但浪费空间。
  • 标记-整理:无碎片但移动成本高。
  • 分代收集:结合多种算法,效率最高,是 JVM 默认选择。

标记-清除、标记-整理、复制算法的原理?

1. 标记-清除(Mark-Sweep)算法

核心原理

  1. 标记阶段(Mark)
    • GC Roots 出发,使用可达性分析遍历对象引用链。
    • 将所有可达对象打上"存活"标记。
  2. 清除阶段(Sweep)
    • 遍历整个堆空间,回收所有未被标记的对象。
    • 清除只是将内存标记为空闲,不会移动存活对象。

内存变化示意

复制代码
[存活][垃圾][存活][垃圾][垃圾] → 清除垃圾 → [存活][空闲][存活][空闲][空闲]

存活对象位置不变,空闲内存分散,容易产生碎片。


优点

  • 实现简单。
  • 不需要移动对象,引用地址不变。

缺点

  • 内存碎片化:清除后空闲内存不连续,可能导致大对象无法分配。
  • 清除阶段需要遍历整个堆,速度较慢。

JVM 应用场景

  • 老年代(Old Generation)中对象存活率高,移动对象成本大,因此常用标记-清除。


2. 标记-整理(Mark-Compact)算法

核心原理

  1. 标记阶段(Mark)
    • 与标记-清除相同,从 GC Roots 出发标记存活对象。
  2. 整理阶段(Compact)
    • 将所有存活对象向一端移动,使内存连续。
    • 更新所有引用地址(因为对象位置发生变化)。

内存变化示意

复制代码
[存活][垃圾][存活][垃圾][垃圾] → 整理 → [存活][存活][空闲][空闲][空闲]

存活对象被压缩到一端,空闲内存连续,避免碎片化。


优点

  • 无碎片化:内存连续,分配效率高。
  • 适合存活率高的区域(如老年代)。

缺点

  • 移动对象需要更新引用,成本高。
  • GC 停顿时间长(Stop-The-World)。

JVM 应用场景

  • 老年代(Old Generation)在 Full GC 时常用标记-整理,保证内存连续性。


3. 复制(Copying)算法

核心原理

  1. 内存分区
    • 将内存分为两块等大小的区域:From 区To 区
  2. 复制阶段
    • GC 时只遍历 From 区,将存活对象复制到 To 区。
    • 清空 From 区,交换两区角色。

内存变化示意

复制代码
From: [存活][垃圾][存活][垃圾]  
To:   [] → 复制 → [存活][存活]  
From 清空 → 交换角色

复制后内存连续,无碎片。


优点

  • 无碎片化:复制后内存连续。
  • 分配速度快(指针碰撞)。
  • 只处理存活对象,效率高(适合存活率低的区域)。

缺点

  • 需要双倍内存空间(只能使用一半)。
  • 存活对象多时复制成本高。

JVM 应用场景

  • 新生代(Young Generation)对象存活率低,复制算法效率高。
  • HotSpot JVM 的 Minor GC 默认使用复制算法。


4. 三种算法对比表

算法 内存碎片 是否移动对象 适用场景 优点 缺点
标记-清除 老年代 实现简单 碎片化严重
标记-整理 老年代 内存连续 移动成本高
复制 新生代 分配快 占用双倍内存

5. JVM 为什么分代选择不同算法

  • 新生代 :对象生命周期短,存活率低 → 复制算法(只复制少量存活对象,效率高)。
  • 老年代 :对象生命周期长,存活率高 → 标记-清除标记-整理(减少移动对象的成本,保证内存连续性)。

总结

  • 标记-清除:简单但有碎片,适合老年代。
  • 标记-整理:无碎片但移动成本高,适合老年代 Full GC。
  • 复制:无碎片且分配快,适合新生代。

分代收集的原理是什么?

1. 为什么需要分代收集

在 JVM 中,所有对象都存放在堆内存中,但不同对象的生命周期差异很大:

  • 短命对象:如方法中的局部变量、临时计算结果,可能在几毫秒内就失效。
  • 长命对象:如缓存、单例对象、线程池,可能在整个程序运行期间都存在。

如果对所有对象使用同一种 GC 算法:

  • 短命对象会频繁创建和销毁,导致 GC 频繁触发。
  • 长命对象每次 GC 都要扫描,浪费时间。

分代收集的核心思想

根据对象的生命周期长短,将堆内存划分为不同区域,并针对不同区域选择最合适的 GC 算法。


2. 堆内存的分代结构

JVM 将堆分为两个主要区域:

  1. 新生代(Young Generation)
    • 存放新创建的对象。
    • 对象存活率低(大部分很快就会被回收)。
    • 进一步细分为:
      • Eden 区:新对象首先分配在这里。
      • Survivor 区 :分为 S0S1 两个区,用于保存上一次 GC 存活的对象。
  2. 老年代(Old Generation)
    • 存放生命周期长的对象。
    • 对象存活率高。
    • 主要存放从新生代晋升过来的对象。

内存布局示意

复制代码
堆内存
├── 新生代 (Young)
│   ├── Eden
│   ├── Survivor 0 (S0)
│   └── Survivor 1 (S1)
└── 老年代 (Old)

3. 分代收集的工作机制

(1) 对象创建

  • 新对象优先分配在 Eden 区
  • 如果 Eden 空间不足,触发 Minor GC

(2) Minor GC(新生代垃圾回收)

  • 使用 复制算法
    1. 从 GC Roots 出发,标记 Eden 和当前 Survivor 区的存活对象。
    2. 将存活对象复制到另一个 Survivor 区。
    3. 对象的**年龄(Age)**加 1。
    4. 如果对象年龄超过阈值(默认 15),晋升到老年代。
  • 清空 Eden 和原 Survivor 区。

(3) Major GC / Full GC(老年代垃圾回收)

  • 老年代对象存活率高,使用 标记-整理算法
    1. 标记存活对象。
    2. 移动存活对象到内存一端,保证连续性。
    3. 清理剩余空间。
  • Full GC 会同时回收新生代和老年代。

(4) 晋升规则

  • 对象在 Survivor 区经历多次 Minor GC 后,年龄超过阈值 → 晋升到老年代。
  • 大对象(如大数组)可能直接进入老年代(避免复制开销)。

4. GC 类型总结

GC 类型 作用范围 触发条件 使用算法
Minor GC 新生代 Eden 空间不足 复制算法
Major GC 老年代 老年代空间不足 标记-整理
Full GC 新生代 + 老年代 多种情况(如 System.gc()) 新生代复制 + 老年代标记-整理

5. 分代收集的优点

  • 高效:新生代对象存活率低,复制算法只处理少量存活对象,速度快。
  • 减少停顿:老年代 GC 不会频繁触发。
  • 内存利用率高:不同区域使用最适合的算法。

6. 分代收集的缺点

  • 老年代 GC(Major GC)停顿时间长。
  • Survivor 区大小需要合理配置,否则晋升过快导致老年代压力大。
  • 大对象直接进入老年代可能引发 Full GC。

7. JVM 中的实现细节(HotSpot)

  • 默认分代比例:新生代约占堆的 1/3,老年代约占 2/3。
  • Eden:Survivor 默认比例为 8:1:1
  • 晋升阈值 :对象年龄超过 15(可通过 -XX:MaxTenuringThreshold 调整)。
  • 大对象直接进入老年代 :可通过 -XX:PretenureSizeThreshold 控制。

8. 分代收集流程图

复制代码
对象创建 → Eden
Eden 满 → Minor GC → 存活对象 → Survivor
对象年龄增加 → 超过阈值 → 晋升到老年代
老年代满 → Major GC / Full GC

9. 示例代码

java 复制代码
public class GCDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            byte[] data = new byte[1024 * 1024]; // 1MB
        }
        System.gc(); // 可能触发 Full GC
    }
}

在这个例子中,大量短命对象会在 Eden 中被快速回收,少量存活对象晋升到老年代。


总结

  • 分代收集是 JVM GC 的核心策略,通过将堆分为新生代和老年代,分别使用不同算法(复制、标记-整理)来提高效率。
  • 新生代 → 复制算法(存活率低,速度快)。
  • 老年代 → 标记-整理算法(存活率高,避免碎片)。
  • 这种设计充分利用了对象生命周期的差异,减少了 GC 的总开销。

新生代、老年代的 GC 策略?

1. 新生代(Young Generation)GC 策略

1.1 内存结构

新生代主要存放新创建的对象 ,生命周期短,存活率低。

它被进一步划分为:

  • Eden 区:新对象首先分配在这里。
  • Survivor 0 (S0)Survivor 1 (S1):两个幸存区,交替使用,用于保存上一次 GC 存活的对象。

默认比例Eden : S0 : S1 = 8 : 1 : 1

(可通过 -XX:SurvivorRatio 调整)


1.2 GC 类型

  • Minor GC(新生代 GC)
  • 只回收新生代,不影响老年代。
  • 触发频率高,但速度快(因为存活对象少)。

1.3 回收算法

复制算法(Copying)

  1. GC Roots 出发,标记 Eden 和当前 Survivor 区的存活对象。
  2. 将存活对象复制到另一个 Survivor 区。
  3. 对象的**年龄(Age)**加 1。
  4. 如果对象年龄超过阈值(默认 15,可通过 -XX:MaxTenuringThreshold 调整),晋升到老年代。
  5. 清空 Eden 和原 Survivor 区。

1.4 触发条件

  • Eden 区满时触发 Minor GC。
  • Survivor 区不足时,部分对象会直接晋升到老年代(称为晋升失败)。

1.5 优缺点

优点

  • 存活对象少,复制成本低。
  • 内存整理后连续,分配速度快(指针碰撞)。

缺点

  • 需要额外 Survivor 区空间。
  • Survivor 区过小会导致对象过早晋升到老年代,增加老年代压力。

1.6 JVM 调优参数

  • -XX:NewRatio:新生代与老年代的比例(默认 1:2)。
  • -XX:SurvivorRatio:Eden 与 Survivor 的比例(默认 8:1:1)。
  • -XX:MaxTenuringThreshold:对象晋升老年代的年龄阈值(默认 15)。

2. 老年代(Old Generation)GC 策略

2.1 内存结构

  • 存放生命周期长、存活率高的对象。
  • 包括:
    • 从新生代晋升的对象。
    • 大对象(超过 -XX:PretenureSizeThreshold 阈值)可能直接进入老年代。

2.2 GC 类型

  • Major GC(只回收老年代)
  • Full GC(回收新生代 + 老年代 + 方法区)
  • 触发频率低,但停顿时间长。

2.3 回收算法

标记-整理(Mark-Compact)

  1. 标记阶段:从 GC Roots 出发,标记所有存活对象。
  2. 整理阶段:将存活对象移动到内存一端,保持内存连续。
  3. 清理剩余空间。

有些老年代 GC(如 CMS)使用标记-清除(Mark-Sweep),但会产生碎片,需要配合整理阶段。


2.4 触发条件

  • 老年代空间不足时触发 Major GC。
  • 新生代晋升对象时,老年代空间不足会触发 Full GC。
  • 调用 System.gc()(默认会触发 Full GC,可用 -XX:+DisableExplicitGC 禁用)。

2.5 优缺点

优点

  • 适合存活率高的区域,减少对象复制成本。
  • 内存整理后连续,避免碎片化。

缺点

  • 停顿时间长(Stop-The-World)。
  • 移动对象需要更新引用,成本高。

2.6 JVM 调优参数

  • -XX:NewRatio:新生代与老年代比例。
  • -XX:PretenureSizeThreshold:大对象直接进入老年代的阈值。
  • -XX:CMSInitiatingOccupancyFraction:CMS GC 触发的老年代使用率阈值。

3. 新生代 vs 老年代 GC 策略对比

对比项 新生代 GC(Minor GC) 老年代 GC(Major/Full GC)
触发条件 Eden 满 老年代满 / 晋升失败
算法 复制算法 标记-整理(或标记-清除)
存活率
速度
停顿时间
频率
适用对象 新创建、短命对象 长期存活对象、大对象

4. GC 流程示意

复制代码
对象创建 → Eden
Eden 满 → Minor GC → 存活对象 → Survivor
对象年龄增加 → 超过阈值 → 晋升到老年代
老年代满 → Major GC / Full GC

5. 总结

  • 新生代 GC(Minor GC) :频繁触发,速度快,使用复制算法,适合短命对象。
  • 老年代 GC(Major/Full GC) :触发频率低,停顿时间长,使用标记-整理算法,适合长期存活对象。
  • 调优关键:合理分配新生代与老年代比例,避免对象过早晋升,减少 Full GC 频率。

Minor GC 与 Full GC 的区别?

1. 定义

Minor GC(新生代 GC)

  • **只回收新生代(Young Generation)**的垃圾对象。
  • 新生代包括 Eden 区 和两个 Survivor 区(S0、S1)
  • 触发频率高,但速度快,因为新生代对象存活率低

Full GC(全堆 GC)

  • **回收整个堆(新生代 + 老年代 + 方法区/元空间)**的垃圾对象。
  • 触发频率低,但停顿时间长,因为需要扫描和回收更多对象。
  • Full GC 通常包含一次 Major GC(老年代 GC)+ 新生代 GC。

2. 触发条件

GC 类型 触发条件
Minor GC Eden 区满时触发
Full GC 老年代满;方法区/元空间满;调用 System.gc()(默认);晋升失败(新生代对象晋升老年代时空间不足)

3. 回收范围

GC 类型 回收范围
Minor GC 仅新生代(Eden + 一个 Survivor 区)
Full GC 新生代 + 老年代 + 方法区(元空间)

4. 使用算法

GC 类型 常用算法
Minor GC 复制算法(Copying):存活对象少,复制成本低,内存整理后连续
Full GC 新生代:复制算法;老年代:标记-整理(Mark-Compact)或标记-清除(Mark-Sweep)

5. 执行流程

Minor GC 流程

  1. GC Roots 出发,标记 Eden 和当前 Survivor 区的存活对象。
  2. 将存活对象复制到另一个 Survivor 区(或晋升到老年代)。
  3. 清空 Eden 和原 Survivor 区。
  4. 对象年龄(Age)+1,超过阈值(默认 15)晋升到老年代。

Full GC 流程

  1. 回收新生代(复制算法)。
  2. 回收老年代(标记-整理或标记-清除)。
  3. 回收方法区/元空间(卸载无用类、常量池等)。
  4. 可能伴随类卸载、软引用/弱引用/虚引用的清理。

6. 性能影响

对比项 Minor GC Full GC
触发频率
停顿时间 短(毫秒级) 长(可能秒级)
回收范围 新生代 整个堆
存活率
执行速度
影响 轻微卡顿 明显卡顿,可能影响吞吐量

7. JVM 调优建议

减少 Full GC 频率

  • 增大新生代比例(-XX:NewRatio),减少对象过早晋升。
  • 调整 Survivor 区比例(-XX:SurvivorRatio),减少晋升失败。
  • 增大老年代容量(-Xmx / -Xms)。
  • 避免频繁调用 System.gc()(可用 -XX:+DisableExplicitGC 禁用)。
  • 优化代码,减少大对象直接进入老年代(-XX:PretenureSizeThreshold)。

优化 Minor GC

  • 保证 Survivor 区足够大,减少晋升到老年代的对象数量。
  • 调整 -XX:MaxTenuringThreshold,延迟对象晋升。

8. 对比总结表

对比项 Minor GC Full GC
回收范围 新生代 新生代 + 老年代 + 方法区
触发条件 Eden 满 老年代满、元空间满、晋升失败、System.gc()
算法 复制算法 新生代复制 + 老年代标记-整理/清除
存活率
速度
停顿时间
频率
影响 轻微卡顿 明显卡顿

9. 形象类比

  • Minor GC:像清理桌面上的垃圾纸屑(快、频繁)。
  • Full GC:像打扫整个房子(慢、彻底、频率低)。

总结

  • Minor GC:频繁、快、影响小,主要清理短命对象。
  • Full GC:少见、慢、影响大,清理整个堆和方法区。
  • 调优目标:减少 Full GC 频率,让大部分垃圾在 Minor GC 阶段就被清理掉。

常见的垃圾收集器有哪些?

1. 垃圾收集器分类

在 HotSpot JVM 中,垃圾收集器主要分为两类:

  1. 新生代收集器 (Young Generation GC)
    • Serial GC
    • ParNew GC
    • Parallel Scavenge GC
  2. 老年代收集器 (Old Generation GC)
    • Serial Old GC
    • Parallel Old GC
    • CMS(Concurrent Mark Sweep)GC
    • G1(Garbage First)GC

新生代收集器通常使用复制算法 ,老年代收集器通常使用标记-整理标记-清除算法。


2. 新生代收集器

2.1 Serial GC

  • 原理 :单线程执行 GC,Stop-The-World ,使用复制算法回收新生代。
  • 执行流程
    1. 停止所有用户线程。
    2. 从 GC Roots 出发,标记 Eden 和 Survivor 区的存活对象。
    3. 将存活对象复制到另一个 Survivor 区或晋升到老年代。
    4. 清空 Eden 和原 Survivor 区。
  • 优点
    • 实现简单,单线程无线程切换开销。
    • 在小堆内存环境下效率高。
  • 缺点
    • 停顿时间长,不适合多核 CPU。
  • 适用场景
    • 单核 CPU 或客户端应用(如桌面程序)。

2.2 ParNew GC

  • 原理 :Serial GC 的多线程版本,使用复制算法回收新生代。
  • 执行流程
    1. 多线程并行标记存活对象。
    2. 并行复制到 Survivor 区或晋升到老年代。
    3. 清空 Eden。
  • 优点
    • 利用多核 CPU 提高新生代回收速度。
    • 可与 CMS 搭配使用(CMS 只能配合 ParNew 作为新生代收集器)。
  • 缺点
    • 多线程会有额外的同步开销。
  • 适用场景
    • 多核服务器,低延迟需求。

2.3 Parallel Scavenge GC

  • 原理 :多线程新生代收集器,使用复制算法 ,但目标是高吞吐量(Throughput)。
  • 执行流程
    1. 多线程并行标记存活对象。
    2. 并行复制到 Survivor 区或晋升到老年代。
    3. 清空 Eden。
  • 优点
    • 吞吐量高(运行用户代码时间 / 总时间)。
    • 可自动调节新生代大小和晋升阈值(自适应调节策略)。
  • 缺点
    • 停顿时间可能较长,不适合低延迟场景。
  • 适用场景
    • 后台计算任务、批处理系统。

3. 老年代收集器

3.1 Serial Old GC

  • 原理 :单线程老年代收集器,使用标记-整理算法
  • 执行流程
    1. 停止所有用户线程。
    2. 标记老年代存活对象。
    3. 移动存活对象到内存一端。
    4. 清理剩余空间。
  • 优点
    • 实现简单。
  • 缺点
    • 停顿时间长。
  • 适用场景
    • 单核 CPU,或与 Serial GC 搭配使用。

3.2 Parallel Old GC

  • 原理 :Parallel Scavenge 的老年代版本,使用标记-整理算法
  • 执行流程
    1. 多线程并行标记老年代存活对象。
    2. 并行移动存活对象。
    3. 清理剩余空间。
  • 优点
    • 吞吐量高。
    • 与 Parallel Scavenge 搭配使用,适合高吞吐量场景。
  • 缺点
    • 停顿时间较长。
  • 适用场景
    • 大数据计算、后台任务。

3.3 CMS(Concurrent Mark Sweep)GC

  • 原理 :老年代收集器,使用标记-清除算法 ,目标是低停顿
  • 执行流程
    1. 初始标记(Stop-The-World):标记 GC Roots 直接引用的对象。
    2. 并发标记:与用户线程并发执行,标记所有可达对象。
    3. 重新标记(Stop-The-World):修正并发标记阶段的变动。
    4. 并发清除:与用户线程并发执行,清理未标记对象。
  • 优点
    • 停顿时间短,适合低延迟场景。
  • 缺点
    • 会产生内存碎片。
    • 并发阶段会占用 CPU 资源。
  • 适用场景
    • Web 服务器、低延迟应用。

3.4 G1(Garbage First)GC

  • 原理 :面向服务端的低延迟收集器,将堆划分为多个Region,同时回收新生代和老年代。
  • 执行流程
    1. 将堆划分为多个 Region(新生代、老年代混合分布)。
    2. 优先回收垃圾最多的 Region(Garbage First)。
    3. 使用复制算法在 Region 内移动对象,避免碎片化。
    4. 支持并发标记和清理。
  • 优点
    • 可预测停顿时间(-XX:MaxGCPauseMillis)。
    • 无碎片化。
  • 缺点
    • 实现复杂,调优难度大。
  • 适用场景
    • 大堆内存、低延迟、高吞吐量并重的应用。

4. 收集器对比表

收集器 代别 算法 并行/并发 优点 缺点 适用场景
Serial GC 新生代 复制 单线程 简单高效(小堆) 停顿长 单核、小堆
ParNew GC 新生代 复制 并行 多核利用率高 同步开销 多核低延迟
Parallel Scavenge 新生代 复制 并行 高吞吐量 停顿长 后台计算
Serial Old 老年代 标记-整理 单线程 简单 停顿长 单核
Parallel Old 老年代 标记-整理 并行 高吞吐量 停顿长 大数据计算
CMS 老年代 标记-清除 并发 停顿短 碎片化 Web 服务
G1 混合 复制+标记整理 并发 可预测停顿 调优复杂 大堆低延迟

总结

  • 吞吐量优先:Parallel Scavenge + Parallel Old。
  • 低延迟优先:ParNew + CMS,或直接使用 G1。
  • 小堆单核:Serial GC + Serial Old。

垃圾收集器(Garbage Collector)和垃圾回收算法(Garbage Collection Algorithm)之间的关系

1. 概念区分

垃圾回收算法

  • 定义 :一种理论方法,用于决定如何识别和回收内存中不再使用的对象。
  • 作用 :描述回收的策略和步骤,不关心具体实现。
  • 常见算法
    • 标记-清除(Mark-Sweep)
    • 标记-整理(Mark-Compact)
    • 复制(Copying)
    • 分代收集(Generational Collection)(其实是策略,内部结合多种算法)

垃圾收集器

  • 定义 :垃圾回收算法的具体实现,运行在 JVM 中的一个模块。
  • 作用 :根据选定的算法和策略,结合硬件和应用场景,执行垃圾回收的实际操作
  • 常见收集器
    • Serial GC
    • ParNew GC
    • Parallel Scavenge GC
    • CMS GC
    • G1 GC

2. 区别

对比项 垃圾回收算法 垃圾收集器
性质 理论策略 实际实现
关注点 如何回收(步骤、逻辑) 如何执行(线程数、并发、优化)
抽象层级 高层(概念) 低层(代码实现)
是否依赖硬件 不直接依赖 会根据 CPU 核数、内存大小优化
示例 标记-清除、复制算法 CMS、G1、Parallel Scavenge

3. 联系

  • 垃圾收集器是垃圾回收算法的实现者
  • 一个收集器可能只实现一种算法,也可能组合多种算法。
  • 收集器会根据**代(新生代/老年代)**选择不同算法:
    • 新生代 → 复制算法(存活率低,复制快)
    • 老年代 → 标记-整理标记-清除(存活率高,减少移动)

4. 举例说明

G1 GC 为例:

  • 算法层面
    • 新生代 Region → 复制算法
    • 老年代 Region → 标记-整理算法
    • 并发标记阶段 → 可达性分析
  • 收集器层面
    • 将堆划分为多个 Region。
    • 预测停顿时间(-XX:MaxGCPauseMillis)。
    • 优先回收垃圾最多的 Region(Garbage First 策略)。
    • 并发执行标记和清理,减少停顿。

5. 总结关系

  • 算法方法论,定义了"怎么做"。
  • 收集器工程实现,定义了"怎么高效地做"。
  • 收集器会根据不同代别、硬件条件、应用需求,选择或组合不同算法来实现垃圾回收。

一句话总结

垃圾回收算法是理论,垃圾收集器是实践;收集器是算法的执行者,并会在实际运行中结合多线程、并发、分代等优化策略,让算法在不同场景下高效运行。

Serial、Parallel、CMS、G1 的特点与适用场景?

1. Serial GC

原理

  • 单线程垃圾收集器。
  • 新生代使用复制算法 ,老年代使用标记-整理算法
  • GC 时会触发 Stop-The-World(STW),暂停所有用户线程。

执行流程

  1. 停止所有用户线程。
  2. 新生代:复制存活对象到 Survivor 区或晋升到老年代。
  3. 老年代:标记存活对象并整理内存。
  4. 恢复用户线程。

特点

  • 实现简单,单线程无多线程切换开销。
  • 在小堆内存环境下效率高。
  • 停顿时间长,不适合多核 CPU。

适用场景

  • 单核 CPU。
  • 小堆内存(如桌面应用、嵌入式系统)。
  • 对停顿时间不敏感的应用。

2. Parallel GC(Parallel Scavenge + Parallel Old)

原理

  • 多线程 垃圾收集器,目标是高吞吐量(Throughput)。
  • 新生代使用复制算法(Parallel Scavenge)。
  • 老年代使用标记-整理算法(Parallel Old)。
  • 可自动调节新生代大小和晋升阈值(自适应调节策略)。

执行流程

  1. 多线程并行标记存活对象。
  2. 新生代:并行复制到 Survivor 区或晋升到老年代。
  3. 老年代:并行标记并整理内存。
  4. 恢复用户线程。

特点

  • 吞吐量高(运行用户代码时间 / 总时间)。
  • 停顿时间可能较长。
  • 适合后台计算任务。

适用场景

  • 多核 CPU。
  • 大堆内存。
  • 吞吐量优先的场景(如批处理、大数据计算)。

3. CMS(Concurrent Mark Sweep)GC

原理

  • 老年代收集器,目标是低停顿
  • 使用标记-清除算法,并发执行部分 GC 阶段。
  • 新生代通常搭配 ParNew GC

执行流程

  1. 初始标记(STW):标记 GC Roots 直接引用的对象。
  2. 并发标记:与用户线程并发执行,标记所有可达对象。
  3. 重新标记(STW):修正并发标记阶段的变动。
  4. 并发清除:与用户线程并发执行,清理未标记对象。

特点

  • 停顿时间短,适合低延迟场景。
  • 并发阶段会占用 CPU 资源。
  • 会产生内存碎片(标记-清除不整理内存)。

适用场景

  • Web 服务器。
  • 对响应时间要求高的应用。
  • 多核 CPU。

4. G1(Garbage First)GC

原理

  • 面向服务端的低延迟收集器。
  • 将堆划分为多个Region(新生代、老年代混合分布)。
  • 优先回收垃圾最多的 Region(Garbage First 策略)。
  • 使用复制算法在 Region 内移动对象,避免碎片化。
  • 支持并发标记和清理。

执行流程

  1. 将堆划分为多个 Region。
  2. 新生代 Region:复制算法回收。
  3. 老年代 Region:标记-整理回收。
  4. 并发标记阶段:可达性分析。
  5. 根据停顿时间目标(-XX:MaxGCPauseMillis)选择回收的 Region。

特点

  • 可预测停顿时间。
  • 无碎片化。
  • 调优复杂。

适用场景

  • 大堆内存(数 GB 以上)。
  • 低延迟、高吞吐量并重的应用。
  • 多核 CPU。

5. 对比总结表

收集器 线程模式 新生代算法 老年代算法 停顿时间 吞吐量 碎片化 适用场景
Serial 单线程 复制 标记-整理 单核、小堆
Parallel 多线程 复制 标记-整理 中长 多核、大堆、吞吐量优先
CMS 并发 复制(ParNew) 标记-清除 多核、低延迟
G1 并发 复制 标记-整理 可控 大堆、低延迟

总结

  • 停顿时间优先:CMS、G1。
  • 吞吐量优先:Parallel GC。
  • 小堆单核:Serial GC。
  • 大堆低延迟:G1 GC。

ZGC 与 Shenandoah 的特点?

1. ZGC(Z Garbage Collector)

设计目标

  • 超低停顿时间 :单次 GC 停顿时间通常 < 10ms
  • 支持超大堆内存(可达 TB 级)。
  • 几乎所有 GC 阶段都与应用线程并发执行。

核心原理

  • Region-based:将堆划分为多个大小可变的 Region。
  • Colored Pointers(彩色指针):在对象引用中嵌入标记位,用于记录 GC 状态(如是否已移动)。
  • Load Barriers(加载屏障):在对象访问时检查并修正引用,保证并发移动对象的安全性。
  • 并发压缩:对象移动和压缩在并发阶段完成,避免长时间 STW。

执行流程

  1. 并发标记:与应用线程并发标记存活对象。
  2. 并发重定位:将存活对象移动到新的 Region。
  3. 并发重映射:修正引用指向新的位置。
  4. 短暂 STW:仅在少数阶段(如初始标记)暂停应用线程,时间极短。

优点

  • 停顿时间极短,几乎与堆大小无关。
  • 支持超大堆内存。
  • 无碎片化(并发压缩)。

缺点

  • 实现复杂,调试难度大。
  • 对 CPU 和内存有一定额外开销。
  • 需要较新版本的 JDK(JDK 11+)。

适用场景

  • 大堆内存(数百 GB ~ TB)。
  • 对延迟极度敏感的应用(金融交易、实时系统)。
  • 多核高性能服务器。

2. Shenandoah GC

设计目标

  • 低停顿时间 :单次 GC 停顿时间通常 < 10ms
  • 堆大小对停顿时间影响很小。
  • 尽量将所有 GC 阶段并发化。

核心原理

  • Region-based:堆划分为固定大小的 Region。
  • Brooks Pointers(转发指针):对象头中存储一个转发地址,用于在对象移动时快速定位新位置。
  • Concurrent Compaction(并发压缩):对象移动在并发阶段完成。
  • Write Barriers(写屏障):在对象引用更新时记录变动,保证并发压缩的正确性。

执行流程

  1. 并发标记:与应用线程并发标记存活对象。
  2. 并发压缩:移动对象到新的 Region。
  3. 引用更新:通过转发指针修正引用。
  4. 短暂 STW:仅在初始标记和最终引用更新阶段暂停应用线程。

优点

  • 停顿时间短,几乎与堆大小无关。
  • 并发压缩避免碎片化。
  • 适合低延迟场景。

缺点

  • 实现复杂,调试难度大。
  • 对 CPU 有额外开销。
  • 需要 JDK 12+(或 OpenJDK 特定版本)。

适用场景

  • 中到大堆内存(几十 GB ~ 数百 GB)。
  • 对延迟敏感的应用(在线服务、游戏服务器)。
  • 多核高性能服务器。

3. ZGC vs Shenandoah 对比表

特性 ZGC Shenandoah
设计目标 超低停顿,支持 TB 级堆 低停顿,堆大小对停顿影响小
堆划分 可变大小 Region 固定大小 Region
引用修正方式 彩色指针 + 加载屏障 转发指针 + 写屏障
并发压缩 支持 支持
停顿时间 < 10ms < 10ms
堆大小支持 TB 级 数百 GB
JDK版本 JDK 11+ JDK 12+(或 OpenJDK)
适用场景 超大堆、极低延迟 中大堆、低延迟
额外开销 CPU + 内存 CPU

总结

  • ZGC 更适合 超大堆内存极低延迟场景,技术更先进(彩色指针)。
  • Shenandoah 更适合 中到大堆 的低延迟应用,设计更偏向通用性。
  • 两者都通过并发标记 + 并发压缩实现低停顿,并且堆大小对停顿时间影响极小。

如何触发 GC?

1. 自动触发 GC

JVM 会根据内存使用情况自动触发 GC,不需要开发者干预。不同类型的 GC 触发条件如下:

1.1 Minor GC(新生代 GC)触发条件

  • Eden 区满时触发。
  • 新生代对象存活率低,GC 频率高但速度快。
  • 过程:
    1. 从 GC Roots 出发标记存活对象。
    2. 将存活对象复制到 Survivor 区或晋升到老年代。
    3. 清空 Eden 区。

1.2 Major GC(老年代 GC)触发条件

  • 老年代空间不足时触发。
  • 存活率高,GC 停顿时间长。
  • 通常伴随一次 Minor GC(但不一定是 Full GC)。

1.3 Full GC(整堆 GC)触发条件

  • 老年代空间不足。
  • 元空间(方法区)空间不足。
  • 调用 System.gc()(默认会触发 Full GC)。
  • 新生代对象晋升到老年代时空间不足(晋升失败)。
  • CMS GC 的并发收集失败(Concurrent Mode Failure)。
  • G1 GC 的混合回收阶段需要回收老年代 Region。

2. 手动触发 GC

虽然 JVM 会自动管理内存,但我们也可以通过代码或命令手动触发 GC(一般不推荐,除非调试或特殊场景)。

2.1 代码触发

  • System.gc()
    • 请求 JVM 执行 Full GC(具体是否执行取决于 JVM)。
    • 可通过 -XX:+DisableExplicitGC 禁用。
  • Runtime.getRuntime().gc()
    • System.gc() 等效。

2.2 命令触发

  • JVM 工具命令
    • jcmd <pid> GC.run → 触发 Full GC。
    • jcmd <pid> GC.run_finalization → 触发对象终结。
    • jmap -histo <pid> → 在生成对象直方图前会触发 GC。
  • JConsole / VisualVM
    • 在监控界面点击"执行 GC"按钮。

3. 特殊触发场景

  • 大对象直接进入老年代 (超过 -XX:PretenureSizeThreshold)。
  • 对象晋升失败(Survivor 区不足,老年代不足)。
  • 元空间溢出(类加载过多)。
  • 软引用/弱引用/虚引用的清理
    • 软引用在内存不足时触发 GC 清理。
    • 弱引用在下一次 GC 时直接清理。
    • 虚引用用于对象回收跟踪。

4. 总结表

GC 类型 触发条件 常见场景
Minor GC Eden 区满 创建大量短命对象
Major GC 老年代满 大对象晋升、长期存活对象积累
Full GC 老年代满、元空间满、晋升失败、System.gc() 内存压力大、类加载过多
手动 GC System.gc()Runtime.gc()jcmd 调试、性能测试

总结

  • 自动触发是 JVM 的正常行为,主要由内存使用情况决定。
  • 手动触发一般用于调试或特殊需求,生产环境应谨慎使用。
  • 调优目标是减少 Full GC 频率,让大部分垃圾在 Minor GC 阶段就被清理掉。

GC 日志如何分析?

1. 开启 GC 日志

在不同 JDK 版本中,开启 GC 日志的参数不同:

JDK 8 及之前

bash 复制代码
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/gc.log
  • -XX:+PrintGCDetails → 打印详细 GC 信息
  • -XX:+PrintGCDateStamps → 打印时间戳
  • -Xloggc:/path/gc.log → 输出到文件

JDK 9+(统一日志框架)

bash 复制代码
-Xlog:gc*:file=/path/gc.log:time,uptime,level,tags
  • gc* → 打印所有 GC 相关日志
  • time → 日历时间
  • uptime → JVM 启动到现在的时间
  • level → 日志级别
  • tags → 日志标签

2. 常见 GC 日志格式与字段解析

2.1 Minor GC(JDK 8 示例)

复制代码
2025-11-06T18:49:12.123+0800: 5.678: [GC (Allocation Failure) [PSYoungGen: 10240K->512K(9216K)] 20480K->10752K(19456K), 0.0056789 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

字段解析

  • 2025-11-06T18:49:12.123+0800 → 日历时间
  • 5.678 → JVM 启动后 5.678 秒
  • (Allocation Failure) → 触发原因(Eden 区满)
  • [PSYoungGen: 10240K->512K(9216K)] → 新生代 GC 前占用 10240K,GC 后 512K,总容量 9216K
  • 20480K->10752K(19456K) → 整个堆 GC 前占用 20480K,GC 后 10752K,总容量 19456K
  • 0.0056789 secs → GC 停顿时间
  • [Times: user=0.01 sys=0.00, real=0.01 secs] → 用户态 CPU 时间、内核态 CPU 时间、实际耗时

2.2 Full GC(JDK 8 示例)

复制代码
2025-11-06T18:50:00.456+0800: 53.789: [Full GC (System.gc()) [PSYoungGen: 512K->0K(9216K)] [ParOldGen: 10240K->512K(10240K)] 10752K->512K(19456K), [Metaspace: 3500K->3500K(1056768K)], 0.0456789 secs] [Times: user=0.05 sys=0.00, real=0.05 secs]

字段解析

  • (System.gc()) → 触发原因是手动调用 System.gc()
  • [ParOldGen: 10240K->512K(10240K)] → 老年代 GC 前占用 10240K,GC 后 512K,总容量 10240K
  • [Metaspace: 3500K->3500K(1056768K)] → 元空间使用情况
  • 0.0456789 secs → 停顿时间(Full GC 一般较长)

3. GC 日志分析步骤

3.1 确认 GC 类型和频率

  • Minor GC 频率高 → 新生代太小或对象存活率高
  • Full GC 频率高 → 老年代空间不足、晋升失败、元空间不足

3.2 关注停顿时间

  • Minor GC:一般 < 50ms
  • Full GC:一般 < 1s(低延迟系统要求更低)
  • 如果停顿时间过长 → 考虑更换 GC 收集器或调优堆参数

3.3 观察 GC 前后内存变化

  • GC 后内存下降不明显 → 存活对象多,可能存在内存泄漏
  • GC 后老年代占用持续上升 → 晋升过多或对象生命周期长

3.4 分析触发原因

  • (Allocation Failure) → Eden 区满
  • (Metadata GC Threshold) → 元空间满
  • (System.gc()) → 手动触发
  • (CMS Initial Mark) → CMS 初始标记阶段

4. 常见问题定位

现象 可能原因 调优方向
Minor GC 频繁 新生代太小、对象存活率高 增大新生代、优化对象创建
Full GC 频繁 老年代不足、晋升失败 增大老年代、减少晋升
GC 停顿长 收集器不适合、堆太大 换低延迟收集器(G1/ZGC)
GC 后内存不降 内存泄漏 使用 MAT/VisualVM 分析引用链

5. 辅助工具

  • GCEasy(在线 GC 日志分析)
  • GCViewer(可视化 GC 日志)
  • Eclipse MAT(内存分析)
  • JClarity Censum(商业 GC 分析工具)

总结

  • GC 日志分析的核心是看频率、看停顿、看回收效果、看触发原因
  • 频繁 Full GC 或停顿时间长是性能瓶颈信号。
  • 结合工具可以更高效地定位问题。

如何避免频繁 Full GC?

1. 为什么会频繁 Full GC?

Full GC(整堆回收)会回收新生代 + 老年代 + 元空间 ,停顿时间长,对性能影响大。

常见触发原因:

触发原因 说明
老年代空间不足 晋升对象过多或大对象直接进入老年代
晋升失败(Promotion Failure) Survivor 区不足,导致对象直接晋升到老年代,老年代又不足
元空间(Metaspace)不足 类加载过多或类卸载不及时
显式调用 System.gc() 代码或第三方库主动触发
CMS 并发收集失败 CMS 回收速度跟不上分配速度
G1 Mixed GC 触发过多 老年代 Region 占比过高

2. 避免频繁 Full GC 的优化思路

2.1 内存分配与代际比例优化

  • 增大堆内存-Xmx) → 给老年代更多空间,减少触发 Full GC 的概率。
  • 调整新生代大小-Xmn-XX:NewRatio) → 减少对象过早晋升到老年代。
  • 调整 Survivor 区比例-XX:SurvivorRatio) → 增大 Survivor 区,减少晋升失败。

2.2 减少大对象直接进入老年代

  • 大对象(超过 -XX:PretenureSizeThreshold)会直接分配到老年代。
  • 优化方式
    • 避免一次性创建超大数组或集合。
    • 分批构建大数据结构。
    • 调整 -XX:PretenureSizeThreshold(仅对 Serial/ParNew 有效)。

2.3 控制对象晋升速度

  • 对象在 Survivor 区存活次数超过 -XX:MaxTenuringThreshold 会晋升到老年代。
  • 优化方式
    • 增大 MaxTenuringThreshold,让对象在新生代多存活几次。
    • 减少长生命周期对象的创建频率。

2.4 避免显式 Full GC

  • 检查代码中是否有 System.gc()Runtime.getRuntime().gc()

  • 禁用显式 GC:

    bash 复制代码
    -XX:+DisableExplicitGC

2.5 元空间优化

  • 元空间不足会触发 Full GC。
  • 优化方式
    • 增大元空间:-XX:MetaspaceSize-XX:MaxMetaspaceSize
    • 避免频繁动态生成类(如反射、CGLIB、JSP 编译)。
    • 卸载无用类(配合 -XX:+ClassUnloadingWithConcurrentMark)。

2.6 收集器选择与调优

  • CMS :避免并发收集失败(-XX:CMSInitiatingOccupancyFraction 提前触发)。
  • G1 :合理设置 -XX:InitiatingHeapOccupancyPercent,避免老年代 Region 占比过高。
  • ZGC / Shenandoah:低延迟场景可直接替换,减少 Full GC 停顿。

3. 参数调优示例(以 G1 为例)

bash 复制代码
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=10
-XX:+DisableExplicitGC

说明

  • 固定堆大小,避免频繁扩容/收缩。
  • 提前触发老年代回收(45% 占用率)。
  • 增大 Survivor 区,减少晋升。
  • 禁用显式 GC。

4. 代码层面优化

  • 减少短时间内创建大量对象(尤其是大对象)。
  • 复用对象(对象池、缓冲区)。
  • 及时释放无用引用 (将不再使用的对象引用置为 null)。
  • 避免内存泄漏(静态集合、缓存未清理、监听器未移除)。
  • 使用合适的数据结构(避免过度扩容的集合)。

5. 监控与验证

  • 开启 GC 日志:

    bash 复制代码
    -Xlog:gc*:file=gc.log:time,uptime,level,tags
  • 使用工具分析:

    • GCEasy(在线 GC 日志分析)
    • GCViewer(可视化 GC 日志)
    • VisualVM / JMC(实时监控)

6. 总结表

问题原因 优化手段
老年代不足 增大堆、调整代际比例、减少晋升
晋升失败 增大 Survivor 区、提高 MaxTenuringThreshold
大对象直接进入老年代 优化数据结构、调整 PretenureSizeThreshold
元空间不足 增大元空间、减少动态类加载
显式 GC 禁用 System.gc()
收集器不适配 更换低延迟收集器(G1/ZGC/Shenandoah)

一句话总结

避免频繁 Full GC 的核心是减少老年代压力 ,通过合理的内存分配策略 + 收集器调优 + 代码优化,让大部分垃圾在新生代就被清理掉。


五、JVM 调优与参数

JVM 调优的目标是什么?

1. 性能目标

  • 降低 GC 停顿时间
    • 尤其是对延迟敏感的系统(如金融交易、在线游戏、实时服务)。
    • 让 GC 对业务线程的影响最小化。
  • 提高吞吐量
    • 吞吐量 = 用户代码运行时间 / 总运行时间。
    • 适合批处理、大数据计算等场景。
  • 减少 GC 频率
    • 避免频繁 GC,尤其是 Full GC。
    • 让大部分垃圾在新生代就被清理掉。

2. 稳定性目标

  • 避免内存溢出(OOM)
    • 包括堆内存、元空间、直接内存等。
  • 避免长时间停顿
    • 防止 GC 暂停导致服务不可用。
  • 防止内存泄漏
    • 保证对象在不再使用时能被及时回收。
  • 可预测的性能表现
    • 在不同负载下保持稳定的响应时间。

3. 资源利用率目标

  • 合理使用内存
    • 堆大小、代际比例、元空间大小合理分配。
  • 合理使用 CPU
    • GC 线程数与业务线程数平衡。
  • 避免资源浪费
    • 避免过度分配导致内存闲置,或分配不足导致频繁 GC。

4. 具体调优方向

  • 选择合适的 GC 收集器
    • 吞吐量优先 → Parallel GC
    • 低延迟优先 → CMS / G1 / ZGC / Shenandoah
  • 调整堆内存大小与代际比例
    • -Xms-Xmx-Xmn-XX:NewRatio
  • 优化 GC 参数
    • -XX:MaxGCPauseMillis-XX:InitiatingHeapOccupancyPercent-XX:SurvivorRatio
  • 代码层面优化
    • 减少对象创建、复用对象、及时释放引用。
  • 监控与验证
    • GC 日志分析、JVM 监控工具(JMC、VisualVM、Prometheus+Grafana)。

5. 总结表

维度 目标 示例指标
性能 降低停顿、提高吞吐量、减少 GC 频率 停顿时间 < 200ms,吞吐量 > 99%
稳定性 避免 OOM、长停顿、内存泄漏 无 OOM,响应时间稳定
资源利用率 合理使用内存和 CPU 堆利用率 60~80%,CPU 利用率合理

一句话总结

JVM 调优的最终目标是在有限的硬件资源下,让应用既快又稳,并且可预测,避免因为 GC 或内存问题影响业务。

常用的 JVM 启动参数有哪些?

1. 内存管理相关参数

参数 作用 示例
-Xms 初始堆大小 -Xms512m
-Xmx 最大堆大小 -Xmx4g
-Xmn 新生代大小 -Xmn1g
-XX:NewRatio 老年代与新生代比例 -XX:NewRatio=2(老年代:新生代=2:1)
-XX:SurvivorRatio Eden 与 Survivor 区比例 -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold 对象晋升到老年代的最大年龄 -XX:MaxTenuringThreshold=15
-XX:MetaspaceSize 元空间初始大小 -XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize 元空间最大大小 -XX:MaxMetaspaceSize=512m
-XX:CompressedClassSpaceSize 压缩类空间大小 -XX:CompressedClassSpaceSize=128m
-XX:MaxDirectMemorySize 最大直接内存大小 -XX:MaxDirectMemorySize=256m

2. GC 相关参数

参数 作用 示例
-XX:+UseSerialGC 使用串行 GC 适合单核、小堆
-XX:+UseParallelGC 使用并行 GC(吞吐量优先) 默认 JDK8
-XX:+UseConcMarkSweepGC 使用 CMS GC(低延迟) JDK8 可用
-XX:+UseG1GC 使用 G1 GC(低延迟) JDK9+ 推荐
-XX:+UseZGC 使用 ZGC(超低延迟) JDK11+
-XX:+UseShenandoahGC 使用 Shenandoah GC OpenJDK 特定版本
-XX:ParallelGCThreads GC 并行线程数 -XX:ParallelGCThreads=8
-XX:ConcGCThreads 并发 GC 线程数 -XX:ConcGCThreads=4
-XX:MaxGCPauseMillis 期望最大 GC 停顿时间 -XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent 老年代占用率触发并发 GC -XX:InitiatingHeapOccupancyPercent=45
-XX:+DisableExplicitGC 禁用显式 GC(System.gc() 防止手动触发 Full GC

3. 性能监控与日志参数

参数 作用 示例
-XX:+PrintGC 打印 GC 简要信息
-XX:+PrintGCDetails 打印 GC 详细信息
-XX:+PrintGCDateStamps 打印 GC 时间戳
-Xloggc:<file> 将 GC 日志输出到文件 -Xloggc:/path/gc.log
-Xlog:gc* JDK9+ 打印 GC 日志 -Xlog:gc*:file=gc.log:time,uptime,level,tags
-XX:+PrintFlagsFinal 打印所有 JVM 参数最终值
-XX:+PrintCommandLineFlags 打印命令行参数
-XX:+HeapDumpOnOutOfMemoryError OOM 时生成堆转储文件
-XX:HeapDumpPath 堆转储文件路径 -XX:HeapDumpPath=/path/heap.hprof

4. 诊断与调试参数

参数 作用 示例
-XX:+UnlockDiagnosticVMOptions 解锁诊断参数
-XX:+PrintSafepointStatistics 打印安全点统计信息
-XX:+PrintConcurrentLocks 打印并发锁信息
-XX:+TraceClassLoading 跟踪类加载
-XX:+TraceClassUnloading 跟踪类卸载
-XX:+FlightRecorder 启用 JFR(Java Flight Recorder)
-XX:StartFlightRecording 启动 JFR 录制 -XX:StartFlightRecording=duration=60s,filename=record.jfr

5. 常用组合示例(G1 GC 调优)

bash 复制代码
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=10
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/gc.log
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/heap.hprof

总结

常用 JVM 启动参数主要分为 内存管理、GC 调优、性能监控、诊断调试 四类,调优时应结合业务场景选择合适的组合,而不是盲目堆参数。

如何定位内存泄漏问题?

1. 判断是否存在内存泄漏

内存泄漏(Memory Leak)是指对象不再被使用,但仍然被引用,导致 GC 无法回收

常见判断依据:

  • 堆内存占用持续上升,且 GC 后没有明显下降。
  • Full GC 频率增加,但回收效果差。
  • 最终触发 OOMjava.lang.OutOfMemoryError: Java heap space)。
  • 监控工具(JVisualVM、JMC、Prometheus+Grafana)显示老年代占用率持续增长

2. 收集证据

当怀疑内存泄漏时,先收集现场数据:

  • GC 日志 (分析 GC 频率、回收效果)

    bash 复制代码
    -Xlog:gc*:file=gc.log:time,uptime,level,tags   # JDK9+
    -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log   # JDK8
  • 堆转储文件(Heap Dump)

    • OOM 自动生成:

      bash 复制代码
      -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/heap.hprof
    • 手动生成:

      bash 复制代码
      jmap -dump:live,format=b,file=heap.hprof <pid>
  • 线程 Dump (排查是否有线程持有引用)

    bash 复制代码
    jstack <pid> > thread.log

3. 分析工具

工具 作用 特点
Eclipse MAT 分析 Heap Dump,找出占用内存最多的对象及引用链 免费、功能强大
VisualVM 实时监控内存、生成 Heap Dump JDK 自带
JProfiler 商业工具,内存分析、对象分配跟踪 界面友好
YourKit 商业工具,支持内存泄漏检测 性能好
Arthas 在线诊断,查看对象引用 适合生产环境

4. 排查方法

4.1 使用 MAT 分析 Heap Dump

  1. 打开 .hprof 文件。
  2. 查看 Histogram (对象直方图):
    • 找出占用内存最多的类。
  3. 使用 Dominator Tree
    • 找出 GC Roots 到对象的引用链。
  4. 分析引用链:
    • 判断是否存在不必要的强引用(如静态集合、缓存、监听器)。

4.2 常见内存泄漏场景

场景 原因 解决方案
静态集合持有对象 静态 Map/List 未清理 使用弱引用或定期清理
缓存未过期 缓存策略不合理 使用 WeakHashMap 或带过期策略的缓存
监听器/回调未移除 注册后未注销 在对象销毁时移除监听器
ThreadLocal 泄漏 线程池复用线程,ThreadLocal 未清理 调用 remove()
连接未关闭 数据库/网络连接未释放 使用 try-with-resources
第三方库 Bug 库内部持有引用 升级或替换库

4.3 在线排查(生产环境)

  • Arthas

    bash 复制代码
    # 查看类加载信息
    sc -d com.example.MyClass
    # 查看对象实例数量
    ognl '@java.lang.management.ManagementFactory@getMemoryMXBean()'
  • jmap + MAT

    • 在生产环境 dump 堆文件,下载到本地分析。

5. 预防建议

  • 及时释放无用引用 (将不再使用的对象置为 null)。
  • 使用弱引用/软引用管理缓存。
  • 定期清理集合(尤其是静态集合)。
  • 关闭资源(数据库连接、文件流、Socket)。
  • ThreadLocal 用完后调用 remove()
  • 监控内存趋势,提前发现问题。

6. 内存泄漏排查流程图

步骤 工具 目标
1. 发现问题 监控、GC 日志 判断是否泄漏
2. 收集数据 jmap、HeapDump 获取现场
3. 分析数据 MAT、VisualVM 找出可疑对象
4. 定位代码 引用链分析 找到持有引用的地方
5. 修复验证 修改代码、调优 确认问题解决

一句话总结

定位内存泄漏的核心是先确认是否泄漏 → 再用 Heap Dump 找出可疑对象 → 分析引用链 → 修改代码释放引用

如何定位 CPU 占用过高问题?

1. 发现问题

CPU 占用过高通常有两种情况:

  • 系统层面top / htop / 监控平台显示某个 Java 进程 CPU 占用率异常。
  • 应用层面:响应变慢、吞吐下降、GC 频繁。

初步判断

  • 如果是短时间峰值 → 可能是 GC 或瞬时计算任务。
  • 如果是持续高占用 → 可能是死循环、频繁锁竞争、IO阻塞导致的忙等。

2. 收集数据

2.1 找到高 CPU 的进程

bash 复制代码
top -Hp <pid>   # 查看进程内线程的 CPU 占用
  • pid → Java 进程 ID(用 jpsps -ef | grep java 获取)

2.2 找到高 CPU 的线程

  • top -Hp 输出中,记录占用高的 线程 ID(TID)
  • 将 TID 转换为十六进制:
bash 复制代码
printf "%x\n" <tid>

3. 分析线程堆栈

3.1 使用 jstack

bash 复制代码
jstack <pid> > thread.log
  • thread.log 中搜索刚才的十六进制 TID。
  • 找到对应的线程堆栈,分析它在执行什么代码。

3.2 常见高 CPU 原因

原因 堆栈特征 说明
死循环 同一方法反复出现 逻辑错误或条件永远不满足
频繁 GC GC 相关线程占用高 内存不足或对象创建过多
锁竞争 java.util.concurrent / synchronized 堆栈 线程等待锁,导致忙等
IO 忙等 java.net / java.nio 堆栈 网络/磁盘阻塞但线程未休眠
计算密集 算法方法占用高 大量数据计算或加密解密

4. 找到代码热点

4.1 在线分析

  • Arthas
bash 复制代码
# 查看方法调用耗时
trace com.example.MyService myMethod
# 查看线程状态
thread

4.2 离线分析

  • 使用 JFR(Java Flight Recorder)
bash 复制代码
-XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,filename=record.jfr
  • JMC(Java Mission Control) 打开 .jfr 文件,查看 CPU Hotspot。

5. 解决与预防

5.1 针对不同原因的解决方案

原因 解决方案
死循环 修复循环条件,增加退出机制
频繁 GC 调整堆大小、优化对象创建、减少内存泄漏
锁竞争 优化锁粒度,使用无锁数据结构
IO 忙等 增加超时、使用异步 IO
计算密集 优化算法、使用并行计算

5.2 预防建议

  • 监控 CPU 使用率(Prometheus + Grafana)。
  • 定期分析线程 Dump,发现潜在热点。
  • 代码审查,避免死循环和无休眠轮询。
  • 合理使用线程池,避免线程过多导致上下文切换开销。

6. CPU 高占用排查流程表

步骤 工具 目标
1. 确认进程 top / jps 找到高 CPU 的 Java 进程
2. 确认线程 top -Hp 找到高 CPU 的线程
3. 转换 TID printf "%x" 转为十六进制
4. 分析堆栈 jstack / Arthas 找到代码热点
5. 修复问题 修改代码 / 调优参数 降低 CPU 占用

一句话总结

定位 CPU 占用过高的核心是先锁定线程 → 再看堆栈 → 找到热点代码 → 对症下药,工具组合用得好,定位速度会非常快。

JVM 调优的常用步骤是什么?

1. 明确调优目标

在调优前必须先确定目标,否则容易陷入"盲调":

  • 性能目标:降低 GC 停顿时间、提高吞吐量、减少 GC 频率。
  • 稳定性目标:避免 OOM、减少长时间停顿、保证响应时间稳定。
  • 资源利用率目标:合理使用内存和 CPU,避免浪费或不足。

2. 收集运行数据

调优必须基于数据,而不是凭经验猜测。

常用数据来源

  • GC 日志

    bash 复制代码
    -Xlog:gc*:file=gc.log:time,uptime,level,tags   # JDK9+
    -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log   # JDK8
  • 堆内存使用情况(VisualVM、JMC、Arthas)

  • 线程状态jstack、Arthas)

  • 系统监控(CPU、内存、磁盘、网络)

  • 业务指标(响应时间、吞吐量)


3. 分析问题

根据收集的数据定位瓶颈:

  • GC 频繁 → 新生代/老年代空间不足、对象晋升过快。
  • Full GC 停顿长 → 老年代压力大、元空间不足。
  • OOM → 内存泄漏、堆/元空间/直接内存不足。
  • CPU 高占用 → 死循环、频繁 GC、锁竞争。
  • 响应时间波动大 → GC 停顿、线程阻塞。

4. 参数调优

根据问题选择合适的调优策略:

4.1 内存参数

参数 作用
-Xms / -Xmx 固定堆大小,避免频繁扩容/收缩
-Xmn / -XX:NewRatio 调整新生代与老年代比例
-XX:SurvivorRatio 调整 Eden 与 Survivor 区比例
-XX:MaxTenuringThreshold 控制对象晋升年龄
-XX:MetaspaceSize / -XX:MaxMetaspaceSize 调整元空间大小

4.2 GC 收集器选择

  • 吞吐量优先 → Parallel GC
  • 低延迟优先 → G1 GC / CMS / ZGC / Shenandoah

4.3 GC 调优参数

参数 作用
-XX:MaxGCPauseMillis 期望最大停顿时间
-XX:InitiatingHeapOccupancyPercent 老年代占用率触发并发 GC
-XX:+DisableExplicitGC 禁用显式 GC

5. 验证与持续优化

  • 验证效果:调优后重新收集数据,确认指标是否达标。
  • 回归测试:确保调优不会引入新问题。
  • 持续监控:上线后保持监控,防止问题反复。
  • 迭代优化:根据业务变化和负载调整参数。

6. JVM 调优流程表

阶段 关键动作 工具
明确目标 确定性能/稳定性/资源利用率目标 业务需求文档
收集数据 获取 GC 日志、堆内存、线程、系统指标 GC 日志、VisualVM、JMC、Arthas
分析问题 找出瓶颈原因 GCViewer、GCEasy、MAT
参数调优 调整内存、GC 收集器、GC 参数 JVM 启动参数
验证优化 对比调优前后指标 压测工具、监控平台
持续优化 监控并迭代调整 Prometheus+Grafana

一句话总结

JVM 调优的核心是先明确目标 → 再用数据定位问题 → 针对性调整参数 → 验证效果 → 持续监控优化,避免盲目改参数。


六、JVM 常用工具

jps 的作用是什么?

jstat 的作用是什么?

jmap 的作用是什么?

jhat 的作用是什么?

jstack 的作用是什么?

jconsole 的作用是什么?

VisualVM 的作用是什么?

MAT(Memory Analyzer Tool)的作用是什么?

Arthas 的作用是什么?

复制代码
## **如何使用这些工具进行线上问题排查?**

七、JVM 高级与面试加分题

类加载器的隔离与热替换原理?

JVM 如何实现方法调用?

JVM 中的即时编译器(JIT)是什么?

解释执行与编译执行的区别?

逃逸分析的原理与作用?

栈上分配与标量替换是什么?

JVM 中的锁优化有哪些?

偏向锁、轻量级锁、重量级锁的区别?

JVM 中的内存屏障是什么?

复制代码
## **JVM 中的 Safepoint 是什么?**
相关推荐
jz_ddk2 小时前
[数学基础] 瑞利分布:数学原理、物理意义及Python实验
开发语言·python·数学·概率论·信号分析
ZHE|张恒2 小时前
深入理解 Java 双亲委派机制:JVM 类加载体系全解析
java·开发语言·jvm
q_19132846952 小时前
基于SpringBoot+Vue2的美食菜谱美食分享平台
java·spring boot·后端·计算机·毕业设计·美食
范德萨_2 小时前
JavaScript 实用技巧(总结)
开发语言·前端·javascript
milanyangbo2 小时前
从同步耦合到异步解耦:消息中间件如何重塑系统间的通信范式?
java·数据库·后端·缓存·中间件·架构
秃了也弱了。2 小时前
elasticSearch之java客户端详细使用:文档搜索API
java·elasticsearch
1024小神2 小时前
Kotlin实现全屏显示效果,挖空和刘海屏适配
android·开发语言·kotlin
kaikaile19952 小时前
34节点配电网牛顿-拉夫逊潮流计算 + 分布式电源(DG)多场景分析的 MATLAB
开发语言·分布式·matlab