一、JVM 核心概念与作用
1. 什么是 JVM
JVM(Java Virtual Machine)即 Java 虚拟机,是运行 Java 字节码的虚拟计算机。它屏蔽了底层操作系统的差异,实现“一次编译,到处运行”——Java 源代码编译为 .class 字节码后,可在任何安装了对应 JVM 的平台上执行,无需修改代码。
2. JVM 的核心作用
- 字节码解释/编译执行:将字节码转换为本地机器指令并运行。
- 内存管理:自动分配、回收对象内存(垃圾回收),避免内存泄漏。
- 跨平台支持:隔离操作系统差异,统一字节码运行环境。
- 提供核心功能:如异常处理、线程管理、类加载机制等。
3. JVM 与 JDK、JRE 的关系
- JDK(Java Development Kit):开发工具包,包含 JRE + 编译/调试工具(javac、javap 等)。
- JRE(Java Runtime Environment):运行时环境,包含 JVM + 核心类库(java.lang、java.util 等)。
- 依赖关系:JDK ⊇ JRE ⊇ JVM,运行 Java 程序需 JRE,开发 Java 程序需 JDK。
二、JVM 架构组成
JVM 架构核心分为五大模块,各模块协同工作完成字节码执行与资源管理:
1. 类加载子系统(Class Loader Subsystem)
核心功能
负责将 .class 字节码文件加载到 JVM 内存中,生成对应的 Class 对象。
加载流程(生命周期)
- 加载(Loading):通过类全限定名(如
java.lang.String)查找字节码文件,读取到内存。 - 验证(Verification):校验字节码合法性(如格式是否符合规范、是否有安全隐患)。
- 准备(Preparation):为类静态变量分配内存并设置默认初始值(如
int默认为 0,引用类型为null)。 - 解析(Resolution):将符号引用(如类名、方法名)转换为直接引用(内存地址)。
- 初始化(Initialization):执行静态代码块、为静态变量赋值(执行显式初始化逻辑)。
类加载器分类(双亲委派模型)
- 启动类加载器(Bootstrap ClassLoader):底层由 C++ 实现,加载 JRE 核心类库(如
rt.jar中的java.lang包)。 - 扩展类加载器(Extension ClassLoader):加载 JRE 扩展目录(
jre/lib/ext)中的类库。 - 应用程序类加载器(Application ClassLoader):加载当前应用 classpath 下的类(自定义类、第三方依赖)。
- 自定义类加载器:继承
ClassLoader重写findClass()方法,实现特殊加载需求(如加密字节码解密、热部署)。
双亲委派机制
- 加载类时,先委托父加载器尝试加载,父加载器无法加载(找不到字节码)时,子加载器才自行加载。
- 作用:避免类重复加载,保证核心类库安全性(防止自定义类篡改核心类,如自定义
java.lang.String)。
2. 运行时数据区(Runtime Data Area)
JVM 内存划分的核心区域,所有字节码执行过程中的数据都存储于此。JDK 8 后永久代被元空间替代,是重要变更点。
(1)程序计数器(Program Counter Register)
- 作用:记录当前线程执行的字节码指令地址(行号)。
- 特点:线程私有(每个线程独立拥有),内存占用极小,无垃圾回收(GC)。
- 特殊情况:线程执行 native 方法时,计数器值为
undefined(native 方法由本地语言实现,无字节码)。
(2)Java 虚拟机栈(Java Virtual Machine Stack)
- 作用:存储线程执行方法时的栈帧(Stack Frame),包含局部变量表、操作数栈、方法出口等。
- 特点:线程私有,栈帧随方法调用创建、方法返回销毁(先进后出)。
- 核心组件:
- 局部变量表:存储方法参数、局部变量(基本数据类型、引用类型地址),容量编译期确定。
- 操作数栈:方法执行时的临时数据栈(如运算、赋值操作的中间结果)。
- 方法出口:记录方法返回地址(如回到调用方的下一条指令)。
- 异常:栈深度超过 JVM 限制时抛出
StackOverflowError(如递归调用无终止);栈扩容失败时抛出OutOfMemoryError(OOM)。
(3)本地方法栈(Native Method Stack)
- 作用:与虚拟机栈类似,专门为 native 方法(本地方法)提供内存支持。
- 特点:线程私有,具体实现依赖底层操作系统,可能抛出
StackOverflowError和OOM。
(4)Java 堆(Java Heap)
- 作用:存储所有对象实例和数组,是 JVM 内存中最大的区域,也是垃圾回收的核心区域。
- 特点:线程共享,所有线程可访问堆中对象;内存可动态扩展(通过
-Xms、-Xmx参数配置)。 - 细分区域(JDK 8 后):
- 年轻代(Young Generation):占堆内存 1/3 左右,存储新创建的对象,GC 频率高(Minor GC)。
- Eden 区:对象初始分配的区域,占年轻代 80%。
- Survivor 区:分为 From Survivor 和 To Survivor,各占 10%,用于存储 Minor GC 后存活的对象。
- 老年代(Old Generation):占堆内存 2/3 左右,存储存活时间长的对象(多次 Minor GC 后仍存活),GC 频率低(Major GC/Full GC)。
- 年轻代(Young Generation):占堆内存 1/3 左右,存储新创建的对象,GC 频率高(Minor GC)。
- 异常:堆内存不足时抛出
OutOfMemoryError: Java heap space。
(5)方法区(Method Area)
- 作用:存储类信息(类名、父类、接口、字段、方法)、静态变量、常量、编译器优化后的字节码等。
- 特点:线程共享,逻辑上属于堆的一部分,但物理上独立(又称“非堆”)。
- JDK 8 变更:
- JDK 7 及之前:称为“永久代”(PermGen),占用 JVM 堆内存,有固定大小限制。
- JDK 8 及之后:改为“元空间”(Metaspace),占用本地内存(Native Memory),默认无大小限制(可通过参数限制),解决永久代 OOM 问题。
- 异常:方法区/元空间不足时抛出
OutOfMemoryError: Metaspace(JDK 8+)或PermGen space(JDK 7-)。
(6)运行时常量池(Runtime Constant Pool)
- 作用:方法区的一部分,存储类的常量池信息(编译期生成的字面量、符号引用),以及运行时动态生成的常量(如
String.intern()方法生成的常量)。 - 特点:常量池中的数据会被解析为直接引用(如类、方法的内存地址)供程序调用。
3. 执行引擎(Execution Engine)
负责执行运行时数据区中的字节码指令,核心组件包括:
(1)解释器(Interpreter)
- 工作方式:逐行解释字节码指令,转换为本地机器指令执行。
- 优点:启动快,无需预热。
- 缺点:执行效率低(重复指令需重复解释)。
(2)即时编译器(Just-In-Time Compiler,JIT)
- 工作方式:识别频繁执行的热点代码(如循环、高频调用的方法),将其编译为本地机器指令并缓存,后续直接执行缓存的机器指令。
- 优点:执行效率高(编译后的机器指令比解释执行快数倍)。
- 缺点:启动时需要编译,有预热开销。
(3)混合模式(默认)
JVM 默认采用“解释器 + JIT”混合模式:
- 程序启动时,解释器快速执行字节码,保证启动速度。
- 运行过程中,JIT 编译器后台编译热点代码,逐步提升执行效率。
(4)垃圾回收器(Garbage Collector,GC)
- 作用:自动回收堆和方法区中不再被引用的对象内存,避免内存泄漏。
- 核心原理:通过可达性分析(以 GC Roots 为起点,遍历对象引用链,不可达对象标记为垃圾)识别垃圾对象,再通过回收算法释放内存。
- GC Roots 包括:虚拟机栈局部变量表中的引用、本地方法栈中的引用、方法区静态变量和常量引用、活跃线程的引用等。
4. 本地方法接口(Native Method Interface,JNI)
- 作用:作为 Java 程序与本地语言(C、C++)编写的代码之间的桥梁。
- 工作流程:Java 方法通过
native关键字声明,调用时通过 JNI 找到对应的本地库,执行本地方法,再将结果返回给 Java 程序。 - 应用场景:需要调用操作系统底层功能(如硬件操作、系统调用)或复用现有 C/C++ 代码时使用。
5. 本地方法库(Native Method Library)
- 作用:存储 JNI 调用的本地方法实现(如 C/C++ 编译后的
.dll或.so文件)。 - 示例:JVM 底层的线程管理、内存分配等功能,部分通过本地方法库实现。
三、垃圾回收(GC)核心机制
1. GC 的核心目标
- 识别并回收“死亡对象”(不再被引用的对象)的内存。
- 保证内存分配效率,避免内存碎片化。
- 最小化 GC 对程序运行的影响(低延迟、高吞吐量)。
2. 垃圾对象判定算法
(1)引用计数法
- 原理:为每个对象维护一个引用计数器,被引用时计数器 +1,引用失效时 -1,计数器为 0 则标记为垃圾。
- 优点:实现简单,判定效率高。
- 缺点:无法解决循环引用问题(如 A 引用 B,B 引用 A,两者计数器均不为 0,但均无其他引用),JVM 未采用。
(2)可达性分析算法(JVM 采用)
- 原理:以 GC Roots 为起点,构建对象引用链。若对象无法通过任何引用链到达 GC Roots,则为不可达对象(标记为垃圾)。
- 优点:能解决循环引用问题,是 JVM 垃圾判定的标准算法。
3. 引用类型(影响对象回收时机)
Java 中引用分为 4 种,强度从高到低:
- 强引用(Strong Reference):默认引用类型(如
Object obj = new Object()),只要强引用存在,对象不会被回收。 - 软引用(Soft Reference):通过
SoftReference实现,内存不足时才会回收,适合缓存场景。 - 弱引用(Weak Reference):通过
WeakReference实现,下次 GC 时必然回收,适合临时数据存储。 - 虚引用(Phantom Reference):通过
PhantomReference实现,仅用于监听对象回收事件,无法通过虚引用获取对象。
4. GC 回收算法
(1)标记-清除算法(Mark-Sweep)
- 流程:1. 标记所有可达对象;2. 清除所有未标记的垃圾对象。
- 优点:实现简单,无需移动对象。
- 缺点:产生内存碎片化,后续分配大对象时可能因无连续内存而触发 Full GC。
(2)复制算法(Copying)
- 流程:将内存分为大小相等的两块(From 和 To),对象仅在 From 区分配。GC 时,标记 From 区可达对象,复制到 To 区,然后清空 From 区,交换 From 和 To 角色。
- 优点:无内存碎片化,回收效率高。
- 缺点:内存利用率低(仅 50%),不适合存储大对象。
- 应用场景:年轻代 Eden 区和 Survivor 区(因年轻代对象存活率低,复制成本低)。
(3)标记-整理算法(Mark-Compact)
- 流程:1. 标记所有可达对象;2. 将可达对象向内存一端移动,紧凑排列;3. 清除移动后另一端的垃圾对象。
- 优点:无内存碎片化,内存利用率高。
- 缺点:需要移动对象,回收效率较低。
- 应用场景:老年代(因老年代对象存活率高,移动成本可接受)。
(4)分代收集算法(Generational Collection)
- 原理:根据对象存活时间将堆分为年轻代和老年代,针对不同代的特点采用不同回收算法。
- 年轻代:对象存活率低,采用复制算法(高效、无碎片)。
- 老年代:对象存活率高,采用标记-清除或标记-整理算法(高内存利用率)。
- 是目前所有 JVM 垃圾回收器的核心设计思想。
5. 常见垃圾回收器(JDK 8+)
JVM 提供多种 GC 实现,需根据应用场景(吞吐量优先/延迟优先)选择:
(1)Serial GC(串行 GC)
- 特点:单线程执行 GC,GC 时暂停所有用户线程(STW,Stop The World)。
- 适用场景:单 CPU 环境、小型应用(如桌面程序),启动快、内存占用小。
- 参数:
-XX:+UseSerialGC(年轻代 Serial + 老年代 Serial Old)。
(2)Parallel GC(并行 GC)
- 特点:多线程执行 GC,提高回收效率,仍有 STW,但暂停时间比 Serial GC 短。
- 核心目标:高吞吐量(用户线程运行时间 / 总时间)。
- 适用场景:多 CPU 环境、后台计算类应用(如大数据处理)。
- 参数:
-XX:+UseParallelGC(年轻代 Parallel Scavenge + 老年代 Serial Old)、-XX:+UseParallelOldGC(年轻代 + 老年代均为并行)。
(3)CMS GC(Concurrent Mark Sweep)
- 特点:并发 GC,大部分回收过程与用户线程并行执行,STW 时间极短(低延迟)。
- 核心目标:低延迟(减少 GC 对用户线程的影响)。
- 流程:初始标记(STW)→ 并发标记 → 重新标记(STW)→ 并发清除。
- 缺点:内存碎片化严重、占用 CPU 资源高、不支持大堆内存。
- 适用场景:对延迟敏感的应用(如 Web 服务)。
- 参数:
-XX:+UseConcMarkSweepGC(年轻代 ParNew + 老年代 CMS)。
(4)G1 GC(Garbage-First)
- 特点:JDK 9 后默认 GC,兼顾吞吐量和延迟,支持大堆内存(如数十 GB)。
- 核心设计:将堆划分为多个大小相等的 Region(区域),跟踪每个 Region 的垃圾量,优先回收垃圾最多的 Region(Garbage-First)。
- 流程:初始标记(STW)→ 并发标记 → 最终标记(STW)→ 筛选回收(STW,可并行)。
- 优点:无内存碎片化(回收时整理 Region)、支持动态调整堆大小、延迟可控。
- 适用场景:中大型应用、大堆内存环境(如服务器应用)。
- 参数:
-XX:+UseG1GC,可通过-XX:MaxGCPauseMillis设置最大 GC 暂停时间(默认 200ms)。
(5)ZGC(Z Garbage Collector)
- 特点:JDK 11 引入的低延迟 GC,支持 TB 级堆内存,STW 时间控制在毫秒级以内。
- 核心优势:并发标记、并发回收、无内存碎片化、支持动态堆扩展。
- 适用场景:超大型应用、对延迟要求极高的场景(如金融交易系统)。
- 参数:
-XX:+UseZGC(需 JDK 11+,Linux/x64 环境支持)。
6. GC 相关概念
- Minor GC:仅回收年轻代的 GC,触发条件:Eden 区满时。
- Major GC:仅回收老年代的 GC,触发条件:老年代空间不足、永久代/元空间不足。
- Full GC:回收年轻代 + 老年代 + 方法区的 GC,触发条件:Major GC 后老年代仍不足、调用
System.gc()(建议不主动调用)、堆内存分配失败。 - STW(Stop The World):GC 执行时暂停所有用户线程,是影响应用性能的关键因素,优质 GC (如 G1、ZGC)会尽量缩短 STW 时间。
四、JVM 内存模型(JMM)
1. JMM 的核心目标
定义线程与内存之间的交互规则,解决多线程环境下的可见性、原子性、有序性问题,确保并发程序的正确性。
2. 多线程并发三大问题
- 可见性:一个线程修改共享变量后,其他线程能否立即看到修改后的结果(因 CPU 缓存导致缓存不一致)。
- 原子性:一个操作或多个操作要么全部执行且执行过程中不被中断,要么全部不执行(如
i++实际是read -> add -> write三步,多线程下可能被中断)。 - 有序性:程序执行顺序与代码编写顺序一致(编译器优化、CPU 指令重排序可能导致顺序打乱,单线程无影响,多线程下可能出错)。
3. JMM 内存结构定义
JMM 抽象了线程与主内存的交互,不直接对应物理硬件,核心划分:
- 主内存(Main Memory):存储所有线程共享的变量(实例变量、静态变量、数组元素),是物理内存的抽象。
- 工作内存(Working Memory):每个线程独立拥有,存储线程私有的变量(局部变量、方法参数)或共享变量的副本,是 CPU 缓存和寄存器的抽象。
4. JMM 交互规则(内存操作)
JMM 定义 8 种原子操作,确保线程与主内存的交互有序性:
lock(锁定):将主内存中的共享变量标记为线程独占。unlock(解锁):释放锁定的共享变量,允许其他线程锁定。read(读取):将主内存的共享变量值读取到线程工作内存。load(载入):将工作内存中读取的变量值存入本地变量表。use(使用):将工作内存中的变量值传递给执行引擎(如运算)。assign(赋值):将执行引擎的结果赋值给工作内存中的变量。store(存储):将工作内存中的变量值写入主内存。write(写入):将store操作的变量值更新到主内存的共享变量中。
5. JMM 对三大问题的解决方案
(1)可见性保障
volatile关键字:修饰共享变量时,禁止 CPU 缓存该变量,线程修改后立即写入主内存,读取时直接从主内存读取,确保其他线程可见。synchronized和Lock:解锁时会将工作内存中变量同步到主内存,加锁时会清空工作内存缓存,从主内存重新读取,间接保证可见性。
(2)原子性保障
- 基本数据类型(
boolean、byte、char、short、int、long、float、double)的赋值和读取操作天然原子性(long和double有特殊情况,32 位 JVM 可能拆分,但 JDK 1.5+ 已优化为原子操作)。 - 复合操作(如
i++、a += b):需通过synchronized、Lock或java.util.concurrent.atomic包下的原子类(AtomicInteger、AtomicLong)保障原子性。
(3)有序性保障
volatile关键字:禁止指令重排序(通过内存屏障实现),确保代码执行顺序与编写顺序一致。synchronized和Lock:同一时刻只有一个线程执行同步块,间接保证有序性。- happens-before 规则:JMM 定义的天然有序性规则,无需额外同步即可保证有序性,核心规则包括:
- 程序次序规则:单线程内,代码执行顺序按编写顺序。
- 管程锁定规则:解锁操作 happens-before 后续对同一锁的加锁操作。
- volatile 变量规则:对 volatile 变量的写操作 happens-before 后续的读操作。
- 线程启动规则:
Thread.start()操作 happens-before 线程内的任何操作。 - 线程终止规则:线程内的任何操作 happens-before 线程的终止检测(如
Thread.join())。
五、JVM 核心参数配置
JVM 参数分为三类:标准参数(- 开头)、非标准参数(-X 开头)、高级参数(-XX 开头,最常用),以下是生产环境高频配置:
1. 堆内存配置(最核心)
-Xms:初始堆内存大小(如-Xms2G,默认是物理内存的 1/64)。-Xmx:最大堆内存大小(如-Xmx4G,默认是物理内存的 1/4)。- 建议:
-Xms和-Xmx设为相同值,避免堆内存动态扩容导致性能损耗(如-Xms4G -Xmx4G)。 -Xmn:年轻代内存大小(如-Xmn2G,默认是堆内存的 1/3),剩余部分为老年代。-XX:SurvivorRatio:Eden 区与单个 Survivor 区的比例(如-XX:SurvivorRatio=8,默认 8,即 Eden:From:To = 8:1:1)。
2. 元空间(方法区)配置(JDK 8+)
-XX:MetaspaceSize:元空间初始大小(如-XX:MetaspaceSize=128M,默认约 21M)。-XX:MaxMetaspaceSize:元空间最大大小(如-XX:MaxMetaspaceSize=256M,默认无限制,建议设置上限避免占用过多本地内存)。
3. 垃圾回收器配置
- 串行 GC:
-XX:+UseSerialGC(年轻代 Serial + 老年代 Serial Old)。 - 并行 GC:
-XX:+UseParallelGC(年轻代 Parallel Scavenge)、-XX:+UseParallelOldGC(老年代 Parallel Old,建议同时启用)。 - CMS GC:
-XX:+UseConcMarkSweepGC+-XX:+UseParNewGC(年轻代 ParNew 配合老年代 CMS)。 - G1 GC:
-XX:+UseG1GC+-XX:MaxGCPauseMillis=100(设置最大 GC 暂停时间)。 - ZGC:
-XX:+UseZGC(JDK 11+) +-XX:ZHeapSize=8G(设置堆大小)。
4. GC 日志配置(问题排查必备)
-XX:+PrintGCDetails:打印详细 GC 日志(包括各代内存变化、GC 时间)。-XX:+PrintGCTimeStamps:打印 GC 发生的时间戳(相对于 JVM 启动时间)。-XX:+PrintHeapAtGC:GC 前后打印堆内存快照。-Xloggc:./gc.log:将 GC 日志输出到指定文件(如./gc.log)。- JDK 9+ 推荐:
-Xlog:gc*:file=./gc.log:time,level,tags:filecount=5,filesize=100M(滚动日志,最多 5 个文件,每个 100M)。
5. 栈内存配置
-Xss:每个线程的虚拟机栈大小(如-Xss1M,默认 1M 左右)。- 注意:栈大小过大会导致可创建的线程数减少(总线程数 = 物理内存 / 栈大小),过小可能抛出
StackOverflowError(如递归深度过大)。
6. 其他常用配置
-XX:+HeapDumpOnOutOfMemoryError:OOM 时自动生成堆转储文件(heapdump.hprof),用于排查内存泄漏。-XX:HeapDumpPath=./heapdump.hprof:指定 OOM 堆转储文件路径。-XX:MaxTenuringThreshold:对象晋升老年代的年龄阈值(如-XX:MaxTenuringThreshold=15,默认 15,对象在 Survivor 区存活 15 次 Minor GC 后进入老年代)。-XX:+DisableExplicitGC:禁止程序调用System.gc()(避免手动触发 Full GC)。
回复