参考文献 :
一篇JVM详细图解,坚持看完!带你真正搞懂Java虚拟机
深入理解 Java 虚拟机(JVM)从入门到精通
初识JVM
JVM即Java Virtual Machine,Java虚拟机,是Java运行的虚拟环境,跨平台的基础,其核心功能主要有:
- 解释运行 。Java是一种既编译又解释的语言,编译器将
.java文件编译为.class字节码文件,JVM负责将其解释为机器码; - 内存管理。对象和方法的内存分配,以及垃圾回收等内存管理工作都由JVM完成;
- 即时编译。为了支持跨平台,代码是由虚拟机实时解释的,若不作优化性能远远不如C,故JVM采用了JIT即时编译技术,将热点代码转为机器码并保存至内存,可达到接近C的性能。
JVM本质也是一种软件,目前也有很多其他语言借助JVM运行(Kotlin、Scala),故只要遵守虚拟机规范即可(官方规范在此,有兴趣也可以自己搓个JVM),常见有以下几种实现:
- HotSpot: Oracle/Sun 官方主推的 JVM 实现,也是目前使用最广泛的版本(OpenJDK、Oracle JDK 默认)
- GraalVM:Oracle 推出的新一代多语言虚拟机,不仅支持 Java,还兼容 Scala、Python、C++ 等
- OpenJ9:IBM 开源的 JVM 实现(基于 J9)
- Dragonwell:阿里基于 Oracle HotSpot(OpenJDK)的定制化
JVM结构详解
JVM结构如下图:

其大致工作过程为:
- 类加载器(ClassLoader)加载.class字节码文件到JVM内存;
- 执行引擎对字节码进行处理(解释执行+JIT编译),转化为机器码;
- JVM管理运行时内存(分配/回收),支撑代码执行;
- 执行完成后,释放资源并退出JVM进程
本地方法为C++实现的方法,不可修改
下面将逐个内容进行详解。
使用工具
后面涉及查看字节码、进程监控等要使用诸多工具,这里提前拿出来整合在一起。
字节码查看工具jclasslib
字节码是未编码的二进制文件,需用特定工具jclasslib查看,该工具目前已集成在IDEA中,打开方式如下:

字节码与源码对照如下:

集成的是个阉割版,要使用更多功能还是得装插件:

使用插件看的一般信息:

可见更精准的字节码如下:

进程监控工具Arthus
Arthas 是阿里开源的Java诊断工具,无侵入式排查JVM/应用问题,可在程序运行时诊断问题。
官方下载链接:https://arthas.aliyun.com/doc/download.html
有关快速使用教程可见:https://arthas.aliyun.com/doc/quick-start.html
上线后的程序未生效,可用该工具查看源码是否正确更新。
较强的功能是可用jad class文件名反编译成源码,如果没有保护的话将直接获取到原源码信息。
注释,变量名等还需处理,能保存基本执行逻辑
介绍一个不停机上线热补丁的功能:
jad --source-only 类名>目录/文件名.java // 反编译
// 修改代码
mc -c 加载器哈希 目录/文件名.java -d 输出目录 // 编译修改过的代码
retransform 字节码路径 // 加载新的字节码
应急手段,无法修改类结构(新增、删除方法),已有对象不受影响(已创建实例使用旧字节码,可能产生数据不一致问题),仅用于紧急bug修复。
其他内置命令
javap -v 字节码文件 jdk自带反编译工具,可查看字节码内容,jar包可用jar -xvf解压后查看
sd-jdi.jar 也可查看字节码及元数据信息,使用方法可见https://javabetter.cn/jvm/hsdb.html
字节码文件
一般信息中包含:
- 魔数:字节码固定为
0xCAFEBABE,系统通过该文件头校验文件类型,JPEG为FFD8FF,PNG为89504E47; - 主副版本号:通常关注主版本号,编译版本为
主版本号-44,环境冲突字节码与环境不一致时通常调整字节码文件; - 方法计数:类中的方法数量,构造方法隐式,故为2;
常量池 :
为避免相同内容重复定义,节省空间,将字段存储在常量池,如定义两个String,不同字段指向同一个常量。
方法 :
方法的字节码文件,常用命令如下:
| 命令 | 含义 |
|---|---|
| istore_1 | 操作数栈顶数存到局部变量表索引1的位置 |
| iload_1 | 局部变量表索引1的位置存入操作数栈 |
| iconst_1 | int常量1压入操作数栈 |
| iadd | 栈顶两个数相加 |
| iinc 1 1 | 局部变量表1自增1 |
由此我们比较i++和i=i+1的性能差距,字节码如下:
2 iinc 1 by 1 # 自增1
5 iload_1 # 加载局部变量表到操作数栈
6 iconst_1 # 常量压入操作数栈
7 iadd # 栈顶两个数相加
8 istore_1 # 保存局部变量
由此可见自增操作效率比重新赋值高得多。(但通常JIT会优化成等价机器码,性能差别不大)
类的生命周期
类的生命周期即类的加载、使用、卸载过程,我们通常关注其准备阶段,即加载、连接、初始化。
加载
该部分负责将类加载到JVM内存中,具体过程如下:
- 类加载器根据全限定类名,以二进制流形式获取字节码信息;
- JVM解析字节码文件后,在元空间生成
InstanceKlass对象(保存类的访问标志、常量池、字段表等基础元数据); - JVM在堆中生成class对象,对象持有指向
InstanceKlass的指针。
补充:
InstanceKlass是 HotSpot JVM 中用 C++ 实现的核心数据结构,是Java类在JVM底层的元数据载体,每个类只有一个实例,对Java层完全透明。
类加载时,JVM 在元空间创建 InstanceKlass存储类元数据,在堆创建 Class作为访问代理,也就是说InstanceKlass是真身,Class是访问代理。
连接
连接阶段主要完成验证、准备和解析工作。
验证负责校验字节码是否满足Java虚拟机要求,主要校验以下内容:
- 文件格式,魔数是否为cafebabe,以及主次版本号;
- 元信息验证,如类必须有父类;
- 字节码验证,检查语义;
- 符号引用验证,是否访问其他类的私有方法。
准备阶段负责完成前置工作,为静态变量static分配内存并设置初始值,final修饰的直接赋值,无初始值过程。
解析过程负责将常量池中的符号引用替换为直接引用。
初始化
执行静态代码块中的代码,为静态变量赋值,执行字节码中的clinit构造部分(静态变量赋值和静态代码块执行)。
以下行为可能触发初始化:
- 访问类的静态变量或方法,final修饰且右边是常量的不会触发;
- 调用
class.forName(String className); new类对象时;Main方法的当前类。
特殊情况不执行clinit方法:
- 无静态代码块且无静态变量赋值语句;
- 有静态变量声明,但无赋值语句;
- 静态变量的定义使用了
static final关键字(编译期常量),准备阶段直接初始化。
访问父类不触发子类初始化,子类调用clinit前,JVM会保证父类的clinit已执行。
类加载器
类加载器ClassLoader负责提供给应用获取类和接口字节码数据,根据加载类的类型不同可分为启动类加载器、扩展类加载器和应用程序类加载器。
启动类加载器Bootstrap
启动类加载器负责加载Java程序的默认依赖,即jre/lib/rt.jar下的文件,如String、ArrayList等类。
该加载器使用C++实现,用户无法通过代码获取启动类加载器。
扩展类加载器Extension
扩展类加载器负责加载通用但非必须的类,通常为jre/lib/ext下的类。
该类间接继承自URLClassLoader,可使用虚拟机参数-Djava.ext.dirs="jar包目录"覆盖或追加加载目录。
应用程序类加载器Application
应用程序类加载器加载额外应用类,具体为classpath用户路径下的类,可用Arthas的classloader -l命令查看类加载器的哈希值、加载的类列表等信息。
要注意的是相同的类加载器+相同的全类名会被视作一个类。
双亲委派机制
三类核心类加载器核心目的是解决类的隔离和安全问题 ,如果使用一个用户能接触到的类加载器加载所有类,可能会出现用户代码篡改核心类库的场景(如加载重写的java.lang.String),具体一个类由何种加载器加载要由双亲委派机制决定。
双亲委派机制的核心功能有两点:
- 保证类加载的安全性,核心类库不可替换;
- 避免重复加载。
具体过程为:
- 向上查找,类加载器接受加载类任务时自底向上查找是否被加载过,未被加载则进入下一阶段;
- 向下加载,若类不在当前加载器的加载目录,则向下委派,可保证优先由启动类加载。
若核心类(如String)已加载,再次加载用户重写的类不会被加载,保证安全性。
若要主动加载可使用反射的Class.forName()加载字节码文件。
打破双亲委派机制
前面提到,同一个加载器加载的相同全类名的类会被视为一个类,当一个服务运行多个应用时,可能导致类的冲突(比如Tomcat会运行多个web应用),所以有时候需要打破双亲委派机制。
首先介绍下ClassLoader的原理,类加载器加载类大致可以分为如下四步:
-
loadClass,类加载的入口,提供双亲委派机制;

