《深入理解JVM》第七章 类加载器的时机 && 类加载器的过程

版权声明:版权为ZZQ所有 https://blog.csdn.net/qq_39148187/article/details/81840892

概述

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行效验,转换解析,连接初始化,都是在程序运行期间进行的,这种策略虽然会令类的加载提供一些性能开销,但是给java提供了很高的灵活性,java天生可以动态拓展的语言特性就是依赖运行时动态加载和动态链接的特点实现的,例如如果写一个面向接口的程序可以等到运行时再写具体的实现,用户可以通过java预定义的自定义类加载器让本地应用程序从网络或者磁盘读取一个二进制的流作为程序的一部分,这种组装应用程序目前广泛应用java程序中,最基础的Applet jsp 到复杂的OSGI,都是在java 运行时类加载完成的

1.一个Class 有可能是一个类,有可能是一个接口

2. java 运行时类加载机制加载的是一个二进制流,无论它是以什么形态都可以

类加载的时机

类从虚拟机中加载到内存中,到卸载出内存为止,他的整个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载,

加载,验证,准备,卸载,使用,初始化这几个阶段是按部就班的来的,类的加载过程必须按照整个这个过程,但是解析阶段不一定,他在某些情况可以在初始化阶段之后开始,这是为了支持java语言的运行时绑定,这里的解释说明了,这些阶段通常都是互相交杂的混合式的进行,通常会在一个阶段执行的过程中调用另外一个阶

什么情况下需要加载? jvm并没有规范对其进行约束,但是初始化阶段,(Initialization) 虚拟机规范了5中情况进行立即初始化,

   1) 遇到new getstatic putstatic invokestatic 这四条指令的时候,如果类没有完成初始化,则要先进行初始化,对常见的场景就是使用new关键字实例化对象的时候,读取或者设置一个静态字段的时候,以及调用这个类的静态方法的时候

   2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有被实例化,则需要先实例化

  3)当初始化一个类,发现他有继承的父类,要先对父类进行实例化操作

  4)当启动虚拟机的时候main在哪里他就会先实例化哪个类

  5) 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandler实例最后解析结果为REF_getStatic REF_putStatic REF_invokeStatic 的方法句柄,这个方法所对应的类没有被初始化,要先进行初始化,

除此之外所有的方法都不会触发初始化,成为被动引用,

package com.jvm.ClassLoaderDemo;

/**
 * @author ZZQ
 * @date 2018/8/20 18:36
 */
public class test {

    public static void main(String[] args) {
        System.out.println(SuperClass.value);
    }
}

class SubClass extends  SuperClass{


    static {
        System.out.println("SubClass init");
    }
}


 class SuperClass {

    static {
        System.out.println("SupperClass init");
    }

    public static  int value = 3 ;

}

静态不属于类,随类的加载加载,上面代码的输出结果为

SupperClass init
3

Process finished with exit code 0

只有定义静态字段的字段被调用时候类才被加载,在sun 的HotSpot 中,可以使用-XX:+TraceClassLoading 参数会导致加载子类

package com.jvm.ClassLoaderDemo;

/**
 * @author ZZQ
 * @date 2018/8/20 18:36
 */
public class test {

    public static void main(String[] args) {
//        System.out.println(SuperClass.value);
        SuperClass[] sca =new SuperClass[10];
    }
}

class SubClass extends  SuperClass{


    static {
        System.out.println("SubClass init");
    }
}


 class SuperClass {

    static {
        System.out.println("SupperClass init");
    }

    public static  int value = 3 ;

}

通过数组定义类的引用,不会对类产生初始化,但是这段代码触发了一个Lorg.fenixsoft.classloading.SuperClass的类的初始化阶段,对于用户来说这不是一个合法的类名,它是由jvm 自动生成的,直接继承Object 类,创建动作由字节码指令newarray触发

这个类代表一个元素类型为org.fenixsoft.classloading.SuperClass的一维数组,数组中应有属性和方法,(用户直接使用的有被public 修饰的public length clone())java中的数组比Cpp中的数组相对安全,因为这个类封装了数组的元素的访问方法,cpp直接翻译为指针的移动,

