JVM
一、类加载机制
JVM整体的运行原理:首先从".java"代码文件编译成".class"字节码文件,然后类加载器把".class"字节码文件中的类给加载到JVM中,接着JVM执行我们写好的那么类中的代码。
![]()
1、JVM什么时候会加载一个类?
一个类从加载到使用,一般会经过下面这个过程:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
当代码中使用类的时候,就会加载一个类。
比如包含main()
方法的主类在JVM进程启动之后被加载到内存(加载字节码文件),然后开始执行main()
方法中的代码。
2、验证、准备、解析、初始化过程

2.1 概念
-
验证阶段:根据Java虚拟机规范,校验加载进来的".class"文件中的内容是否符合指定的规范。
-
准备阶段:给类分配一定的内存空间,以及它里面的类变量(即static修饰的变量)分配内存空间,设置默认的初始值。(而实例变量在创建类的实例对象时才会初始化)
-
解析阶段:将
符号引用
替换为直接引用
-
初始化阶段
(核心阶段):正式执行类初始化的代码,完成类变量的真正赋值操作。static
静态代码块,也是在这个阶段完成的。(这个阶段主要是准备好类级别的数据,比如静态代码块,静态成员赋值,
初始化跟对象无关,用new关键字才会构造出一个对象出来)
例子:
public class ReplicaManager {
public static int flushInterval = Configuration.getInt("replica.flush.interval");
}
- 准备阶段:首先给ReplicaManager类分配一定的内存空间,然后给类变量flushInterval分配内存空间,设置0初始值
- 初始化阶段:`Configuration.getInt("replica.flush.interval") `完成一个配置项的读取,然后赋值给类变量`flushInterval`
2.2 什么时候初始化一个类?
- 比如"new ReplicaManager()"实例化对象,就会触发类的加载到初始化过程,把这个类准备好,然后再实例化一个对象出来。
- 包含"main()"方法的主类,必须是立马初始化的
- 初始化一个类的时候,如果父类还没初始化,那么必须先初始化它的父类
类初始化时机:
- 当创建某个类的新实例时(如通过new或者反射、克隆、反序列化等)
- 当调用某个类的静态方法时
- 当使用某个类或者接口的静态字段时
- 调用Java API的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
- 当初始化某个子类时
- 当虚拟机启动某个被标明为启动类的类
3、类加载器和双亲委派机制
3.1 类加载器
-
启动类加载器:
Bootstrap ClassLoader
负责加载机器上安装的Java目录下的核心类(
"lib"
目录) -
扩展类加载器:
Extension ClassLoader
负责加载
"lib\ext"
目录中的类 -
应用程序类加载器:
Application ClassLoader
负责加载
"ClassPath"
环境变量所指定的路径中的类,可以理解为自己写好的Java代码 -
自定义类加载器
根据自己的需求加载一些类
(如何实现一个自定义类加载器?自己写一个类,继承ClassLoader类,重写类加载的方法)
3.2 双亲委派机制
启动类加载器位于最上层、扩展类加载器在第二层、应用程序类加载器在第三层、最后一层是自定义类加载器

如果一个应用程序类加载器需要加载一个类,首先委派给自己的父类加载器去加载,最后传导到顶层的类加载器去加载,如果父类加载器在自己负责加载的范围内,没找到这个类,那么就下推加载权力给自己的子类加载器。
好处:
- 每个层级的类加载器各司其职,不会重复加载一个类
- 保护一些核心类的安全
3.3 Tomcat类加载机制
Tomcat本身就是用Java写的,它自己就是一个JVM,我们写好的那些系统程序,通过编译后的.class
文件放入一个war包,然后在tomcat中运行。

- Tomcat自定义了Common、Catalina、Shared等类加载器,是用来加载Tomcat自己的一些核心基础类库的
- Tomcat为每个部署在里面的Web应用都有一个对应的WebApp类加载器,负责加载我们部署的这个Web应用的类
- Jsp类加载器,则是给每个JSP都准备了一个Jsp类加载器
每个WebApp负责加载自己对应的那个Web应用的class文件,即我们写好的系统打包好的war包中的所有class文件,不会传到给上层类加载器去加载。
Shared底层细分了不同的web类加载器用于隔离不同的web项目,打破了双亲委派机制,由自定义类加载器先加载类。
3.3.1 破坏双亲委派
原因:隔离、灵活、性能
-
不同的项目依赖Spring不同的包,那么就会导致依赖冲突问题,如果用不同的加载器,就能起到隔离的作用
-
当需要增加或者减少单独的某个web项目的部署,用多个类加载器可以灵活的实现
-
用多个类加载器性能要比用一个类加载器性能要高
二、内存区域
JVM在运行我们写好的代码时,必须使用多块内存空间,不同的内存空间用来放不同的数据,然后配合我们写的代码流程,才能让我们的系统运行起来。
1、内存区域划分
- 线程共享的区域
- 堆
- 方法区
- 直接内存(非运行时数据区的一部分)
- 线程私有的区域
- 程序计数器
- 虚拟机栈
- 本地方法栈
如图是JDK1.8
之前:

如图是JDK1.8
:

1.1 存放类的方法区
方法区在JDK1.8以前的版本,代表JVM中的一块区域。
主要是放从".class"文件里加载进来的类,还有一些类似常量池的东西也放在这个区域里。
JDK1.8以后,这块区域改成了"Metaspace",即元数据空间的意思,主要还是存放我们自己写的各种类相关的信息。

1.2 执行代码指令用的程序计数器
我们编写的代码首会存在于".java"后缀的文件中,但是计算机是看不懂我们写的代码的,所以就得通过编译器,把".java"后缀的源文件编译成".class"后缀的字节码文件,
这份文件存放的就是我们写出来的代码编译好的字节码。
而字节码指令
对应了一条一条的机器指令,计算机只有读到这种机器码指令,才知道具体应该要干什么。比如字节码指令可能会让计算机从内存读取某个数据,或者把某个数据写入到内存里。
在执行字节码指令的时候,JVM需要一个特殊的内存区域,就是"程序计数器"
,用来记录当前执行的字节码指令的位置,即记录目前执行到了哪一条字节码指令。
1.3 虚拟机机栈
Java代码在执行的时候,一定是线程来执行某个方法中的代码。在方法里,我们经常会一定一些方法内的局部变量。
因此,JVM必须有一块区域是用来保存每个方法内的局部变量等数据的,这个区域就是Java虚拟机栈
每个线程都有自己的Java虚拟机栈,如果线程执行了一个方法,就会对这个方法调用创建对应的一个栈帧
栈帧有这个方法的:局部变量表、操作数栈、动态链接、方法出口等信息。
例子:
public class ReplicaManager {
public void loadReplicasFromDisk() {
Boolean hasFinishedLoad = false;
if(isLocalDataCorrupt()) {
}
}
private Boolean isLocalDataCorrupt() {
Boolean isCorrupt = false;
return isCorrupt;
}
}
整个过程如图所示:
结合前面的知识,如图所示:
1.4 Java堆内存
存放我们在代码中创建的各种对象,实例变量也是在堆内存的。
案例:
public class Kafka {
public static void main(String[] args) {
ReplicaManager replicaManager = new ReplicaManager();
replicaManager.loadReplicasFromDisk();
}
}
public class ReplicaManager {
private long replicaCount;
public void loadReplicasFromDisk() {
Boolean hasFinishedLoad = false;
if(isLocalDataCorrupt()) {
}
}
private Boolean isLocalDataCorrupt() {
Boolean