-
findClass,子类的具体实现,获取具体二进制数据;
-
defineClass,调用JVM方法,将数据加载到内存;
-
resolveClass,进入连接阶段。
打破双亲委派机制有以下三种方法:
- 自定义类:自定义类加载器,并重写
loadClass或findClass方法。 - 线程上下文:JDBC用启动类加载器加载
DriverManager,但用应用类加载器加载mysql驱动,同样违反了双亲委派机制。可用Thread.currentThread().getContextClassLoader();获取应用程序类加载器,得以找到驱动。 - OSGI框架:osgi模块化框架,存在不同级之间类加载器的委托加载,可实现热部署。
JVM内存区域
JVM内存区域主要指的是进程运行时的数据区域,根据是否被线程共享可分为两类:
- 线程不共享:程序计数器、JVM栈、本地方法栈;
- 线程共享:方法区、堆区。
程序计数器
程序计数器(PC 寄存器)记录当前线程正在执行的字节码指令地址;类加载阶段会将指令偏移量转换为实际内存地址,JVM 执行引擎根据程序计数器的值定位并执行对应指令。
多线程环境下,CPU 会不断切换线程执行,为了保证线程切换后能恢复到正确的执行位置,每个线程都必须拥有独立的程序计数器,因此程序计数器是线程私有的。
程序计数器是 JVM 中唯一不会发生内存溢出(OOM)和栈溢出(SOF) 的区域,开发者无需手动管理。
栈
虚拟机栈是描述 Java 方法执行的内存模型,每个线程独有 ,一个虚拟机栈。每个方法执行时,都会创建一个栈帧(Stack Frame),用于存储:局部变量表、操作数栈、动态链接、方法返回地址等信息。
方法从调用到执行完毕的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧核心组成:
- 局部变量表:数组结构,存储方法内的局部变量、方法参数;
- 操作数栈:栈式结构,最大深度由编译器确定,用于执行运算、存储中间结果;
- 动态链接:将符号引用转换为直接内存地址的引用;
- 方法返回地址:方法执行完毕后,回到调用该方法的位置。
异常与配置
该区域可能发生两种异常 :
StackOverflowError:线程请求的栈深度超过虚拟机允许的最大深度(如无限递归);
OutOfMemoryError:栈扩展时无法申请到足够内存。
栈默认大小约 1MB(取决于操作系统与架构),可通过虚拟机参数-Xss指定栈大小(如-Xss256k)。HotSpot 虚拟机对栈大小的限制范围:100k ~ 1024M,256k 是生产环境常用配置,可有效节省内存。
堆
JVM 内存中最大的一块区域,线程共享 ,唯一用途是存储对象实例与数组,是垃圾回收(GC)的核心管理区域。
堆内存分为三部分:used已使用、total已分配和max最大可分配,堆内存溢出 即used=max=total内存已满且无法继续扩容时,会报OutOfMemory错误。
通常使用虚拟机参数-Xms数值指定初始堆内存大小、Xmx数值指定最大可分配内存大小,生产环境建议将-Xms与-Xmx设为相同值,避免堆内存频繁扩容带来性能损耗。
方法区
线程共享区域,用于存储 JVM 加载的类元信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
JDK7前方法区存放在永久代空间,托管在JVM堆内,JDK8后方法区在元空间,该空间位于操作系统维护的直接内存。
直接内存指不属于Java运行的内存区域,JDK1.4后引入NIO,使用直接内存,主要解决两个问题:
- 避免堆内对象频繁 GC 带来的性能影响;
- 减少 IO 操作中「本地内存 ↔ Java 堆」的数据拷贝,提升效率。
运行时常量池是方法区的一部分,存放编译期生成的字面量、符号引用,类加载后存入运行时常量池,支持运行期间动态添加常量。
总结
JVM内存加载的整体流程如下:
- 类加载器通过双亲委派模型加载字节码到 JVM 内存,保证核心类不被篡改,实现类加载的安全与隔离;
- 堆 (存储对象 / 数组)和方法区(存储类元信息)是线程共享区域,其中堆是 JVM 最大内存区域,也是垃圾回收的核心目标;
- 每个线程拥有独立的程序计数器 (记录执行位置)和虚拟机栈 (管理方法调用);程序计数器无溢出风险,虚拟机栈可能发生栈溢出或内存溢出,但线程私有区域本身不会产生内存泄漏;
