02_JVM 类加载机制
2564字约9分钟
2024-08-10
JVM 在什么情况下会加载一个类?
类加载过程是非常繁琐复杂的,从实用的角度来说,我们主要把握他的核心工作原理就可以了
一般一个类从加载到使用会经历的过程:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
那什么时候 JVM
就会加载一个类呢?答案就是当你的代码中用到这个类的时候
举一个例子,你有一个类(Order.class
),里面有一个 main()
方法作为入口,当 JVM
进程启动之后,他一定会先把这个类(Order.class
)加载到内存中,然后从 main()
方法的入口代码开始执行
public class Order {
public static void main(String[] args) {
}
}
假设上面的代码中,出现如下这么一行代码
public class Order {
public static void main(String[] args) {
MqManager mqManager = new MqManager();
}
}
代码中很明显需要使用 MqManager
这个类去实例化一个对象,所以这个时候就会触发 JVM
通过类加载器,从 MqManager.class
字节码文件中加载对应类到内存中使用,这样代码才能跑起来,如下图
简单总结一下:首先代码中包含 mian()
方法的主类一定会在 JVM
进程启动之后被加载到内存,开始执行你的 main()
方法中的代码,接着遇到了使用别的类,此时就会从对应的 .class
字节码文件加载对应的类到内存中来
认识一下验证、准备和解析的过程
对于这三个概念,没太大的必要去深究里面细节,这些细节很多很繁琐,我们脑子里面有这几个概念就可以了
验证阶段
根据 Java
虚拟机规范,校验加载进来的 .class
文件中的内容,是否符合指定的规范
如果 .class
文件中的字节码不符合规范,那么 JVM
是没法去执行这个字节码的,过程如下图
准备阶段
我们写好的类,可能都有一些类变量,比如 MqManager
这个类
public class MqManager {
public static int maxConsumerNumber;
}
MqManager.class
文件内容被加载到内存之后,会对字节码文件内容进行规范验证,接着就会进行准备工作,这个准备工作就是给 MqManager
类分配一定的内存空间
给类中的类变量(static
修饰的变量)分配内存空间,再赋值一个默认的初始值,比如这里给 maxConsumerNumber
变量一个初始值 0
,过程如下图
解析阶段
解析阶段实际上就是把符号引用替换为直接引用的过程,这个部分的内容很复杂,涉及到 JVM
的底层,这里不做详细解读,知道有这么一个阶段就可以了,如下图所示
三个阶段的小结
在这三个阶段里,最核心的就是 准备阶段
在这个阶段给加载进来的类分配好了内存空间,类变量也分配好了内存空间,并且给了默认的初始值
核心阶段:初始化
初始化
在准备阶段,就会给 MqManager
类分配好内存空间,包括他的类变量 maxConsumerNumber
也会给一个默认值 0
,那么接下来的初始化阶段,就会正式执行我们的类初始化代码了,我们来看看下面这段代码
public class MqManager {
public static int maxConsumerNumber = Configuration.getInt("mq.maxConsumerNumber");
}
可以看到 maxConsumerNumber
这个类变量,是打算通过 Configuration.getInt("mq.maxConsumerNumber")
这段代码获取一个配置项给他赋值,在准备阶段,仅仅是给 maxConsumerNumber
类变量开辟了一个内存空间,然后给了个初始值 0
Configuration.getInt("mq.maxConsumerNumber")
这段代码的执行则会在 初始化
阶段,完成一个配置项的读取,然后赋值给 maxConsumerNumber
这个类变量
另外像下面的 static
静态代码块,也会在初始化阶段执行
public class MqManager {
public static int maxConsumerNumber = Configuration.getInt("mq.maxConsumerNumber");
static {
loadMessageFromDisk();
}
public static void loadMessageFromDisk() {
Map<String, Object> messageMap = new HashMap<>();
}
}
什么时候会初始化一个类
一般来说有以下时机:
1、包含
main()
方法的主类,必须是立马初始化的2、
new MqManager()
实例化对象的时候,此时会触发类的初始化,把这个类准备好,再实例化对象3、初始化一个类的时候,发现他的父类还没初始化,必须先初始化他的父类
类加载器与双亲委派机制
类加载器
加载 -> 验证 -> 准备 -> 解析 -> 初始化
这些过程是依赖类加载器实现的,简单来说有下面几种
启动类加载器
Bootstrap ClassLoader
,主要负责加载我们安装的 Java
目录下的核心类,在 Java
安装目录下有一个 lib
目录,这里面是 Java
最核心的一些类库,支撑着 Java
系统的运行
一旦启动 JVM
,那么首先就会依托启动类加载器,去加载 Java
安装目录下 lib
目录中的核心类库
扩展类加载器
Extension ClassLoader
,主要负责加载我们安装 Java
目录下 lib/ext
目录,这里面的一些类就需要使用这个类加载器来加载,支撑系统的运行
一旦启动 JVM
,就会从 Java
安装目录下,加载 lib/ext
目录中的类
应用程序类加载器
Application ClassLoader
,主要负责去加载 ClassPath
环境变量所指定路径中的类
可以大概理解为是去加载你写的 Java
代码,负责将你写好的那些类加载到内存里
自定义类加载器
除了前面几种以外,我们还可以自定义类加载器,去根据自己的需要加载类
双亲委派机制
JVM
的类加载器是有亲子层级结构的,启动类加载器是最上层,扩展类加载器在第二层,应用程序类加载器在第三层,最后一层是自定义类加载器,如下图
基于这个亲子层级结构,就有一个双亲委派的机制
假设你的应用程序类加载器需要加载一个类,他首先会委派自己的父类加载器去加载,最终传导至顶层的类加载器去加载,如果父类加载器在自己负责加载的范围内找不到这个类,那么就会把加载权交给自己的子类加载器,也就是说,父级能加载则加载,加载不了的会让子类去加载,子类还加载不了,就会让子类的子类去加载
这就是所谓的双亲委派模型:先找父类去加载,不行的话再由儿子来加载。这样的话,可以避免多层级的加载器结构重复加载某些类,最后我们再来一张总的图
思考环节
1、我们用 Java
开发的 Web
系统一般采用 Tomcat
之类的 Web
容器部署的,Tomcat
本身就是用 Java
写的,他自己就是一个 JVM
,那么 Tomcat
的类加载机制应该怎么设计,才能把我们动态部署进去的 war
包中的类,加载到 Tomcat
自身运行的 JVM
中,然后去执行我们写好的代码呢?
首先 Tomcat
自定义了很多类加载器的,如下图所示
Tomcat
自定义了 Common
、Catalina
、Shared
等类加载器,其实就是用来加载 Tomcat
自己的一些核心类库
然后 Tomcat
为每个部署在里面的 Web
应用都有一个对应的 WebApp
类加载器,负责加载我们部署的这个 Web
应用的类,至于 JSP
类加载器,则是给每个 JSP
都准备了一个 JSP
类加载器
一定要记得的是,Tomcat
是打破了双亲委派机制的,每个 WebApp
负责加载自己对应的 Web
应用的 class
文件,不会传导给上层类加载器去加载,感兴趣的话可以另行了解 Tomcat
的类加载机制
2、父类子类加载、初始化是怎么样的呢?
加载父类就是父类,除非用到了子类才会加载子类;加载子类初始化之前,必须先加载父类,初始化父类
3、类只有用到的时候才加载到内存中,那么 new
对象的时候肯定会用到,是不是也要先经历类的所有过程才将类实例化?
没错,默认的类加载机制,就是在代码运行过程中,遇到什么类就加载什么类,必须先加载类,再实例化对象
4、类加载器机制为什么是一级一级往上找,直接从顶层类加载器开始找不就行了吗?
类加载器本身做的就是父子关系模型。 双亲委派模型在某些情况下是需要打破的,从顶层类加载器开始找就实现不了这个点了。
5、是在执行 new MqManager()
这行代码的时候加载 MqManager
类吗?还是说加载 Order
的时候就同时加载了呢?
执行 new MqManager()
的时候加载类
6、JVM
和平时运行在机器上的系统之间是什么关系呢?
运行在机器上的系统,其实就是一个 JVM
进程,JVM
进程会执行你系统里的代码
7、为什么 Tomcat 需要打破双亲委派模型?
为了解决 Web
应用程序的类加载冲突问题,并提供更好的灵活性和可扩展性