深入理解java虚拟机笔记(6)虚拟机类加载机制1

一、概述

在Class文件中描述的各类信息, 最终都需要加载到虚拟机中之后才能被运行和使用。 而虚拟机如何加载这些Class文件, Class文件中的信息进入到虚拟机后会发生什么变化, 是本章将要讲解的内容。

Java虚拟机把描述类的数据从Class文件加载到内存, 并对数据进行校验、 转换解析和初始化, 最终形成可以被虚拟机直接使用的Java类型, 这个过程被称作虚拟机的类加载机制。

与那些在编译时需要进行连接的语言不同, 在Java语言里面, 类型的加载、 连接和初始化过程都是在程序运行期间完成的, 这种策略让Java语言进行提前编译会面临额外的困难, 也会让类加载时稍微增加一些性能开销,但是却为Java应用提供了极高的扩展性和灵活性, Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

例如, 编写一个面向接口的应用程序, 可以等到运行时再指定其实际的实现类, 用户可以通过Java预置的或自定义类加载器, 让某个本地的应用程序在运行时从网络或其他地方上加载一个二进制流作为其程序代码的一部分。 这种动态组装应用的方式目前已广泛应用于Java程序之中, 从最基础的Applet、 JSP到相对复杂的OSGi技术, 都依赖着Java语言运行期类加载才得以诞生。

二、类加载的时机

一个类型从被加载到虚拟机内存中开始, 到卸载出内存为止, 它的整个生命周期将会经历七个阶段:

1.加载(Loading)

2.验证(Verification)

3.准备(Preparation)

4.解析(Resolution)

5.初始化(Initialization)

6.使用(Using)

7.卸载(Unloading)

其中验证、 准备、 解析三个部分统称为连接(Linking) 。

加载、 验证、 准备、 初始化和卸载这五个阶段的顺序是确定的, 类型的加载过程必须按照这种顺序按部就班地开始, 而解析阶段则不一定: 它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性 。

《Java虚拟机规范》严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、 验证、 准备自然需要在此之前开始) : 1) 遇到new、 getstatic、 putstatic或invokestatic这四条字节码指令时, 如果类型没有进行过初始化, 则需要先触发其初始化阶段。 能够生成这四条指令的典型Java代码场景有:

1.1.使用new关键字实例化对象的时候。

1.2.读取或设置一个类型的静态字段(被final修饰、 已在编译期把结果放入常量池的静态字段除外)的时候。

1.3.调用一个类型的静态方法的时候。

2) 使用java.lang.reflect包的方法对类型进行反射调用的时候, 如果类型没有进行过初始化, 则需要先触发其初始化。

3) 当初始化类的时候, 如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化。

4) 当虚拟机启动时, 用户需要指定一个要执行的主类( 包含main()方法的类) , 虚拟机会先初始化这个主类。

5) 当使用JDK 7新加入的动态语言支持时, 如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、 REF_putStatic、 REF_invokeStatic、 REF_newInvokeSpecial四种类型的方法句柄, 并且这个方法句柄对应的类没有进行过初始化, 则需要先触发其初始化。

6) 当一个接口中定义了JDK 8新加入的默认方法( 被default关键字修饰的接口方法) 时, 如果有这个接口的实现类发生了初始化, 那该接口要在其之前被初始化。

对于这六种会触发类型进行初始化的场景, 《Java虚拟机规范》 中使用了一个非常强烈的限定语——“有且只有”, 这六种场景中的行为称为对一个类型进行主动引用。

除此之外, 所有引用类型的方式都不会触发初始化, 称为被动引用。 下面举三个例子来说明何为被动引用:

1.通过子类引用父类的静态字段, 会导致父类初始化,但不会导致子类初始化

2.通过数组定义来引用类, 不会触发此类的初始化

3.调用类引用的外部类的常量在编译阶段会存入调用类的常量池中, 本质上没有直接引用到定义常量的类, 因此不会触发定义常量的类的初始化

三、类加载的过程

3.1 加载

