深入理解JVM:从原理到实践的完整指南

为你系统地介绍Java虚拟机的核心知识。JVM是Java生态的基石,理解它的工作原理不仅有助于编写更高效的代码,还能帮助你在遇到性能问题时快速定位原因。本文将从JVM的发展历史开始,逐步深入到内存模型、类加载机制、垃圾回收等核心概念。

1. JVM简介与发展史

什么是JVM?

JVM(Java Virtual Machine,Java虚拟机)是通过软件模拟的、运行在完全隔离环境中的完整计算机系统。与VMware、VirtualBox等通过软件模拟物理CPU指令集的虚拟机不同,JVM通过软件模拟Java字节码的指令集,可以说是"一台被定制过的现实当中不存在的计算机"。

JVM的发展历程

1. Sun Classic VM(1996年)

  • 世界上第一款商用Java虚拟机

  • 只提供解释器,JIT编译器需要外挂

  • 解释器和编译器不能协同工作

  • JDK 1.4后被完全淘汰

2. Exact VM(1998年)

  • 具备现代高性能虚拟机雏形

  • 引入热点探测技术和混合工作模式

  • 只在Solaris平台短暂使用

  • 后被HotSpot VM替代

3. HotSpot VM(主流)

  • 最初由"Longview Technologies"公司设计,1997年被Sun收购

  • 2009年Sun被甲骨文收购

  • 从JDK 1.3起成为默认虚拟机

  • 名称源于其"热点代码探测技术"

  • 目前市场占有率最高,是Oracle JDK和OpenJDK的默认虚拟机

4. JRockit(专注于服务器端)

  • 专注于服务器端应用,不包含解析器

  • 全部代码靠即时编译器编译执行

  • 号称"世界上最快的JVM"

  • 2008年被Oracle收购,优秀特性被整合到HotSpot

5. J9 JVM(IBM)

  • IBM开发的商用虚拟机

  • 用于IBM各种Java产品

  • 2017年开源为OpenJ9

6. Taobao JVM(国产骄傲)

  • 阿里基于OpenJDK深度定制

  • 创新的GCIH技术实现off-heap

  • 针对大数据场景优化

  • 已在淘宝、天猫全线替换Oracle官方JVM

重要概念:所有这些JVM实现都必须符合Oracle发布的《Java虚拟机规范》,这是一本完整描述JVM组成部分的技术规范。

2. JVM的运行流程

JVM的执行流程可以概括为以下四个核心部分:

  1. 类加载器(ClassLoader):将.class文件加载到内存

  2. 运行时数据区(Runtime Data Area):Java程序运行时的内存空间

  3. 执行引擎(Execution Engine):将字节码翻译成系统指令

  4. 本地库接口(Native Interface):调用其他语言实现的功能

执行流程:Java代码 → 字节码文件 → 类加载器加载 → 运行时数据区 → 执行引擎翻译 → 本地库接口 → CPU执行

3. JVM运行时数据区(内存布局)

JVM运行时数据区是Java程序运行时的内存空间,与Java内存模型(JMM)是不同的概念。它由以下五大部分组成:

3.1 堆(Heap)- 线程共享

  • 作用:存储程序中创建的所有对象实例

  • 参数-Xms(最小启动内存)、-Xmx(最大运行内存)

  • 结构

    • 新生代(Young Generation):存放新建对象

      • Eden区:对象出生地

      • Survivor区(S0/S1):幸存者区

    • 老年代(Old Generation):存放长期存活对象

垃圾回收时,Eden区存活对象会移到未使用的Survivor区,并清理当前Eden和正在使用的Survivor。

3.2 Java虚拟机栈(Stack)- 线程私有

  • 作用:描述Java方法执行的内存模型

  • 栈帧(Stack Frame)结构

    1. 局部变量表:存放方法参数和局部变量

    2. 操作数栈:方法执行的操作栈

    3. 动态链接:指向运行时常量池的方法引用

    4. 方法返回地址:PC寄存器的地址

线程私有的概念:由于线程切换需要恢复到正确位置,每个线程需要独立的程序计数器等资源。

3.3 本地方法栈 - 线程私有

  • 与虚拟机栈类似,但为本地方法(Native Method)服务

3.4 程序计数器 - 线程私有

  • 作用:记录当前线程执行的行号

  • 执行Java方法时:记录字节码指令地址

  • 执行Native方法时:值为空

  • 唯一不会发生OutOfMemoryError的区域

