JVM类加载机制
当java的源代码当编译之后,将java文件编译为class文件(字节码文件),然后虚拟机会将字节码文件加载到内存中,并对字节码文件进行校验(字节码文件是否正确、当前的虚拟机是否可以加载该文件),并转换解析,将字节码文件中的内容加载到内存,以便后续进行处理和翻译为机器码,这就是类加载机制
类加载的时机
JVM加载字节码文件的时机是什么,什么时候开始加载是由固定的条件来进行触发
-
当使用
new、getstatic、putstatic、invokestatic这4条字节码指令的时候,如果当前操作的类没有被加载,那么就会对该类进行初始化-
new指令:即使用new实例化对象 -
getstatic指令:读取类的静态字段 -
putstatic指令:设置类的静态字段 -
invokestatic指令:调用类的静态方法
使用
getstatic、putstatic时,如果该静态字段是final所修饰,那么不会触发初始化类,因为final所修饰的字段在编译期已经放入常量池中 -
-
对类进行反射的时候,如果该类没有进行过初始化,那么会对该类进行初始化
-
当子类进行初始化的时候,如果父类还没有初始化,那么会先加载其父类
-
当虚拟机启动的时候,需要指定一个执行的启动类,虚拟机会先加载指定的主类
类加载过程
类加载的过程可分为:加载、验证、准备、解析、初始化这5个阶段,每个阶段在加载的过程中承担不同的工作,当初始化之后,该类就已经加载完成
加载
在加载的过程中,虚拟机会做3件事情
- 通过一个类的全限定名来获取此类的二进制字节流
- 将类的静态内容等存储到方法区的运行时常量池中
- 在内存中生成一个
java.lang.Class对象,作为字节码文件在内存中的表示,并且作为方法区中这个类的各种数据的访问入口
验证
验证阶段是为了确保Class文件的正确性,保证当前的虚拟机可以加载和解析该字节码文件,并且该文件并不会破坏虚拟机,验证的过程主要分为:文件格式验证、元数据验证、字节码验证、符号引用验证
-
文件格式验证
- 主要验证字节码文件是否以魔数(cafe babe)开头
- 当前字节码文件的版本是否在虚拟机可以加载的版本中
- Class文件本身是否有附加的其他信息或者文件内容是否完整
-
元数据验证
主要是验证当前的字节码的信息是否符合Java的规范
- 当前的类是否有父类(所有类都有父类Object,除了Object自己)
- 是否继承了不允许继承的类(final修饰的类)
- 是否实现了父类中必须重写的方法
-
字节码验证
主要是通过数据和控制流分析,确认程序语义是合法的,符合逻辑的。(例如保证数据类型的赋值四正确的,不会出现数据类型的转换异常)
-
符号引用验证
该阶段主要是符号引用转换为直接引用的时候,判断是否转换之后的直接引用是否可以正常找到引用的对象,是否可以正常使用类中的属性。
这一验证的阶段会在解析阶段执行
准备
准备阶段是为静态变量分配内存空间,并给静态变量设置变量默认值的阶段(注意是默认值,在准备阶段不会进行计算赋值,而是设置变量的默认值,例如int类型的默认值为0)
对静态变量的赋值,需要等到初始化阶段才会执行,因为对静态变量赋值的操作都被收集到**类构造器**中,只有当类初始化的时候才会调用类构造器,才会执行对静态变量的赋值操作
在此阶段,只会给static修饰的变量(即类变量)分配内存空间,不包含实例变量,实例变量是属于对象,所以会在对象初始化的时候和对象一起在Java堆中分配内存空间
解析
解析阶段主要是将常量池内的符号引用转换为直接引用,即将符号引用转换为引用对象的真实内存地址
- 符号引用:通过符号来描述所引用的目标,符号引用可以是任何形式的标识,在使用的时候可以定位到目标接口
- 直接引用:直接引用可以是直接指向目标的指针、或者句柄。相当于可以定位到目标对象的内存地址,如果有了目标引用,相当于目标已经在内存中分配了内存地址
所以验证阶段的符号引用验证会在解析阶段将符号引用替换为直接引用之后,进行验证是否可以正确的指向目标引用
初始化
初始化阶段是类加载过程的最后一阶段,在此阶段会执行定义在类中的Java代码。可以理解为初始化阶段是执行类构造器<clinit>()的阶段。(注意这里是类加载过程,所以初始化指的是类的初始化,并不是对象的初始化)
-
类构造器<clinit>()是如何定义类构造器<clinit>()并不是通过Java代码定义,而是编译器在编译的时候通过收集类中所有静态变量的赋值操作和静态语句代码块(static{})所产生。在类初始化阶段会有虚拟机执行 -
<clinit>()和实例构造器<init>()方法<clinit>()构造器并不是通过Java代码定义并且在调用的时候,不会显示的调用父类的类构造器,因为虚拟机会保证父类在子类之前进行了类的初始化动态,而实例构造器<init>()需要显式调用父类的实例构造方法 -
因为父类的类构造器优先子类的类构造器执行,所以父类的静态代码块也优先于子类的执行
-
如果类中没有静态代码块、静态变量等,那么编译器不会生成类构造器
在准备阶段只会对静态变量设置类型默认值,只有在初始化阶段才会进行真正的赋值操作,因为静态变量的赋值操作需要在执行**构造器()**才会执行,而类构造器在类加载的初始化阶段执行
双亲委派模型
-
类加载器
在JVM中有2种不同的类加载,启动类加载器、其他类加载器
但是JVM中除了启动类加载器,还实现了2种类加载器,可以帮助进行类加载
-
启动类加载器
这个类加载器负责加载
<JAVA_HOME>/lib目录中的所有jar -
扩展类加载器
这个类加载器负责加载
<JAVA_HOME>/lib/ext中的扩展类库 -
应用类加载器
这个类加载器负责加载加载用户的应用程序
-
自定义类加载器
如果用户自定义了类加载器,可以通过
loadclass方法执行加载的类,那么该类的加载就会通过自定义类加载器进行加载,当然对于系统的内置的类会遵从双亲委派模型,通过启动类加载器进行加载
-
-
双亲委派模型的流程
如果一个类加载器收到了类加载请求,首先不会自己加载这个类,而是将类加载的请求委派给父类加载器进行加载,每一个层次的类加载器都是如此,直到请求到达启动类加载器,如果启动类加载器可以加载,那么就会直接加载,如果启动类加载器无法加载该类,那么就会将类加载请求交给子类加载器
-
为什么使用双亲委派模型
因为使用双亲委派模型的好处是保证内存中所有类都只有一份,不会出现多份相同的类,保证类的安全性,并且保证系统类(即Java内存的类,例如String、Interger等)优先加载
彩蛋
为什么反射影响性能
因为在反射的时候会进行类的加载,反射的时候如果该类灭有加载,那么会先加载该类,需要经过类加载的过程
如何破坏双亲委派模型
双亲委派模型是JVM保证内存中类的安全性和保证系统类的优先加载,可以通过自定义类加载器(继承ClassLoader)并重写findClass和loadClass方法即可加载自定义的类
为什么父类的静态代码块优先子类的静态代码块执行
因为在类加载的过程中,JVM会保证父类的优先加载(类加载的过程执行之前,JVM会检测父类是否加载),会优先加载父类,并且在类加载过程中的初始化过程中会执行类构造器<clinit>(),静态代码块在编译阶段由编译器收集到类构造器中,在类加载的初始化阶段执行,所以父类的静态代码块优先子类的执行。