“加载”(Loading) 阶段是整个“类加载”(Class Loading) 过程中的一个阶段, 在加载阶段, Java虚拟机需要完成以下三件事情:

1) 通过一个类的全限定名来获取定义此类的二进制字节流。

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

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

二进制字节流可以从各种渠道获取,而不一定非要是.class文件。也可以选择:

1.从ZIP压缩包中读取, 这很常见, 最终成为日后JAR、 EAR、 WAR格式的基础。

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

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

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

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

6.可以从加密文件中获取, 这是典型的防Class文件被反编译的保护措施, 通过加载时解密Class文件来保障程序运行逻辑不被窥探。

相对于类加载过程的其他阶段, 非数组类型的加载阶段是开发人员可控性最强的阶段。 加载阶段既可以使用Java虚拟机里内置的引导类加载器来完成, 也可以由用户自定义的类加载器去完成, 开发人员通过定义自己的类加载器去控制字节流的获取方式(重写一个类加载器的findClass()或loadClass()方法) , 实现根据自己的想法来赋予应用程序获取运行代码的动态性。

数组类加载

对于数组类而言, 情况就有所不同, 数组类本身不通过类加载器创建, 它是由Java虚拟机直接在内存中动态构造出来的。 但数组类与类加载器仍然有很密切的关系, 因为数组类的元素类型(ElementType, 指的是数组去掉所有维度的类型) 最终还是要靠类加载器来完成加载, 一个数组类( 下面简称为C) 创建过程遵循以下规则:

1.如果数组的组件类型( Component Type, 指的是数组去掉一个维度的类型, 注意和前面的元素类型区分开来) 是引用类型, 那就递归采用本节中定义的加载过程去加载这个组件类型, 数组C将被标识在加载该组件类型的类加载器的类名称空间上

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

3.数组类的可访问性与它的组件类型的可访问性一致, 如果组件类型不是引用类型, 它的数组类的可访问性将默认为public, 可被所有的类和接口访问到。

加载阶段结束后, Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了, 方法区中的数据存储格式完全由虚拟机实现自行定义, 《Java虚拟机规范》 未规定此区域的具体数据结构。 类型数据妥善安置在方法区之后, 会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的类型数据的外部接口。

加载阶段与连接阶段的部分动作( 如一部分字节码文件格式验证动作) 是交叉进行的, 加载阶段尚未完成, 连接阶段可能已经开始, 但这些夹在加载阶段之中进行的动作, 仍然属于连接阶段的一部分, 这两个阶段的开始时间仍然保持着固定的先后顺序

3.2 验证

验证是连接阶段的第一步, 这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》 的全部约束要求, 保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

Java虚拟机如果不检查输入的字节流, 对其完全信任的话, 很可能会因为载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃, 所以验证字节码是Java虚拟机保护自身的一项必要措施。

验证阶段是非常重要的, 这个阶段是否严谨, 直接决定了Java虚拟机是否能承受恶意代码的攻击, 从代码量和耗费的执行性能的角度上讲, 验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重。

但从整体上看, 验证阶段大致上会完成下面四个阶段的检验动作: 文件格式验证、 元数据验证、 字节码验证和符号引用验证。

1.文件格式验证

第一阶段要验证字节流是否符合Class文件格式的规范, 并且能被当前版本的虚拟机处理。 这一阶段可能包括下面这些验证点:

·是否以魔数0xCAFEBABE开头。

·主、 次版本号是否在当前Java虚拟机接受范围之内。

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

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

·CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。

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

实际上第一阶段的验证点还远不止这些, 上面所列的只是从HotSpot虚拟机源码中摘抄的一小部分内容, 该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内, 格式上符合描述一个Java类型信息的要求。 这阶段的验证是基于二进制字节流进行的, 只有通过了这个阶段的验证之后, 这段字节流才被允许进入Java虚拟机内存的方法区中进行存储, 所以后面的三个验证阶段全部是基于方法区的存储结构上进行的, 不会再直接读取、 操作字节流了

