类加载过程
简介
在Java中,类加载是一个重要的概念,它是Java虚拟机将类的字节码文件加载到内存并执行的过程。了解Java类加载的过程对于理解Java程序的运行机制以及解决一些常见的类加载问题非常有帮助。本文将深入探讨Java类加载的过程。
类的生命周期
类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。
类加载过程
Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?主要分为以下几个过程。
加载
加载是类加载过程的第一个阶段。在加载阶段,其主要完成以下三件事:
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口
Java 虚拟机并没有详细规范以上三点,所以对于一个类的二进制字节流来说,我们可以从任何地方获取,比如zip,网络等等。
什么是类的二进制字节流?
类的二进制字节流是指表示该类的字节码数据的一种数据流。在Java中,每个类都对应着一个Class对象,而Class对象包含了该类的所有信息,包括类名、方法、字段等。类的二进制字节流就是将这些信息以二进制形式进行编码后的数据。
加载类的过程主要是由类加载器来完成的,在Java中类加载器有很多种,当我们想要加载一个类的时候,具体使用什么类加载器是由双亲委派模型决定的,也就是说每个类使用的类加载器并不统一。
Java中都有哪些类加载器,如何使用这些类加载器进行加载,这些下次详细说明。
验证
我们都知道连接阶段又分为三个小部分,分别是验证、准备、解析。首先来看验证阶段,验证是连接阶段的第一步,其主要目的是确保Class文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证阶段主要验证以下几部分:
- 文件格式验证,验证字节流是否符合class文件格式的规范。
- 元数据验证,对于字节码描述的信息进行语义分析,确保其符合规范。
- 字节码验证,通过数据流和控制流分析,确定程序语义是否合法,是否符合逻辑。
- 符号引用验证,验证类的正确性。
文件格式验证这一阶段是基于该类的二进制字节流进行的,主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。除了这一阶段之外,其余三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。
验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 -Xverify:none
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
- 这个时候分配的变量都是类的变量,也就是一些静态变量。不包括实例变量,实例变量都是分配在堆中的。
- 理论上来说,类变量所使用的内存都应在方法区中,在
jdk7
之前,这种说法是没错的,但是在jdk7
之后HotSpot
已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。 - 这里设置的初始通常情况下都是默认值,除非静态变量加上了
final
关键字修饰,才会被赋值。
解析
**解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。**解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
什么是符号引用?
符号引用是一种编译时的引用,它以符号的形式来表示目标对象。在Java源代码中,使用类名、方法名、字段名等符号来引用其他类、方法或字段。符号引用是与具体内存地址无关的,它在编译阶段就已经确定了,包含了对目标对象的描述信息。也就是说符号引用其实就是一个符号表示。
其实就是说每个类都会有class文件,但是在编译期,一个类并不知道其引用的其他类的实际地址,因为还没有加载到内存,所以使用符号来代替,在加载的过程中再将符号转换为直接引用地址。
什么是直接引用?
直接引用是指向具体内存地址的引用,它可以直接定位到目标对象在内存中的位置。在类加载过程中,虚拟机会将符号引用转化为直接引用,使得程序能够准确地访问目标对象。直接引用是与具体内存地址相关的,它是在程序运行时动态生成的。其实就是类的元信息位于内存的地址串。
初始化
初始化阶段是执行初始化方法 <clinit> ()
方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
<clinit> ()
方法的作用是什么?
还记得么?在准备阶段,已经对类中static修饰的变量赋予了初始值。() 方法的作用,就是给这些变量赋予程序员实际定义的“值”。同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。
对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
-
当遇到
new
、getstatic
、putstatic
或invokestatic
这 4 条字节码指令时,比如new
一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。 -
使用
java.lang.reflect
包的方法对类进行反射调用时如Class.forname("...")
,newInstance()
等等。如果类没初始化,需要触发其初始化。 -
初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
-
当虚拟机启动时,用户需要定义一个要执行的主类 (包含
main
方法的那个类),虚拟机会先初始化这个类。 -
MethodHandle
和VarHandle
可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用findStaticVarHandle
来初始化要调用的类。 -
当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
类卸载
卸载类即该类的 Class 对象被 GC
卸载类需要满足 3 个要求:
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
参考:
我的个人博客,欢迎交流!