package com.jvm.ClassLoaderDemo;

/**
 * @author ZZQ
 * @date 2018/8/20 18:36
 */
public class test {

    public static void main(String[] args) {
//        System.out.println(SuperClass.value);
//        SuperClass[] sca =new SuperClass[10];
         System.out.println( SuperClass.HelloWorld);
    }
}

class SubClass extends  SuperClass{


    static {
        System.out.println("SubClass init");
    }
}


 class SuperClass {

    static {
        System.out.println("SupperClass init");
    }

    public static  final  String HelloWorld ="Hello world" ;
    public static  int value = 3 ;

}
常量在编译阶段进行了常量传播优化,会调入调用类的常量池中,本质上没有引用定义常量的类,因此不会触发类的初始化

接口的加载过程和类的不一样,针对接口需要做特殊的说明,接口也有初始化过程,这点和类是一致的,上述代码都是使用Static{} 来输出,接口中不能使用Static 但是编译器仍然会为接口生成<clinit>() 类构造器,用于初始化接口中的成员变量,

当类被加载的时候,要求类所继承的父类都要被加载完毕,但是接口不用,只有真正用到夫接口的时候才会被初始化

类的加载过程

加载

“加载”是类加载过程的一个阶段,希望读者没有混淆,这两个看起来相近的名词 ,在加载阶段,虚拟机需要完成三件事:

  1)通过类的全限定名来获取次类的二进制字节流

  2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构

   3) 在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

虚拟机规范的这三个规则都是不具体的,所以虚拟机的实现与具体应用的灵活性 就第一条而言

  1.类加载可以从 zip中加载 war  rar jar  

  2.可以从网络中加载常见的Applet 

  3. 运行时动态生,这种场景多用于动态代理技术,在java.lang.reflect.Proxy中就是使用ProxyGenerator.generateProxyClass 来为特定的接口生成形式为 $Proxy的代理类的二进制字节流

  4.由其他文件生成,最典型的就是jsp,由jsp生成的对应的Class 类

  5.从数据库中读取,这种场景很少见,例如有些中间件服务器,(SAP Netweaver) 可以选择把程序安装到数据库中完成代码在集群中的分发

    想对与类加载过程的其他阶段,,一个非数组的类的加载阶段,(准确的来说是加载阶段中的获取二进制字符流的动作)在这个阶段是开发可控性最强的阶段,因为加载阶段可以使用系统提供的引导类加载器来完成,也可以由用户定义的类加载器区完成,开发者可以自己定义类加载器去控制字节流的获取方式(重写一个类加载器的loadClass)

https://zhidao.baidu.com/question/1445521721033697340.html

https://www.cnblogs.com/gdpuzxs/p/7044963.html

    相对于数组而言情况有些不同数组本身不通过类加载器创建,它是由java 虚拟机直接创建,但是数组类和类加载器有密切的关系,因为数组类的元素数据类型(Element Type ,指的是数组去掉所有维度的类型)最终靠类加载器创建,一个数组类创建过程遵循一下规则:

   1)如果数组组件类型(数组去掉一个维度的类型)是引用类型,呢就递归采用本节中定义的加载过程去加载这个组建类型,数组C将在加载该类组件类型的类加载器的类名称空间上标识(一个类必须与类加载器一起确定类型)

  2)如果数组不是引用类型,jvm会将数组C标记为引导类加载器关联

  3)数组类的可见性和他的组件的可见性一致,如果数组类型不是引用类型,呢数组类的可见性默认为public

加载完成之后,虚拟机的外部二级制字节流就按照虚拟机所需的格式存储在方法区之中,方法区的存储数据格式是由虚拟机实现自定义的,虚拟机规范未规定此区域的具体数据结构,然后在内存中实例化一个java.lang.Class类的对象,(Class 对象比较特殊,相对于HotStop而言,Class 对象虽然是一个对象,但是存放在方法区)这个对象作为程序访问方法区中的这种数据类型的外部接口

