Java类加载机制

1 概述

虚拟机将class文件加载进内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程,为Java类加载机制。Java加载机制的生命周期:加载 验证 准备 解析 初始化 使用 卸载 7个阶段。

在Java语言中,类的加载,连接和初始化都是在程序运行期间完成的。

2 初始化

有且仅有以下情况下,若类没有初始化过,必须初始化:

  1. 遇到new getstatic putstatic或invokestatic这4个字节码时,若没有初始化则必须初始化。
    其中,new 用于实例化对象;getstatic/putstatic 读取/设置一个非final的类静态字段(未加载进常量池的字段);invokestatic用于调用一个类的静态方法。

  2. 使用java.lang.reflect包的方法对类进行反射调用。

  3. 初始化一个类的时候,发现其父类未被初始化。则先初始化父类。对接口而言,初始化接口的时候并不会先初始化父类接口,而是到真正使用到父类接口的时候才会初始化父类接口

  4. 虚拟机启动的时候,会初始化main方法所在的类。

  5. JDK1.7以后版本,使用java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,且这些句柄所对应的类未被初始化时。

初始化部分,有以下需要注意的部分:

  1. 对于静态字段的调用,只有直接定义这个字段的类才会被初始化,因此通过子类调用父类的静态字段,只会初始化父类,不会初始化子类的。
  2. new 一个(T类型的)数组的时候,并不会触发T类型的初始化,而是触发 [T 类型的初始化。这个类型是由虚拟机生成的,直接继承java.lang.Object的子类,创建动作由字节码newarray触发。此类封装了数组访问方法
  3. 类A访问类B中定义的常量的时候,也不会造成B类的初始化。因为在编译阶段通过常量传播优化,已经将B类中的常量的值,存储到了A类的常量池中。也就是说这两个类在编译成class文件后已经没有关系了。

接口特殊部分

  1. 接口也有初始化过程,和类的初始化类似。也会生成方法。
  2. 接口初始化和类唯一不同出,见类初始化第三条:对接口而言,初始化接口的时候并不会先初始化父类接口,而是到真正使用到父类接口的时候才会初始化父类接口

3 类加载

类加载部分分为:加载、连接(验证、准备、解析)、初始化五部分。

3.1 加载

在加载阶段,虚拟机需要完成以下事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区的这个类的各个数据的访问入口。

注:数组类并不由类加载器加载,而是直接由虚拟机创建的。数组类的创建遵循以下规则:

  1. 如果数组的组件类型(即数组去掉一个维度的类型)是引用类型,将递归采用加载过程去加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识,以达到一个类必须与类加载器一起确定唯一性。
  2. 若数组的组件类型不是引用类型,Java虚拟机将会把数组标记为与引导类加载器关联。
  3. 数组类的可见性与它的组件类型的可见性一致。若组件类型不是引用类型,则其可见性默认是public

加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,然后在内存中初始化一个java.lang.Class类的对象(对于HotSpot虚拟机,这个对象存储在方法区中),这个对象作为程序访问方法区这些类型数据的外部接口。加载阶段和连接阶段可能是交叉进行的。

3.2 验证

验证是连接阶段的第一步。用于保证Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害到虚拟机自身的安全。验证分4个阶段:

  1. 文件格式验证,主要目的是保证输入的字节流能正确地解析并存储在方法区中,格式符合描述一个Java类型信息的要求。验证内容是:字节流是否符合class文件格式的规范,且能被当前虚拟机处理等。比如

    是否以魔数0xCAFEBABE开头

    主次版本号是否在当前虚拟机处理范围之类

    常量池的常量中是否有不被支持的常量类型(检查常量tag标志)

    指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量

    CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据

    Class文件中各种部分以及文件本身是否有被删除的或者附加的其他信息

    …………

  2. 元数据验证:语义分析,保证其描述的信息符合Java语言规范的要求:

    这个类是否有父类,除了Java.lang.Object,所有类都有父类。

    这个类的父类是否继承了final修饰的类等不被允许继承的类。

    若这个类不是抽象类,他是否实现了父类或接口中要求实现的所有方法。

    类中的字段方法是否与父类产生矛盾:覆盖了父类的final字段/不合规的重载等。

    ……

  3. 字节码验证

    主要目的是通过数据流和控制流分析,确定程序的语义是否合法、符合逻辑。这里主要验证的是方法体。Java6以后,通过StackMapTable记录了方法体中所有基本块(按照控制流程拆分的代码块)开始时本地变量表和操作数栈应有的状态。故只需要检查这里的记录的合法性即可(此部分可参考Java虚拟机之类文件结构

    保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。

    保证跳转指令不会跳转到方法体以外的字节码指令上

    保证方法体中的类型转换是有效的。

    ……

  4. 符号引用验证

    此验证发生在虚拟机将符号引用转化为直接引用的时候。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。符号引用验证的目标是保证解析动作能正常的执行。失败会抛出Java.lang.IncompatibleClassChangeError的子类(比如Java.lang.IllegalAccessError/java.lang.NoSuchFieldError/java.lang.NoSuchMethodError等)

    符号引用中通过字符串描述的全限定名能否找到对应的类

    在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

    符号引用中的类、字段、方法访问限定符是否可以被当前类访问

    ……

3.3 准备

准备阶段是正式为类变量分配内存设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配

注意:

  1. 这时候进行内存分配的仅包括类变量(static修饰的变量),不包括实例变量;实例变量将在类实例化的时候随对象一起分配在Java堆中。
  2. 初始值是数据类型的0值,比如int的0;long的0l;short的(short)0;char的\u0000;byte的(byte)0;boolean的false;double的0.0d;reference的null等。
  3. 若类字段属性表中存在ConstantValue属性,且类字段包含ACC_FINAL属性。(比如属性被final static修饰),则该属性会在此时被初始化为ContentValue中的值。其他情况则是在中初始化实际值。
    比如:public static final int a=10;在这里a将被设置为10,而不是0;
    public int b = 10 在这里被初始化为0, 在<clinit>中才被设置为10

3.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

首先分析下这里两个概念:

  1. 符号引用:以一组符号来描述所引用的目标。符号引用可以是任何类型的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定以及加载到内存中。具体查询Java虚拟机之类文件结构

  2. 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。直接引用与虚拟机实现的内存布局是相关的。若存在直接引用,则其目标一定存在内存中。

虚拟机要求在执行了 anewarray,checkast,getfield,getstatic,instanceof,
invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,
ldc,ldc_w,multianewarray, new, putfield, putstatic这十六个用于操作数符号引用的字节码指令之前,先对他们所使用的符号引用进行解析。

对于invokedynamic,其由于是专门设计用来支持动态语言特性的,故其解析必须发生在程序实际运行到这条指令的时候。

解析动作主要是针对 类和接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类符号引用进行。对应了常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info, CONSTANT_InterfaceMethodref_info, CONSTANT_MethodType_info, CONSTANT_MethodHandle_info, CONSTANT_InvokeDynamic_info的7种常量类型。

以下详细分析其解析过程:

3.4.1. 类和接口的解析

虚拟机在当前代码所在类D,将一个未解析过的符号引用N,解析为类或接口C 的步骤如下:

  1. 若C不是一个数组类型,则虚拟机会将N的全限定名传递给D的类加载器去加载C这个类,具体加载和验证过程前面已经分析。
  2. 若C是一个数组类型,且数组的元素的类型为对象,例如N的描述符为[Ljava/lang/Integer,则会按照1中规则加载数组元素类型;接着由虚拟机生成一个代表此数组维度和元素的数组对象。
  3. 此时C已经在虚拟机中成为一个有效的类或者接口了,将进行符号引用验证,确定D具备对C的访问权限,若不具备访问权限则抛出java.lang.IllegalAccessError
3.4.2. 字段的解析

字段解析首先会对字段表内class_index项索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。虚拟机将会按规范进行此类C的后续字段搜索:

  1. 若C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  2. 否则,若C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,若接口中包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用,查找结束。
  3. 否则,如果C不是java.lang.Object,则会按照继承关系从下往上递归搜索其父类,如果在父类包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用,查找结束。
  4. 否则,抛出java.lang.NoSuchFieldError

若查找过程成功返回了引用,将会对这个字段进行权限验证,若不具备访问权限则抛出java.lang.IllegalAccessError

3.4.3. 类方法的解析

类方法的解析首先会对类方法表内class_index项索引的CONSTANT_Class_info符号引用进行解析,也就是方法所属的类或接口的符号引用。虚拟机将会按规范进行此类C的后续类方法搜索:

  1. 类方法和接口方法符号引用的常量类型定义是分开的,若在类方法表中发现class_index中索引的C是个接口,则直接抛出java.lang.IncompatibleClassChangeError.
  2. 若第一步成功,则在类C中查找是否有简单名称和描述符都与目标相匹配的方法,有就返回这个方法的引用,查找结束
  3. 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  4. 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,存在则说明类C是一个抽象类,此时查找结束,抛出java.lang.AbstractMethodError
  5. 否则,宣告查找方法失败,抛出java.lang.IllegalAccessError

3.4.4. 接口方法的解析

接口方法的解析首先会对接口方法表内class_index项索引的CONSTANT_Class_info符号引用进行解析,也就是方法所属的类或接口的符号引用。虚拟机将会按规范进行此接口C的后续接口方法搜索:

  1. 若在接口方法表中发现class_index中索引的C是个类而不是接口,则直接抛出java.lang.IncompatibleClassChangeError.(这里与类方法解析正好相反)
  2. 若第一步成功,则在接口C中查找是否有简单名称和描述符都与目标匹配的方法,若有则返回此方法的直接引用,查找结束
  3. 否则,在接口C的父接口中递归查找,直到Java.lang.Object(包含)为止,查找是否有简单名称和描述符都与目标匹配的方法,若有则返回此方法的直接引用,查找结束。
  4. 否则,查找失败。抛出java.lang.NoSuchMethodError
  5. 接口中方法都是public的,所以不存在权限问题,故这里不需要权限验证。

3.4.5 MethodType解析

3.4.6 MethodHandle解析

3.4.7 InvokeDynamic解析

3.5 初始化

类初始化是类加载过程的最后一步。除了加载阶段可以使用用户自定义加载器加载外,其余阶段都是虚拟机主导的。到了初始化阶段才是真正执行Java代码:执行方法,完成类初始化。

使用注意:

  1. 是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,其中编译器收集的顺序是基于源码中顺序的。静态语句块中只能访问到定义在其前面的变量,定义在其后面的静态变量可以在静态语句块中赋值,但不能访问。

    public class TestClinit {
    static {
        i = 10; //可以赋值,不报错
        System.out.println(i);
        //不能访问 Cannot reference a field before it is defined
    }
    static int i;
    }
  2. 方法与类实例的构造函数方法不同,不需要显式的调用父类的构造器,虚拟机会保证在子类的执行前,父类的方法已经执行完毕。因此虚拟机中第一个被执行的肯定是java.lang.Object的.

  3. 由于父类的先执行,故父类的静态语句块要由于子类的语句块先执行。例如,以下的例子中最终输出的将使1.(这里若是吧静态块放在变量A定义的后面,则会输出2,说明静态区域的执行顺序只和源码顺序有关系。)

     static class Parent{
        static{
            A = 2;
        }
        public static int A =1;
    }
    
    static class Sub extends Parent{
        public static int B = A;
    }
    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
  4. 方法对于类和接口并不是必须的,若类中无静态块和静态变量赋值操作,则编译器可以不生成方法。

  5. 接口中存在变量初始化赋值操作,故也需要方法,接口和类的区别是:执行接口的方法并不要求先执行父接口的,只有当父接口中定义的变量使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的方法
  6. 虚拟机会保证一个类的方法在多线程环境中被正确的加锁和同步,若多个线程同时去初始化一个类时,只有一个线程能去执行初始化方法,其他线程阻塞等待,直到该执行线程结束。唤醒其他线程,此时,其他线程不会再次进入这个类的方法,因为同一个类加载器下,一个类只会被初始化一次

4 类加载器

类加载器的作用是通过一个类的全限定名来获取描述此类的二进制字节流。

对于任何一个类,都需要由加载他的类加载器和这个类本身一同确定其在虚拟机中的唯一性,每个类加载器都有一个独立的类名称空间。也就是说比较两个类”相等”是基于同一个加载器的情况下才有意义。否则一定不等。

相等是指类的Class对象的equal方法/isAssignableFrom方法/isInstance方法/以及instanceof关系判定等。

4.1 类加载器分类

对于Java虚拟机而言,只存在两类加载器:启动类加载器(Bootstrap Classloader),是虚拟机的一部分;另一个就是其他加载器,这些加载器继承了抽象类java.lang.ClassLoader

对于Java开发而言,有三种类型的加载器:

  1. 启动类加载器(Bootstrap ClassLoader),这个类加载器负责将存放在$JAVA_HOME/lib目录中或被-Xbootclasspath参数指定路径中的,虚拟机识别的(按照文件名)的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,若需要吧加载请求委派给引导类加载器,直接使用null代替即可。
  2. 扩展类加载器(Extension ClassLoader),这个加载器是由sun.misc.Launcher$ExtClassLoader实现,它负责加载$JAVA_HOME/lib/ext目录下中的,或者被java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用该加载器。
  3. 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现,这个类加载器是ClassLoader类的getSystemClassLoader()方法的返回值,故也称为系统类加载器。它负责加载用户路径(ClassPath)上所指定的类库。程序默认的类加载器,开发人员可以直接使用。

4.2 双亲委派模型

若一个类加载器收到了类加载的请求,它首先不会尝试自己去加载此类,而是把请求委派给父类(不是继承关系,只是逻辑父类)去加载。如此迭代,最终所有的加载请求都会传达到顶层的启动类加载器进行加载。以避免出现诸如多个类加载都有自己的java.lang.Object类的情况造成的混乱。

4.3 线程上下文类加载器

这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法设置一个classloader进来,在调用此方法设置自己的类加载器之前,会使用父线程中继承的类加载器。其和双亲委派模型相反,实现了父类调用子类加载器。

4.4 OSGI类加载器

为了实现热替换技术,退出的加载器,这里已经不是双亲委派模型,而是网状结构,当收到类加载请求时,OSGI按照以下顺序进行搜索:

  1. 若是以java.*开头的类,则委派给父类加载器加载。
  2. 否则,若是委派列表名单内的类,则委派给父类加载器加载。
  3. 否则,若是Import列表中的类,则委派给Export这个类的Bundle的类加载去加载。
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
  5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
  6. 否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle的类加载器加载。
  7. 否则,查找失败。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值