欢迎浏览高耳机的博客
希望我们彼此都有更好的收获
感谢三连支持!
🙉Java是面向对象编程,一切皆对象。这些对象是如何从一堆代码变成程序中的一部分?Java虚拟机(JVM)在这个过程中扮演了至关重要的角色。当你的代码通过编译器转换成.class文件,再到运行时的动态加载,JVM内部的类加载机制确保了这一切的顺利进行。
本文将带你探索Java类加载,从类的加载、连接到初始化,一步步解读JVM如何管理类的生命周期。
目录
类加载过程
🥝**《Java虚拟机规范》**是由Oracle发布的Java领域最重要和最权威的著作,完整且详细地描述了JVM的各个组成部分。
PS:本文以下部分,默认都是使用HotSpot,即Oracle Java默认的虚拟机为前提来进行介绍的。
🍇在Java虚拟机(JVM)中,类的加载过程是实现Java程序动态性的关键。整个类加载过程大致可以分为三个主要阶段:加载、连接和初始化。
加载
🍉在加载阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名 来获取定义此类的二进制字节流。(例如,如果有一个类MyClass位于包com.example.myapp中,那么这个类的全限定名就是com.example.myapp.MyClass。)
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
连接
🍊JVM类加载过程通常被描述为包含五个步骤,但实际上,这包含了"连接"阶段被细分的三个更具体的操作。因此,可以说JVM类加载实际上涉及三个详细步骤。
1) 验证
🍍验证是连接阶段的第一步,这一阶段的目的是确保**.class文件的字节流**中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证选项: • 文件格式验证 • 字节码验证 • 符号引用验证...
这就使我们需要确保编译时使用的JDK版本要和运行时使用的JDK版本一致,或者向前兼容。
2) 准备
🥥JVM在准备阶段为类变量(即静态变量)分配内存,并设置类变量初始值。
java
public static int value = 123;
在准备阶段,value的值会被初始化为0,而不是123。 这样做避免了内存中残留的数据被当前程序错误的使用,从而产生bug。
3) 解析
🍑将常量池内的符号引用替换为直接引用的过程,也就是将常量池中的引用转换为直接指向内存的引用。
java
Class Test {
String s = "Hello";
}
在这个时刻,字符串常量s已经存在于.class文件中。然而,这里并没有提到"地址"这个概念,因为在.class文件的上下文中,我们不直接使用内存地址。相反,我们使用**"偏移量"** 来指代字符串常量s在内存中的位置。
初始化
🍅初始化阶段,Java虚拟机真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程。
java
Class Test {
static int a = 123;
}
现在,**是真正执行将 123 赋值给 a 的过程。**执行了类中的静态代码块,并且这个操作也会触发对父类(即超类)的加载过程。
🫐回看这个过程,是否对类加载机制有了一个基本的了解?
双亲委派模型
双亲委派模型是JVM中类加载机制的核心概念之一。双亲实际上指代的是子类与父类,而不是广义上的概念。
模型概述
🍎当一个类加载器接收到加载类的请求时,它不会立即尝试加载这个类。相反,它会将请求传递给其父类加载器来处理。这个过程会一直向上传递,直到最顶层的父类启动类加载器。只有在父类加载器表示无法找到所需类的文件时,也就是说它自己的搜索范围内没有这个类,子加载器才会接手并尝试加载这个类。
这种机制被称为类加载的委派模型。简而言之,类加载器会逐级向上请求帮助,直到最顶层,如果上级无法处理,才会由下级自己来加载类。
类加载器层次
启动类加载器(Bootstrap ClassLoader):使用C++实现,是虚拟机自身的一部分,负责加载JAVA_HOME/jre/lib目录中的核心类库。
扩展类加载器(Extension ClassLoader):由Java语言实现,继承自ClassLoader类,负责加载JAVA_HOME/jre/lib/ext目录或者由系统属性java.ext.dirs指定位置中的类库。
应用程序类加载器(Application ClassLoader):也称为系统类加载器,由Java语言实现,继承自ClassLoader类,负责加载环境变量classpath或系统属性java.class.path指定路径下的类库。
自定义类加载器:根据需求定制的类加载器。
🥑当应用程序类加载器开始工作时,它会逐层向上请求其父加载器协助加载类。在这个过程中,**如果任何一个加载器成功地加载了所需的类,那么这个类就会被进一步处理。**如果所有上级加载器都未能找到并加载这个类,最终请求会返回到应用程序类加载器。
如果这时应用程序类加载器也无法找到对应的.class文件,就会报告一个错误,抛出ClassNotFoundException
异常。
简而言之,类加载是一个从应用程序类加载器开始的逐级向上请求的过程,如果最终无法加载类,就会报告找不到类的异常。
核心目的
防止类重复加载:当A类和B类共享一个共同的父类C时,如果在A类加载过程中C类已经被加载,那么在B类加载时就无需再次加载C类。这样可以避免同一个类被重复加载。
保障核心API的安全性:通过使用双亲委派模型,可以确保Java的核心API不被随意篡改。如果没有这种模型,每个类加载器都可能加载自己的版本,这可能导致系统中存在多个不同的Object类实例,其中一些可能是用户自定义的,这样就会威胁到系统的安全性。
双亲委派模型定义了Java标准库的最高优先级,扩展库其次,第三方库最低,确保了Java核心库的单一性和安全性。
破坏双亲委派模型
🥝尽管双亲委派模型有许多优点,但在某些场景下,比如JDBC的实现,它并不完全适用。
JDBC的Driver
接口定义在JDK中,而具体的实现则由各个数据库服务商提供。
例如MySQL的驱动包。当我们查看DriverManager
的源码时,会发现它实际上是位于系统的核心库rt.jar
中,由顶层的启动类加载器(Bootstrap ClassLoader)加载。然而,在DriverManager
的getConnection
方法中,它实际上是使用子类加载器,也就是线程上下文类加载器(Thread.currentThread().getContextClassLoader()
)来加载具体的数据库驱动实现,例如MySQL的Jar包。
🍇这种做法实际上打破了双亲委派模型的常规流程,因为按照双亲委派模型的原则,所有类的加载都应该由父类加载器来完成。但在JDBC的情况下,由于Driver
接口的实现类是由服务商提供的,需要通过子类加载器来加载,这就导致了双亲委派模型的规则被打破了。
🍉这种设计是为了确保Java核心API的安全性和灵活性,允许在运行时动态加载数据库驱动,而不是在JVM启动时静态确定。
🙉我们从类的加载、连接到初始化,逐步了解了JVM如何精心管理类的生命周期。双亲委派模型不仅确保了类的一致性和安全性,还为我们提供了一个有序、可预测的类加载环境。通过对双亲委派模型的深入理解,我们能够更好地把握Java程序的行为,优化性能,并确保程序的稳定性。
希望这篇博客能为你理解JVM类加载机制提供一些帮助
如有不足之处请多多指出
我是高耳机