加载阶段和链接阶段的部分内容是交叉进行的,加载可能尚未结束,链接阶段可能已经开始了,但这些夹在加载阶段中进行的动作,仍然属于链接阶段的内容,两个阶段仍然保持着固定的顺序,

验证

验证是链接的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

   java 语言本身是相对安全的语言(依然是对C++来说),使用纯粹的Java代码无法做到诸如 访问到数组边界外的数据,将一个对象转换为他的实现类型,跳转到不存在的代码之类的,这样做了编译器会拒绝编译, Class 文件不一定由java编译 出来,可以使用任意途径,甚至包括用一个十六进制的编辑器就行了(大佬才可以的),如果虚拟机不检查输入的字节流,对其完全信任的话,很可能因为载入有害的字节流导致系统崩溃,所以验证是保证虚拟机自身安全的重要工作,

   如果验证输入的字节流不符合Class 文件的约束和约束,就会抛出一个java.lang.VerifyErroryi异常或者他的子类,验证分为四个动作 

文件格式验证,元数据验证,字节码验证,符号引用验证

   文件格式验证

检查验证码字节流是否符合Class 文件格式的规范,并且能被当前版本的jvm处理,包括一下几个验证点

1. 是否以魔数开头0xCAFEBABE

 2. 主,次版本是否在当前虚拟机的范围内,

 3.常量池的常量中是否有不支持的常量

 4.指向常量的各种索引是否有不支持的常量类型(检查常量tag类型)

 5.COUNSTANT_Utf8_info型的常量中是否有不符合utf编码的数据

 6.Class 文件中的各个以及文件本身是否被删除,或者附加其他信息

第一阶段远远不止这些,这只是从Hotspot虚拟机源码中摘抄的一小部分,该阶段的目的是可以正确的把流解析存储到方法区中,格式符合描述一个java类型信息要求,这阶段验证是基于二进制字节流进行的,字节流通过解析之后就会存方法区内存中,后面的三个验证都是基于方法区存储的存储结构进行的不会操作字节流

元数据处理

  对字节码的信息进行语义处理分析,保证描述的信息符合java 语言规范的要求,包括验证点如下: 

   1. 这个类是否包含子类,(除了java.lang.Object)其他类都有父类

   2.这个类是否继承了不可进行的类 final修饰

  3.这个类不是抽象类,是否实现了父类,接口中的实现的方法

  4.类中的字段,方法和父类是否矛盾,(覆盖了fnal,出现不合法的方法负载,)

第二部主要对类的元数据进行检验,保证不符合java语言规范的元数据信息

字节码验证

主要目的是通过数据流的控制流分析,确定程序语义的合法,符合逻辑,这个阶段主要对方法体进行检验,确保验证类的方法在运行的时候不会对jvm产生危害,

  1. 保证任意时刻操作数栈的数据类型和指令代码序列都能配合工作,不会出现栈中放置一个int类型,使用时候却按照long类型来加载本地变量表中

  2 .保证跳转指令不会跳转到方法体以外的代码上,

 3 .保证方法体的中的类型转换是有效的,例如把子类对象赋值给了父类数据类型,这是安全的,但是但是如果让父类赋值给一个子类类型这样是不安全的不合法的,甚至把一个毫不相干的类型之间赋值,这些都是不可以的

如果一个类的方法体没有通过字节码验证,呢这个方法体肯定有问题,但是一个方法体通过了字节码验证也不一定是安全的,这里涉及到著名的离散数学问题,Halting Problem 通俗说法就是通过程序去验证程序逻辑这一点是很难准确做到的---不能通过程序准确的检验程序是否在有限的时间内结束运行

对于数据流验证的高复杂性,jvm设计团队为了避免过多的时间耗费在字节码的检验上,在JDK1.6之后的javac 编译器进行了一系列的优化,给方法体中的code 属性的属性表中添加了一个名为StackMapTable 的属性

