Java 虚拟机(JVM)深入理解
JVM 是 Java 技术的核心,深入理解 JVM 对于写出高效 Java 程序至关重要。
JVM 内存结构
运行时数据区
┌─────────────────────────────────────────────────────┐
│ JVM 进程内存 │
├─────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 方法区 │ │ 堆内存 │ │
│ │ (Method Area)│ │ (Heap) │ │
│ │ - 类信息 │ │ - 对象实例 │ │
│ │ - 静态变量 │ │ - 数组 │ │
│ │ - 常量池 │ │ │ │
│ └─────────────┘ └─────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ ┌──────┴──────────────────┴──────┐ │
│ │ 直接内存 (Direct) │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 程序计数器 │ │ 虚拟机栈 │ │ 本地方法栈 │ │
│ │ (PC Register)│ │ (VM Stack) │ │ (Native M.) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────┘
堆内存详解
Java 堆是 GC 的主要管理区域:
javapublic class HeapDemo { // 对象在堆中分配 private byte[] largeData = new byte[1024 * 1024]; // 1MB public static void main(String[] args) { // 局部变量引用在栈上,对象在堆上 HeapDemo obj = new HeapDemo(); // 引用在栈上,对象在堆上 // 引用类型的数组 String[] arr = new String[10]; // 数组对象在堆上 } }
虚拟机栈
每个线程有自己的虚拟机栈:
javapublic class StackDemo { public static int factorial(int n) { if (n <= 1) return 1; return n * factorial(n - 1); // 递归调用 } public static void main(String[] args) { // 方法调用创建栈帧 factorial(5); // 5 个栈帧 } }
类加载机制
类加载过程
加载 → 连接(验证→准备→解析) → 初始化
类加载器层次
Bootstrap ClassLoader (根加载器,C++实现)
↑
Extension ClassLoader (扩展类加载器)
↑
Application ClassLoader (应用类加载器)
↑
用户自定义类加载器
双亲委派模型
javaclass ClassLoader { protected Class<?> loadClass(String name, boolean resolve) { // 1. 检查类是否已加载 Class<?> c = findLoadedClass(name); if (c != null) return c; // 2. 委派给父类加载器 if (parent != null) { c = parent.loadClass(name, false); } else { // 3. 父加载器为空,尝试 Bootstrap c = findBootstrapClassOrNull(name); } // 4. 找到则返回,找不到则自己加载 if (c == null) { c = findClass(name); } return c; } }
垃圾回收算法
常见 GC 算法
1. 标记-清除(Mark-Sweep)
java// 缺点:产生内存碎片,效率不稳定 // 步骤: // 1. 标记所有可达对象 // 2. 清除未标记对象
2. 复制算法(Copying)
┌────────────────────────────────┐
│ 新生代 (Eden + S0 + S1) │
│ │
│ ┌───────┐ ┌───────┐ ┌──────┐ │
│ │ Eden │ │ From │ │ To │ │
│ │ 80% │ │ S0 10% │ │ S1 10%│ │
│ └───────┘ └───────┘ └──────┘ │
└────────────────────────────────┘
3. 标记-整理(Mark-Compact)
解决内存碎片问题,适用于老年代。
分代收集理论
年轻代 (Young Generation)
├── Eden 区 (对象优先分配)
├── Survivor S0
└── Survivor S1
│
│ Minor GC (频繁)
▼
老年代 (Old/Tenured Generation)
│
│ Major/Full GC (较少)
▼
永久代/元空间 (方法区)
JVM 调优参数
堆内存设置
bash# 设置堆最小和最大 java -Xms512m -Xmx2g -jar app.jar # 年轻代大小 java -Xmn256m -jar app.jar # Eden 和 Survivor 比例 java -XX:SurvivorRatio=8 -jar app.jar # Eden:S0:S1 = 8:1:1
GC 收集器选择
bash# Serial GC(单线程,适合小型应用) java -XX:+UseSerialGC -jar app.jar # Parallel GC(吞吐量优先) java -XX:+UseParallelGC -XX:+UseParallelOldGC -jar app.jar # CMS GC(低延迟) java -XX:+UseConcMarkSweepGC -jar app.jar # G1 GC(推荐,更好的大堆场景) java -XX:+UseG1GC -jar app.jar # ZGC(超低延迟,JDK 11+) java -XX:+UseZGC -jar app.jar
常见面试问题
Q1: 对象分配过程?
javapublic class ObjectAllocation { public static void main(String[] args) { Object obj = new Object(); // 分配过程: // 1. 检查常量池是否已加载类 // 2. 在堆中分配内存 // 3. 对象头设置(Mark Word、Klass 指针) // 4. 调用构造方法 } }
Q2: 如何判断对象可回收?
java// 1. 引用计数法(不用,循环引用无法回收) // 2. 可达性分析(GC Root) // - 虚拟机栈引用的对象 // - 方法区静态属性引用的对象 // - 方法区常量引用的对象 // - 本地方法栈 JNI 引用的对象 Object obj = new Object(); // obj 是局部变量,在虚拟机栈中 // obj 引用堆中的 Object 实例 // 如果 obj = null,则 Object 实例无引用,可被回收
Q3: Minor GC 和 Full GC 的区别?
| 类型 | 触发条件 | 回收区域 | 频率 |
|---|---|---|---|
| Minor GC | Eden 满 | 年轻代 | 频繁 |
| Major GC | 老年代满 | 老年代 | 较少 |
| Full GC | 多种条件 | 全部 | 少 |
Q4: G1 和 CMS 的区别?
| 特性 | CMS | G1 |
|---|---|---|
| 回收范围 | 老年代 | 整个堆 |
| 停顿时间 | 可预期 | 可控制 |
| 内存碎片 | 有碎片 | 无碎片 |
| 并发阶段 | 4 阶段 | 多阶段 |
| 适用场景 | 低延迟 | 大堆 |
Q5: JVM 排查工具?
bash# 查看 JVM 参数 jinfo -flags <pid> # 查看 GC 情况 jstat -gcutil <pid> 1000 # 生成堆转储 jmap -dump:format=b,file=heap.hprof <pid> # 分析堆转储 jhat heap.hprof # 更好的分析工具 # jvisualvm(GUI) # Arthas(阿里诊断工具)
最佳实践
- 对象分配:优先在栈上分配(小对象),大对象直接进入老年代
- 避免频繁 Full GC:合理设置堆大小,关注对象生命周期
- 减少对象创建:复用对象,使用对象池
- 合理选择 GC:根据业务特性选择合适的收集器
- 监控和调优:使用 jstat、jstack、jmap 等工具监控