3.5 方法区 - 线程共享

  • 作用:存储类信息、常量、静态变量、即时编译器编译后的代码

  • 实现演变

    • JDK7及之前:永久代(PermGen)

    • JDK8及之后:元空间(Metaspace)

  • 重要变化

    • 元空间使用本地内存,不受JVM最大内存参数限制

    • JDK8将字符串常量池移动到堆中

比喻理解:方法区好比汽车定义的"动能提供装置",永久代和元空间则是具体的实现技术(汽油发动机 vs 电动发动机)。

运行时常量池

  • 方法区的一部分

  • 存放字面量(字符串、final常量、基本数据类型值)和符号引用(类、字段、方法的名称和描述符)

4. 内存溢出问题分析

Java堆溢出

  • 现象java.lang.OutOfMemoryError: Java heap space

  • 原因:对象数量达到堆容量上限

  • 调试参数-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError

  • 分析工具:MAT(Memory Analyzer Tool)

  • 区分概念

    • 内存泄漏(Memory Leak):泄漏对象无法被GC回收

    • 内存溢出(Memory Overflow):内存对象确实应该存活

栈溢出

  • 参数设置-Xss设置栈容量

  • 两种异常

    1. StackOverflowError:请求深度大于允许的最大深度

    2. OutOfMemoryError:扩展栈时无法申请足够内存

  • 多线程场景:减少最大堆和栈容量来换取更多线程

5. JVM类加载机制

类的生命周期

加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载

类加载详细过程

  1. 加载(Loading)

    • 通过全限定名获取二进制字节流

    • 转换为方法区的运行时数据结构

    • 生成Class对象作为访问入口

  2. 验证(Verification)

    • 确保字节流符合规范

    • 包括文件格式、字节码、符号引用验证

  3. 准备(Preparation)

    • 为静态变量分配内存

    • 设置默认初始值(如int为0)

  4. 解析(Resolution)

    • 将符号引用替换为直接引用
  5. 初始化(Initialization)

    • 执行类构造器方法

    • Java程序代码真正开始执行

双亲委派模型

模型结构

  1. 启动类加载器(Bootstrap ClassLoader):C++实现,加载核心类库

  2. 扩展类加载器(Extension ClassLoader):加载ext目录

  3. 应用程序类加载器(Application ClassLoader):加载应用程序

  4. 自定义类加载器:根据需要定制

工作流程:子加载器收到请求 → 委派给父加载器 → 最终到达启动类加载器 → 父加载器无法完成时子加载器尝试

优点

  1. 避免重复加载类

  2. 保证Java核心API安全(防止篡改)

破坏双亲委派模型 - JDBC案例

问题背景:JDBC的Driver接口在rt.jar中(由启动类加载器加载),但具体实现在数据库厂商的jar包中。

解决方案:使用线程上下文类加载器(Thread Context ClassLoader)

复制代码
// DriverManager中的关键代码
ClassLoader callerCL = Thread.currentThread().getContextClassLoader();

交互流程:应用程序 → 线程上下文类加载器 → 加载数据库驱动 → 破坏标准的双亲委派模型

6. 垃圾回收机制

死亡对象判断算法

1. 引用计数算法(Java未采用)

  • 每个对象维护引用计数器

  • 无法解决循环引用问题

  • Python等语言使用此算法

2. 可达性分析算法(Java采用)

  • 从GC Roots对象开始向下搜索

  • 形成引用链

  • 不可达对象判定为可回收

GC Roots包括

  • 虚拟机栈中引用的对象

  • 方法区中静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中JNI引用的对象

引用类型(强度依次递减):

  1. 强引用(Strong Reference):Object obj = new Object()

  2. 软引用(Soft Reference):内存不足时回收

  3. 弱引用(Weak Reference):下次GC时回收

  4. 虚引用(Phantom Reference):对象回收时收到通知

垃圾回收算法

1. 标记-清除算法

  • 标记所有需要回收的对象

  • 统一回收被标记对象

  • 缺点:效率低、产生内存碎片

2. 复制算法

  • 内存分为两块,每次使用一块

  • 存活对象复制到另一块

  • HotSpot实现:Eden:Survivor From:Survivor To = 8:1:1

  • 适合新生代(对象存活率低)

3. 标记-整理算法

  • 标记存活对象

  • 向一端移动存活对象

  • 清理边界外内存

  • 适合老年代

4. 分代收集算法

  • 新生代:复制算法

  • 老年代:标记-清除或标记-整理

  • 对象晋升:大对象、经历15次GC的对象进入老年代

GC类型

  • Minor GC:新生代GC,频繁快速

  • Full GC/Major GC:老年代GC,较慢(可能慢10倍以上)