该属性描述了方法体中的基础块开始时本地变量表和操作栈的所有状态,在字节码验证期间就不需要根据程序推到这些状态的合法性,主需要检测StackMapTable中属性是否合法即可,这样可以节省时间,

理论上StackMapTable 属性是可以被篡改的,

 符号引用验证

最后一个阶段的校验发生在jvm将符号引用转换为直接引用的时候,这个转化动作在解析中发生,符号引用验证可以看作对类自身以外(常量池中的各种引用)的信息发生校验,

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

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

  3.符号引用中的类. 字段  方法的访问性(private public default protected ) 是否被当前类引用

如果这一步出错回产生 java.lang.IncompatibleClassChangeError ,或者子类,对于虚拟机类加载机制来说验证阶段是一个非常重要,但是不是必须的阶段,可以通过-Xverify:none 

准备

准备阶段是正式为类变量分配内存设置类变量初始化值的阶段,这些变量所使用的内存都将在方法区分配,(这里的变量指的是被static 修饰的类变量)不包括实例变量,实例变量将会在对象被实例化的时候进入堆内存中,这里的变量初始化值为0 比如

private static int a = 1234 ; 

这个值在准备阶段过后他的值是 0  ,因为这个时候并没有开始执行java 的任何方法,而把value 赋值为 1234的是putstatic 指令被程序编译过后放在<clinit>() 构造方法中,所以value = 1234 是在初始化阶段执行的,在准备过程下图的类型值通常情况下都是0 

特殊情况下,如果类字段的字段属性表中存在ConstantValue属性,那么value 会被率先初始化为对应的值比如

private static final  int value = 123 

编译时javac 直接把value 生成ConstantValue 属性,在准备阶段就会根据ConstanValue 把value 设置为  123 

解析

在验证中我们就提到,解析把符号引用转换为直接引用, 所谓的符号引用转换为直接引用其实就是一个能确定引用的”对象“的一个标识,他和内存无关,就像我们写的com.zzq.demo 全限名一样,这个标识标识的东西可以没有还没有被加载进来(内存)  转换为 直接指向内存的指针,相对偏移量或者能间接定位到目标的句柄,如果出现直接引用,被引用的”对象“一定在内存中,

jvm 没有规定什么时候发生解析,但是要求了,在执行anewarray. checkcast getfield getstatic instanceof invokeddynamic inv...

这16 个关键字之前对他们的使用符号引用进行解析,所以虚拟机可以根据需求判断到底实在类加载时就对常量池中的符号进行解析,还是用到的时候进行解析

 对一个符号引用对此解析是很常见的事情,这样来说就会降低jvm 的性能,所以jvm 在除了invokedynamic 指令之外,虚拟机实现可以对一些解析的结果进行缓存(在运行时常量池中记录直接引用,把常量标识为已经解析状态)这样可以避免重复解析同一个符号引用多次,提高效率,无论是否真正执行了多少次解析动作,虚拟机需要保证的是在同一个实体中,如果一个符号引用被成功解析过,那么后续的引用解析请求一定成功,如果第一次失败,那么其他指令对这个指令的解析请求也会收到相同的异常

 对于invokedynamic 指令上面个的规则是不成立的,当遇到某个前面已经由invokedynamic 指令引发的解析的引用符号时,并不意味着其他的invokedynamic 引用指令也会同样生效,因为invokedynamic 的目的就是为了支持动态语言(java 不会生成这个指令)因为java 是静态强制性语言, 动态是指,必须等到程序实际运行到这条指令的时候解析动作才会开始,相对的就是静态, 在加载完成还没有开始执行代码的时候就开始解析