2.元数据验证

第二阶段是对字节码描述的信息进行语义分析, 以保证其描述的信息符合《Java语言规范》 的要求, 这个阶段可能包括的验证点如下:

·这个类是否有父类(除了java.lang.Object之外, 所有的类都应当有父类) 。

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

·如果这个类不是抽象类, 是否实现了其父类或接口之中要求实现的所有方法。

·类中的字段、 方法是否与父类产生矛盾(例如覆盖了父类的final字段, 或者出现不符合规则的方法重载, 例如方法参数都一致, 但返回值类型却不同等) 。

第二阶段的主要目的是对类的元数据信息进行语义校验, 保证不存在与《Java语言规范》 定义相悖的元数据信息

3.字节码验证 第三阶段是整个验证过程中最复杂的一个阶段, 主要目的是通过数据流分析和控制流分析, 确定程序语义是合法的、 符合逻辑的。 在第二阶段对元数据信息中的数据类型校验完毕以后, 这阶段就要对类的方法体(Class文件中的Code属性) 进行校验分析, 保证被校验类的方法在运行时不会做出危害虚拟机安全的行为, 例如:

·保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作, 例如不会出现类似于“在操作栈放置了一个int类型的数据, 使用时却按long类型来加载入本地变量表中”这样的情况。

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

·保证方法体中的类型转换总是有效的, 例如可以把一个子类对象赋值给父类数据类型, 这是安全的, 但是把父类对象赋值给子类数据类型, 甚至把对象赋值给与它毫无继承关系、 完全不相干的一个数据类型, 则是危险和不合法的。

如果一个类型中有方法体的字节码没有通过字节码验证, 那它肯定是有问题的; 但如果一个方法体通过了字节码验证, 也仍然不能保证它一定就是安全的。

由于数据流分析和控制流分析的高度复杂性, Java虚拟机的设计团队为了避免过多的执行时间消耗在字节码验证阶段中, 在JDK 6之后的Javac编译器和Java虚拟机里进行了一项联合优化, 把尽可能多的校验辅助措施挪到Javac编译器里进行。

具体做法是给方法体Code属性的属性表中新增加了一项名为“StackMapTable”的新属性, 这项属性描述了方法体所有的基本块开始时本地变量表和操作栈应有的状态, 在字节码验证期间, Java虚拟机就不需要根据程序推导这些状态的合法性, 只需要检查StackMapTable属性中的记录是否合法即可。 这样就将字节码验证的类型推导转变为类型检查, 从而节省了大量校验时间。

4.符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候, 这个转化动作将在连接的第三阶段——解析阶段中发生。 符号引用验证可以看作是对类自身以外(常量池中的各种符号引用) 的各类信息进行匹配性校验, 通俗来说就是, 该类是否缺少或者被禁止访问它依赖的某些外部类、 方法、 字段等资源。 本阶段通常需要校验下列内容:

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

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

·符号引用中的类、 字段、 方法的可访问性(private、 protected、 public、 <package>) 是否可被当前类访问。

符号引用验证的主要目的是确保解析行为能正常执行, 如果无法通过符号引用验证, Java虚拟机将会抛出一个java.lang.IncompatibleClassChangeError的子类异常, 典型的如:java.lang.IllegalAccessError、 java.lang.NoSuchFieldError、 java.lang.NoSuchMethodError等。

验证阶段对于虚拟机的类加载机制来说, 是一个非常重要的、 但却不是必须要执行的阶段, 因为验证阶段只有通过或者不通过的差别, 只要通过了验证, 其后就对程序运行期没有任何影响了。 如果程序运行的全部代码(包括自己编写的、 第三方包中的、 从外部加载的、 动态生成的等所有代码) 都已经被反复使用和验证过, 在生产环境的实施阶段就可以考虑使用-Xverify: none参数来关闭大部分的类验证措施, 以缩短虚拟机类加载的时间

posted @ 2022-03-18 14:10  Mars.wang  阅读(51)  评论(0编辑  收藏  举报