垃圾收集器

收集器类型矩阵

收集器 新生代/老年代 算法 特点 应用场景
Serial 新生代 复制 单线程,STW Client模式默认
ParNew 新生代 复制 Serial的多线程版 与CMS配合
Parallel Scavenge 新生代 复制 吞吐量优先 后台计算
Serial Old 老年代 标记-整理 Serial的老年代版 Client模式
Parallel Old 老年代 标记-整理 Parallel Scavenge的老年代版 吞吐量优先
CMS 老年代 标记-清除 并发收集,低停顿 B/S系统
G1 全区域 标记-整理+复制 区域划分,可预测停顿 替换CMS

重要概念

  • 并行(Parallel):多GC线程并行,用户线程等待

  • 并发(Concurrent):GC与用户线程交替执行

  • 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间)

CMS收集器详解

  1. 初始标记(STW):标记GC Roots直接关联对象

  2. 并发标记:GC Roots Tracing

  3. 重新标记(STW):修正并发标记期间的变化

  4. 并发清除

G1收集器特点

  • 将堆划分为多个Region

  • 可预测停顿时间

  • 不产生内存碎片

  • 适合大内存应用

一个对象的一生

"我是一个普通的Java对象,出生在Eden区,在Survivor区来回迁移,18岁后进入老年代,经历多次GC后最终被回收。"

7. Java内存模型(JMM)

主内存与工作内存

  • 主内存:所有变量存储位置

  • 工作内存:线程私有的变量副本

  • 线程间变量传递必须通过主内存

内存交互操作(8种原子操作)

  1. lock(锁定)

  2. unlock(解锁)

  3. read(读取)

  4. load(载入)

  5. use(使用)

  6. assign(赋值)

  7. store(存储)

  8. write(写入)

Java内存模型三大特性

  1. 原子性:基本数据类型访问具备原子性,复杂操作需要synchronized

  2. 可见性:volatile、synchronized、final保证

  3. 有序性:happens-before原则

happens-before原则

  1. 程序次序规则

  2. 锁定规则

  3. volatile变量规则

  4. 传递规则

  5. 线程启动规则

  6. 线程中断规则

  7. 线程终结规则

  8. 对象终结规则

volatile关键字

特性

  1. 保证可见性:修改立即对其他线程可见

  2. 禁止指令重排序

注意:volatile不保证原子性

复制代码
// 错误示例:volatile不保证自增原子性
private volatile int num = 0;
num++;  // 非原子操作,可能线程不安全

适用场景

  1. 运算结果不依赖当前值

  2. 变量不需要与其他状态变量共同约束

单例模式双重检查锁定

复制代码
class Singleton {
    private volatile static Singleton instance = null;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {  // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {  // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile必要性:防止指令重排序导致返回未完全初始化的对象

8. 总结

JVM是Java生态的核心,理解其工作原理对于编写高性能、稳定的Java应用至关重要。本文从JVM的发展历史、内存模型、类加载机制、垃圾回收到Java内存模型进行了全面介绍。关键点包括:

  1. 内存管理:理解堆、栈、方法区的区别和作用

  2. 类加载:掌握双亲委派模型及其破坏场景

  3. 垃圾回收:熟悉不同算法和收集器的适用场景

  4. 线程安全:理解JMM和volatile的内存语义

JVM的优化是一个持续的过程,需要根据具体应用场景选择合适的垃圾收集器和调优参数。希望这篇博客能帮助你建立起对JVM的系统性理解,为深入Java性能优化打下坚实基础。

相关推荐
Rick19932 小时前
Java内存参数解析
java·开发语言·jvm
明湖起风了3 小时前
mqtt消费堆积
java·jvm·windows
Free Tester4 小时前
如何判断 LeakCanary 报告的严重程度
java·jvm·算法
wgzrmlrm7410 小时前
如何解决ORA-28040没有匹配的验证协议_sqlnet.ora版本兼容设置
jvm·数据库·python
wgzrmlrm7412 小时前
如何从SQL中提取年份或月份:EXTRACT与日期函数用法
jvm·数据库·python
ruan11451415 小时前
关于HashMap--个人学习记录
java·jvm·servlet
__土块__1 天前
大厂后端一面模拟:从线程安全到分布式缓存的连环追问
jvm·redis·mysql·spring·java面试·concurrenthashmap·大厂后端
fly spider2 天前
一文概括 JVM 核心内容
jvm
brahmsjiang2 天前
Java类加载机制解析:从JVM启动到双亲委派,再到Android的特殊实现
android·java·jvm