1、基本介绍:
- 类的加载指的是将类的.class文件【编译器将java文件编译成class文件】中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
- 类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
- JVM有预加载功能:类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
2、类加载流程:【类的生命周期】
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载(Unloading)七个阶段。其中验证、准备和解析三个部分统称为连接(Linking),这七个阶段的发生顺序如下图所示:
Tips:其中加载、验证、准备、初始化和卸载这5个顺序是确定的,而解析阶段不一定,它可以在初始化阶段之后才开始,这是为了支持java语言的运行时绑定。
连接:连接就是把二进制数据组装为可以运行的状态,分为校验,准备,解析这3个阶段。
2.1、加载:
- 通过ClassLoader的双亲委托机制查找并加载类的二进制数据。
- 在加载阶段,虚拟机需要完成以下三件事情:
- 通过一个类的权限名称来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
- 相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
- 加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
2.2、验证 :
- 确保被加载的类的二进制数据是否符合当前JVM虚拟机版本规范,而且不会危害虚拟机自身的安全。
- 分为四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
文件格式的验证:- 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
- 该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。
- 经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
- 例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:- 对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
- 例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:- 该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
符号引用验证:- 这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。保解析动作能正确执行。
2.3、准备 :
- 准备阶段是为类静态变量分配内存并设置类静态变量初始默认值的阶段,这些内存都将在方法区中进行分配。【如int分配4个字节并赋值为0,long分配8字节并赋值为0】
- 这时候进行内存分配的仅包括类静态变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
- 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。【显示赋予值是在初始化阶段进行】
- 假设一个类变量的定义为:public static int value = 3;
- 那么该静态变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法。
2.3.1、默认值:
- 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
- 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
- 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
- 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
2.4、解析 :
- 解析阶段主要是将类中的符号引用转化为直接引用的过程。【比如A类中的a方法引用了B类中的b方法,那么它会找到B类的b方法的内存地址,将符号引用替换为直接引用(内存地址)】
- 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
2.4.1、符号引用(Symbolic Reference):
- 符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量。
- 符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。
2.4.2、 直接引用(Direct Reference):
- 直接引用指的是这些符号引用加载到虚拟机中以后 的内存地址。
- 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
- 直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
2.5、初始化:
- 类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了加载阶段用户可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
- 这是类加载的最后阶段,这里所有的静态变量会被赋初始值, 静态代码块将被执行。
- 初始化阶段开始执行类构造器
<clinit>()方法,该构造器方法会进行所有类变量的赋值动作、执行static代码块。 - 在 Java 类中,如果有静态 static 代码块、静态 static 变量的话,编译器会为这个类自动生成一个类构造器方法
<clinit>()(注意,不是实例构造器),在 类构造器中会执行静态 static 代码块,初始化静态 static 变量,类构造器 就是在类的 初始化 阶段执行的。
2.5.1、类构造器方法<clinit>():
- 类构造器()方法与实例构造器()方法不同,不需要显式的调用父类的构造器,虚拟机会保证父类构造器
<clinit>()先执行,因此在虚拟机中第一个执行的<clinit>()方法的类一定是java.lang.Object。 - 由于父类的
<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。 - 类构造器()方法对于类或者接口不是必须的,如果一个类既没有静态变量赋值操作,也没有静态语句块,则不会生成该类构造器()方法。
- 接口中可能会有静态变量赋值操作,因此接口也会生成类构造器
<clinit>()方法。但是接口与类不同,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的静态变量被使用时,父接口类构造器<clinit>()方法才会被执行。 - 虚拟机会保证一个类的
<clinit>()方法在多线程环境下能够被正确的加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行该类的<clinit>()方法 ,其他线程会被阻塞。 - 在
<clinit>()中执行的操作:-
父类静态变量初始化
-
父类静态语句块
-
子类静态变量初始化
-
子类静态语句块
-
2.5.2、实例构造器方法<init>():
<init>()实例构造器方法的执行时期: 实例对象的初始化阶段,<clinit>一定比<init>先执行。- 实例化一个类的四种途径:
- 调用 new 操作符。
- 调用 Class 或 java.lang.reflect.Constructor 对象的newInstance()方法。
- 调用任何现有对象的clone()方法。
- 通过 java.io.ObjectInputStream 类的 getObject() 方法反序列化。
- 在
<init>()中执行的操作:- 父类变量初始化
- 父类语句块
- 父类构造函数
- 子类变量初始化
- 子类语句块
- 子类构造函数
2.5.3、触发类初始化时机:
2.5.3.1、类被主动引用:【会触发类初始化过程】
- 创建对象的实例:我们new对象的时候,会引发类的初始化,前提是这个类没有被初始化。
- 调用类的静态属性或者为静态属性赋值。
- 调用类的静态方法。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的子类的时候,如果发现其父类还没有进行过初始化,则需要触发父类的初始化。
- 当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
2.5.3.2、类被被动引用:【不会触发类初始化过程】
- 通过子类引用父类的静态字段,不会导致子类初始化。只有直接定义该静态字段的类才会被初始化,因此当我们通过子类来引用父类中定义的静态字段时,只会触发父类的初始化,而不会触发子类的初始化。
- 通过数组定义来引用类,不会触发此类的初始化。
- 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化过程。
2.6、卸载:
- 当没有任何引用指向Class对象时就会被卸载,结束类的生命周期。如果再次用到就再重新开始加载、连接和初始化的过程。
- 结束类加载的生命周期:【在如下几种情况下,Java虚拟机将结束生命周期】
- 执行了System.exit()方法。
- 程序正常执行结束。
- 程序在执行过程中遇到了异常或错误而异常终止。
了解更多,欢迎关注: