虚拟机类加载机制

记录下Java中类加载的时机,类在加载过程经历的阶段。<未完待续>

7.2 类加载的时机

类从被加载到虚拟机内存开始,到卸载出内存,其整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析部分统称为连接。

​ 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,什么时候开始加载并没有进行强制约束,可以交给虚拟机的具体实现来自由把握。

​ 对于初始阶段,虚拟机规范严格规定了有且只有5种情况必须对类立即进行初始化:

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类时,发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 虚拟机启动时,用户需指定一个要执行的主类(包含main()方法的那个类),虚拟机会初始化这个类。
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

​ 接口加载过程与类加载过程与类加载过程稍微有一些不同,接口也有初始化的过程,接口中不能使用static{}语句块,但编译器仍然会为接口生成“()”类构造器,用于初始化接口中所定义的成员变量。接口与类真正有所求区别的是前面讲述的5种“有且只有”需要初始化场景中的第3种:当初始化一个类时,发现其父类还没有进行过初始化,则需要先触发其父类的初始化。但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

7.3 类加载的过程

7.3.1 加载

加载阶段虚拟机需要完成3件事情:

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

​ 这三点要求并不算具体,因此虚拟机实现与具体应用的灵活度都是相当大的,例如:通过全限定名来获取此类的二进制流,没有规定要从Class文件中获取,所以就有多种花样:

​ 从ZIP包中读取,最终成为日后JAR、EAR、WAR格式的基础。

​ 从网络中获取,这种场景最典型的应用就是Applet。

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

​ 由其他文件生成,典型场景是JSP应用,即由JSP文件生成对应的Class类。

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

​ 对于数组类而言,情况有所不同,数组类本身不通过类加载器创建,由Java虚拟机直接创建。但数组类与类加载器仍由紧密的联系,因为数组类的元素类型最终是要靠类加载器去创建,一个数组(C)创建过程如下:

​ 数组组件类型是引用型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识。

​ 如果数组的组件类型不是引用类型(例如int[]数组),Java虚拟机将会把数组C标记为与引导类加载器关联。

​ 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。

​ 加载阶段完成后,虚拟机外部的二进制字节流按照虚拟机所需格式存储在方法区中,数据存储格式由虚拟机自行定义。然后在内存实例化一个java.lang.Class类的对象(没有明确在Java堆中,对于HotSpot而言,Class虽然是对象,但是存放在方法区中),这个对象作为程序访问方法区中的这些类型数据的外部接口。加载阶段和连接阶段的部分内容是交叉进行的。

7.3.2 验证

​ JVM通过检查输入的字节流是否符合Class文件格式的约束,验证阶段大致上会完成4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

  1. 文件格式验证

    ​ 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理:

    是否以魔数0xCAFEBABE开头。

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

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

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

    ……………………

​ 第一阶段的验证远不止如此,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。该验证基于二进制字节流进行,通过该验证后字节流才进入内存的方法区中进行存储,所以后面的验证都是基于方法区的存储结构进行的,不会直接操作字节流。

  1. 元数据验证

    ​ 第二阶段是对字节码描述的信息进行语义分析以确保描述的信息符合Java语言规范的要求:

    这个类是否有父类(除了Object外所有类都要有父类)

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

    如果不是抽象类是否实现了父类或接口中要求实现的所有方法

    类中字段、方法是否与父类产生矛盾

    ………………………………..

​ 第二阶段目的主要是对类的元数据信息进行语义分析验证,保证不存在不符合Java语言规范的元数据信息。

  1. 字节码验证

    ​ 第三阶段是最复杂的阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。该阶段对类的方法体进行校验分析,保证被校验类方法在运行时不会做出危害虚拟机安全的事件:

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

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

    保证方法体中类型转换是有效的 子类可赋值给父类 反之不行。

    ………………………………………..

​ 若一个类方法的字节码未通过字节码验证,肯定是有问题的;但如果一个方法通过字节码验证,也不能说明其一定是安全的。这里涉及著名的“Halting Problem”(停机问题):通俗讲就是通过程序去校验程序逻辑是无法做到绝对准确的——不能通过程序准确地检查出程序是否能在有限的时间之内结束运行。

​ JDK1.6之后Javac编译器和JVM进行了优化,给方法体Code属性的属性表里增加了一项“StackMapTable“属性,描述了方法体所有基本块(Basic Block,按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节码验证期间就不需要根据程序推导这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法即可。

  1. 符号引用验证

    ​ 最后阶段校验发生在虚拟机将符号引用转化为直接引用,这个转化将在连接的第三阶段——解析阶段中发生。可看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验:

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

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

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

    …………………………………………..

符号验证目的是确保解析动作正常执行。

7.3.3 准备

​ 该阶段是正式为类变量分配内存并设置类变量初始值的阶段,类变量所使用的内存都将在方法区中进行分配。该阶段有两个概念:首先,进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象初始化的时候随着对象一起分配在Java堆中。其次这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:public static int value = 123;那变量在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法中,所以把value赋值为123的动作将在初始化阶段才会执行。

1526473361124

​ 上述提到的只是“通俗情况”下初始值是零值,还有一些“特殊情况”下:如果类字段的字段属性表中存在ConstantValue属性所指定的值,假设上述变量定义为:public static final int value = 123;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据这个设置将value赋值为123.

1526474197627

可以看到字节码中多了ConstantValue属性并且有一个初始值为123

7.3.4 解析
0%