在前两期内容中,我们覆盖了 Java 基础、高级特性、并发编程及实战案例。本期将深入 JVM(Java 虚拟机)的核心机制,这是 Java 跨平台特性的基石,也是解决性能问题、内存泄漏的关键。掌握 JVM 知识,能让你从 "知其然" 迈向 "知其所以然"。
一、JVM 内存模型:数据存储的 "五脏六腑"
1. 程序计数器(Program Counter Register)
- 作用:记录当前线程执行的字节码指令地址(行号)。
- 特点:
- 线程私有(每个线程有独立的程序计数器);
- 唯一不会发生 OutOfMemoryError 的区域;
- 如果执行的是 Native 方法,计数器值为 undefined。
2. Java 虚拟机栈(VM Stack)
- 作用:存储方法调用的栈帧(局部变量表、操作数栈、动态链接、方法出口等)。
- 特点:
- 线程私有,生命周期与线程一致;
- 局部变量表存储基本数据类型(boolean、byte 等)和对象引用;
- 可能抛出的异常:
- StackOverflowError:线程请求的栈深度超过虚拟机允许的深度(如递归调用无终止条件);
- OutOfMemoryError:栈扩展时无法申请到足够内存。
public class StackOverflowDemo {
private static int count = 0;
public static void recursiveCall() {
count++;
recursiveCall(); // 无限递归
}
public static void main(String[] args) {
try {
recursiveCall();
} catch (StackOverflowError e) {
System.out.println("递归次数:" + count);
e.printStackTrace();
}
}
}
3. 本地方法栈(Native Method Stack)
- 作用:与虚拟机栈类似,但为 Native 方法(非 Java 实现的方法)提供服务。
- 特点:
- 线程私有;
- 也可能抛出 StackOverflowError 和 OutOfMemoryError。
4. Java 堆(Heap)
- 作用:存储对象实例和数组,是 JVM 中最大的一块内存区域。
- 特点:
- 所有线程共享;
- 垃圾回收的主要区域("GC 堆");
- 可能抛出:OutOfMemoryError(对象实例过多,无法分配内存)。
- 新生代(Young Generation):新创建的对象优先分配在这里,分为:
- Eden 区(伊甸园):新对象分配的主要区域;
- Survivor 区(幸存者区):From Survivor 和 To Survivor,用于存放经历一次 GC 后仍存活的对象。
- 老年代(Old Generation):存放存活时间较长的对象(通常是经历多次 GC 后仍存活的对象)。
- 元空间(Metaspace,JDK 8 及以上):存储类信息、常量、静态变量等(替代 JDK 7 及以前的永久代)。
5. 方法区(Method Area)
- 作用:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
- 特点:
- 所有线程共享;
- JDK 8 及以上由元空间(Metaspace)实现,元空间使用本地内存,不再受 JVM 内存限制;
- 可能抛出:OutOfMemoryError(如类加载过多导致元空间溢出)。
二、垃圾回收(GC):自动内存管理的核心
1. 如何判断对象 "已死"?
(1)引用计数法
- 原理:给对象添加一个引用计数器,被引用时 + 1,引用失效时 - 1,计数器为 0 的对象可回收。
- 缺陷:无法解决循环引用问题(如 A 引用 B,B 引用 A,两者计数器都不为 0,但实际已无用)。
(2)可达性分析算法(JVM 采用)
- 原理:以 "GC Roots" 为起点,向下搜索引用链,不可达的对象即为可回收对象。
- GC Roots 包括:
- 虚拟机栈中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 Native 方法引用的对象。
2. 垃圾回收算法
(1)标记 - 清除算法(Mark-Sweep)
- 步骤:
- 优点:简单高效;
- 缺点:产生大量不连续的内存碎片,可能导致大对象无法分配内存。
(2)复制算法(Copying)
- 步骤:
- 优点:无内存碎片,实现简单;
- 缺点:内存利用率低(仅 50%);适合对象存活率低的场景(如新生代)。
(3)标记 - 整理算法(Mark-Compact)
- 步骤:
- 优点:无内存碎片,内存利用率高;
- 缺点:效率较低(需要移动对象);适合对象存活率高的场景(如老年代)。
3. 常见垃圾收集器
收集器 | 特点 | 适用场景 |
Serial GC | 单线程收集,收集时暂停所有用户线程(Stop The World) | 客户端应用、内存较小的场景 |
Parallel GC | 多线程收集,注重吞吐量(吞吐量 = 用户时间 /(用户时间 + GC 时间)) | 后台计算、批处理任务 |
CMS(Concurrent Mark Sweep) | 并发收集,低延迟(尽可能缩短 STW 时间) | 响应时间优先的应用(如 Web 服务) |
G1(Garbage-First) | 区域化分代式,兼顾吞吐量和延迟 | 大内存应用(堆内存 > 4GB) |
ZGC/Shenandoah | 超低延迟(毫秒级以下),支持 TB 级内存 | 对延迟要求极高的场景 |
- JDK 8 默认是 Parallel GC;
- JDK 9 及以上默认是 G1;
- 对于 Web 应用,推荐 G1 或 ZGC(JDK 11+);
- 通过
-XX:+UseG1GC
等参数指定收集器。
三、类加载机制:Class 文件的 "生命周期"
1. 类加载的全过程
(1)加载(Loading)
- 通过类的全限定名(如
com.example.User
)获取二进制字节流(Class 文件); - 将字节流的静态存储结构转换为方法区的运行时数据结构;
- 在内存中生成一个代表该类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
(2)验证(Verification)
- 确保 Class 文件的字节流包含的信息符合 JVM 规范,保证安全(如文件格式验证、元数据验证、字节码验证等)。
(3)准备(Preparation)
- 为类变量(static 修饰的变量)分配内存并设置默认值(如
static int a = 10
,此处设置为 0)。
(4)解析(Resolution)
- 将常量池中的符号引用转换为直接引用(符号引用是字符串形式的类名 / 方法名,直接引用是内存地址)。
(5)初始化(Initialization)
- 执行类构造器
<clinit>()
方法(由类变量的赋值语句和静态代码块合并产生); - 初始化顺序:父类先于子类,静态代码块按出现顺序执行。
public class ClassInitDemo {
static {
System.out.println("父类静态代码块");
}
public static int parentVar = 100;
public ClassInitDemo() {
System.out.println("父类构造器");
}
public static void main(String[] args) {
new SubClass();
}
}
class SubClass extends ClassInitDemo {
static {
System.out.println("子类静态代码块");
}
public static int subVar = 200;
public SubClass() {
System.out.println("子类构造器");
}
}
// 输出顺序:
// 父类静态代码块
// 子类静态代码块
// 父类构造器
// 子类构造器
2. 类加载器(ClassLoader)
- 启动类加载器(Bootstrap ClassLoader):
- 负责加载
JAVA_HOME/lib
目录下的核心类库(如rt.jar
); - 由 C++ 实现,不是 Java 类(
getClassLoader()
返回 null)。
- 扩展类加载器(Extension ClassLoader):
- 负责加载
JAVA_HOME/lib/ext
目录下的扩展类库。
- 应用程序类加载器(Application ClassLoader):
- 负责加载用户类路径(classpath)上的类;
- 是默认的类加载器。
- 类加载器加载类时,先委托给父类加载器,只有父类加载器无法加载时,才自己加载;
- 目的:保证类的唯一性(如
java.lang.Object
只会被启动类加载器加载,避免篡改核心类)。
四、JVM 性能调优实战
1. 常用 JVM 参数
(1)内存设置
-Xms:初始堆大小(如-Xms512m)
-Xmx:最大堆大小(如-Xmx1024m,建议与-Xms设为相同,避免频繁扩容)
-Xmn:新生代大小(如-Xmn256m,一般为堆大小的1/3~1/4)
-XX:SurvivorRatio:Eden区与Survivor区的比例(如-XX:SurvivorRatio=8,即Eden:From:To=8:1:1)
-XX:MetaspaceSize:元空间初始大小(如-XX:MetaspaceSize=128m)
-XX:MaxMetaspaceSize:元空间最大大小(如-XX:MaxMetaspaceSize=256m)
(2)GC 设置
-XX:+UseG1GC:使用G1收集器
-XX:MaxGCPauseMillis:G1目标最大停顿时间(如-XX:MaxGCPauseMillis=200)
-XX:+UseParallelGC:使用Parallel收集器
-XX:+UseConcMarkSweepGC:使用CMS收集器(JDK 9+已废弃)
(3)日志设置
-XX:+PrintGC:打印GC基本信息
-XX:+PrintGCDetails:打印GC详细信息
-XX:+PrintGCTimeStamps:打印GC时间戳
-Xloggc:gc.log:将GC日志写入文件
2. 性能问题诊断工具
(1)JDK 自带工具
- jps:查看 Java 进程 ID;
- jstat:监控 JVM 统计信息(如 GC 情况、类加载数);
jstat -gc 12345 1000 10 # 监控进程12345的GC情况,每1000ms一次,共10次
- jmap:生成堆转储快照(分析内存泄漏);
jmap -dump:format=b,file=heapdump.hprof 12345 # 生成进程12345的堆快照
- jstack:生成线程快照(分析死锁、线程阻塞);
jstack 12345 > threaddump.txt # 生成线程快照并保存到文件
- jconsole/jvisualvm:图形化工具,监控内存、线程、GC 等。
(2)第三方工具
- MAT(Memory Analyzer Tool):分析堆快照,定位内存泄漏;
- GCEasy:在线 GC 日志分析工具(https://gceasy.io/)。
3. 常见性能问题及解决方案
(1)内存泄漏
- 现象:堆内存持续增长,频繁 Full GC,最终 OOM;
- 原因:对象不再使用但仍被引用(如静态集合缓存未清理、监听器未移除等);
- 解决:
(2)GC 频繁
- 现象:Young GC 次数多,或 Full GC 频繁;
- 原因:
- 新生代设置过小,导致对象频繁进入老年代;
- 大对象直接进入老年代,触发 Full GC;
- 解决:
- 调大新生代大小(-Xmn);
- 调整大对象阈值(-XX:PretenureSizeThreshold);
- 检查是否有内存泄漏。
(3)GC 停顿时间过长
- 现象:单次 GC 停顿超过 1 秒,影响用户体验;
- 原因:
- 老年代过大,Full GC 耗时久;
- 使用了不适合的收集器(如 Serial GC);
- 解决:
- 切换到低延迟收集器(如 G1、ZGC);
- 调整 G1 的 MaxGCPauseMillis 参数;
- 避免大对象分配。