程序员面试宝典

一站式面试准备平台

返回分类
Java高级

Java 虚拟机(JVM)深入理解

深入理解 JVM 内存结构、类加载机制、垃圾回收算法及性能调优

2026-03-26
阅读时间: 11分钟

Java 虚拟机(JVM)深入理解

JVM 是 Java 技术的核心,深入理解 JVM 对于写出高效 Java 程序至关重要。

JVM 内存结构

运行时数据区

┌─────────────────────────────────────────────────────┐
│                   JVM 进程内存                        │
├─────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐                  │
│  │   方法区     │  │   堆内存     │                  │
│  │ (Method Area)│  │   (Heap)    │                  │
│  │ - 类信息    │  │ - 对象实例  │                  │
│  │ - 静态变量  │  │ - 数组      │                  │
│  │ - 常量池    │  │             │                  │
│  └─────────────┘  └─────────────┘                  │
│         ▲                  ▲                          │
│         │                  │                          │
│  ┌──────┴──────────────────┴──────┐                  │
│  │         直接内存 (Direct)        │                  │
│  └─────────────────────────────────┘                  │
│                                                     │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐ │
│  │  程序计数器  │  │   虚拟机栈   │  │ 本地方法栈   │ │
│  │ (PC Register)│  │  (VM Stack)  │  │ (Native M.)  │ │
│  └─────────────┘  └─────────────┘  └─────────────┘ │
└─────────────────────────────────────────────────────┘

堆内存详解

Java 堆是 GC 的主要管理区域:

java
public 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]; // 数组对象在堆上
    }
}

虚拟机栈

每个线程有自己的虚拟机栈:

java
public 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 (应用类加载器)
       ↑
   用户自定义类加载器

双亲委派模型

java
class 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: 对象分配过程?

java
public 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 GCEden 满年轻代频繁
Major GC老年代满老年代较少
Full GC多种条件全部

Q4: G1 和 CMS 的区别?

特性CMSG1
回收范围老年代整个堆
停顿时间可预期可控制
内存碎片有碎片无碎片
并发阶段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(阿里诊断工具)

最佳实践

  1. 对象分配:优先在栈上分配(小对象),大对象直接进入老年代
  2. 避免频繁 Full GC:合理设置堆大小,关注对象生命周期
  3. 减少对象创建:复用对象,使用对象池
  4. 合理选择 GC:根据业务特性选择合适的收集器
  5. 监控和调优:使用 jstat、jstack、jmap 等工具监控

相关标签