1.类或者接口的解析

  如果现在有一个类C  ,如果要把一个 从未解析过的符号引用N解析为一个类或者接口C 的直接引用,虚拟机要完成下面3个步骤

   1. 如果C 不是一个数组类型,那么JVM会把C 的全限权限类名传递给D 的类加载器去加载C ,在加载的过程中由于元数据的验证,字节码验证的需要,可能触发相关联的加载动作,例如加载这个类的父类或者实现的接口,一旦这个加载过程出现异常,那么解析就失败,

  2.如果C 是一个数组类型,并且数组的元素类型是对象,那么N 的描述会是 [Ljava/lang/Integer  的形式,那么就会按照规则的第一点加载数组中的元素类型,如果N 的描述符是前面假设的形式,需要加载的类型就是java.lang.Integer 接着有虚拟机生成一个数组维度和数组对象

    3.如果上面没有出现异常,C 会在jvm中成为了一个有效的接口或者类,但是解析还是需要符号引用验证,确定D 是否由访问C 的全限如果没有全限还是会抛出java.lang.IllegalAccessError 异常或者子类

   2.字段解析

要解析一个字段符号引用,首先会对字段表中的class_index索引的COUNSTANT_Class_info符号进行解析,也是字段所属类或者接口的符号引用,如果解析这个类或者接口的符号引用 的过程中,出现任何异常,都会导致字段符号引用失败,如果解析完成这个字段所属的类接口用C 表示,虚拟机规范按照一下步骤对C 进行后续字段搜索、

   1.如果C 本身包含简单名称和字段描述都与目标匹配的字段,则返回这个字段的直接引用,查找结束

   2.否则在C 中时间接口,会按照上下关系依次递归搜索各个父接口,如果接口中有简单名和字段的描述相同返回直接引用

   3 .否则,如果C 不是java.lang.Object 将会按照继承关系往上递归寻找字段如果有简单名和字段的描述与之匹配返回直接引用, 查找结束

  4 .抛出异常查找失败,java.lang.NoSuchFieldError

如果查找到对应的字段,还会对字段的访问权限进行验证,如果访问权限不对应抛出java.lang.IllegalAccessError 

  3. 类方法解析

类方法解析的和类字段解析大同小异 ,首先解析出来类方法表的class_index项中索引方法所属类或者接口的符号引用,如果解析成功我们依然用C表示这个类,jvm 发生以下步骤:

  1. 类和接口符号引用个的常量类型是分开的,如果发现类方法表中class_index中所以C是一个接口,直接抛出java.lang.incompatibleClassChangeError 

   2 .在类C 中查找是否有简单名称和描述都是目标相匹配的方法,如果发现有简单名称和描述符 直接返回这个方法的引用,查找结束 (一下相同。。。)

。。。。。

4 .接口方法解析

。。。。(同)类的字段解析

初始化

   类的初始化是类的加载的最后一步,前面的类的加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器之外,其他代码全部由jvm' 主导控制,到了初始化阶段才真正开始字节码的执行 

   在准备阶段,变量已经赋值过一次系统要求的值,在初始化阶段,根据程序员设定的值进行赋值,初始化阶段是执行<clinit>()方法的执行过程, 

   <clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作静态代码块中的语句和并动作,编译器的收集过程,是由字节码文件的静态代码块的顺序决定的,

   <clinit>()方法和类的构造函数(实例构造器<init>())不同,他不需要显式的调用父类构造器,虚拟机保证会在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,在虚拟机中第一个执行<clinit>()方法的类肯定是Object()

   由于父类的<clinit>()先执行也 就说父类的静态代码块要比子类的静态代码块先执行,

  <clinit>()方法对于类或者接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,编译器可以不为这类生成<clinit>()方法

   接口中不能使用静态语句块,但是仍然有赋值过程因此接口和类都会生成<clinit>()方法,但是接口和类不同,接口的<clinit>()方法执行不需要先执行父接口<clinit>()方法,只有使用父接口中的变量时候才会执行父接口中的<clinit>()方法,,接口实现类也是一样的,只有用到接口中的变量时候才会调用执行<clinit>()方法

   虚拟机会保证一个类的<clinit>()方法是线程安全的,在多线程环境下,如果多个线程同时初始化一个类,那么只有一个线程会去完成这个任务其他线程都会阻塞等待,知道活动线程执行<clinit>()方法完毕,实际应用中,这种阻塞往往可以隐藏的

猜你喜欢

转载自blog.csdn.net/qq_39148187/article/details/81840892