7.1概述
虚拟机把描述类的数据从class文件加载到内存,经过校验、转换解析、初始化,最形成能够被虚拟机直接使用的java类型,这就是虚拟机的加载过程
类的加载、连接、初始化实在运行期进行的,会稍微增加性能开销,但是也会提高灵活性
动态扩展的语言特性是依赖于运行期动态加载和动态连接这个特点实现的
运行期类加载应用:applet、jsp、osgi
7.2类的加载时机
类的加载、验证、准备和初始化顺序确定,按部就班的开始, 而解析不一定:某些情况下载初始化之后,这是为了支持java语言的运行时绑定(也成动态绑定,或者晚期绑定)
互相交叉混合进行
类的加载时机,没有强制约束,有且只有5种情况需要对类初始化(而加载、验证、准备自然需要在这之前)--主动引用
遇到new、getstatic、putstatic、invokestatic四条字节码指令时候,如果没有初始化,则需要触发初始化;最常见的场景:使用new关键字实例化对象、获取或设置类的静态字段(被final修饰,在编译期把结果已经写入常量池的排除)、调用类的静态方法
使用java.lang.reflect包的方法对类进行反射调用的时候
当初始化一个类的时候,发现父类还没有初始化,则需要先初始化后父类
虚拟机启动的时候,用户需要指定一个执行的主类,虚拟机会先初始化这个类
当使用jdk1.7的动态语言支持时,如果一个Java.lang.invoke.MethodHandle实例解析的结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这些方法句柄对应的类没有实例化
所有引用的方式都不会触发初始化,称为被动引用
通过子类调用父类静态字段,不会导致子类初始化
通过数组定义类引用类,都不会触发此类的初始化
常量在编译期被存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发初始化
接口与类的差异:初始接口的时候,并不要求其父类接口全部初始化,只有真正使用父类接口的时候才会初始化
7.3类的加载过程
7.3.1加载
在加载阶段,虚拟机需要完成3件事情
通过一个类的全限定名来获取定义该类的二进制字节流
将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成代表该类的java.lang.Class对象(没有明确规定在Java堆上,对于hotSpot虚拟机,Class对象比较特殊,虽然是对象,存放在方法区),作为方法区这个类各种数据的访问入口
二进制流的获取
从zip包获取,最终成为日后jar、ear、war格式的基础
从网络获取,这种场景最典型的应用就是applet
运行时计算机生成,使用最多的就是动态代理
由其他文件生成,典型的应用jsp,即由jsp生成对应的类
从数据库获取,场景比较少见,有些中间件(SAP Netweaver)可以选择把程序安装到数据库来完成程序代码在集群间的分发
... ...
一个非数组类的加载,是开发人员可控性最强的,可以自定义类加载器,定义自己的类加载器控制字节流的获取方式
数组类本身不是通过类加载器创建,是由Java虚拟机直接创建,创建数组需要遵循的规则如下:
如果组件类型是一个引用类型,递归采用加载过程去加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上标识
如果组件类型非引用类型,将会把数组标记为与引导类相关联
数组类的可见性和他的组件类可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public
加载阶段和连接阶段的部分内容是交叉进行的
7.3.2验证
验证的目的是为了确保class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全
四个阶段的检验过程
1.文件格式验证
是否已魔数0xCAFEBABF开头
主、次版本号是否在当前虚拟机处理范围
常量池的常量是否有不被支持的常量类型
指向常量的各种索引值中是否有指向不存在或者不符合类型的常量
CONSTACT_Utf8_info型的常量是否有不符合utf8编码的数据
Class文件中各个部分及文件本身是否有被删除或者附加其他信息
......
是否符合Class文件格式规范,且能够被当前版本虚拟机处理;保证输入的字节流能够被解析并存储于方法区之内,后面的三个阶段是基于方法区存储结构进行的,不会直接操作二进制流
2.元数据校验
这个类是否有父类(出Object类以外,其他类都应该有父类)
父类是否继承了不允许被继承的类(被final修饰的类)
如果类不是抽象类,是否实现了其父类或者接口中要求实现的方法
类中的字段、方法是否与父类产生矛盾(例如覆盖父类的final字段,或不符合规则的冲则)
对字节码的描述信息(元数据)进行语义分析,保证描述的信息符合Java语言规范要求
3.字节码校验
保证任意时刻的数据栈的数据类型与指令代码序列都能配合执行(例如不会出现
操作数栈放置了int类型数据,使用时却按long类型加载到本地变量表中)
保证跳转指令不会调到方法体以外的字节码指令上
保证方法体的类型转换时有效的
通过数据流和控制流确定程序语义是合法的、符合逻辑的;对类的方法体进行校验分析,
保证被校验类的方法在运行期间不会做出危害虚拟机的事情
4.符号引用验证
符号引用中通过字符串的全限定名能否找到对应的类
在指定类中是否存在符合方法的字段描述符以及简单名称所描述的字段和方法
符号引用中的类、字段、方法的访问性是否可以被当前类访问
... ...
发生在将符号引用转换为直接引用的时候,对类信息以外的的信息进行匹配校验;
保证解析动作能够正常执行
验证阶段是非常必要的,但不是必须的
7.3.3准备
为类变量分配内存并设值初始值;变量所使用的内存都在方法区上分配
通常情况下初始为零值,如果字段属性表中存在ConstantValue属性,则被初始化ConstantValue指定的值
7.3.4解析
将常量池中的符号引用替换为直接引用的过程
符号引用:以一组符号来描述所引用的对象,符号可以是任意形式的字面量,只要使用时能无歧义的定位到目标即可,
与虚拟机实现布局无关,引用的目标不一定加载到内存
直接引用:直接指向目标的指针、相对偏移量或者一个能够间接定位目标的的句柄,与虚拟机布局相关
1.类或接口解析
2.字段解析
3.类方法解析
4.接口方法解析
7.3.5类初始化
初始化过程是执行类构造器()的过程
clinit()方法执行过程中可能影响的程序运行特点和细节
由编译器自动收集类中的所有类变量赋值动作和静态语句块中的语句合并产生的,编译收集顺序由语句在源文件的顺序决定,静态语句块中只能访问到定义在语句块之前的变量,定义在他后的变量,可以赋值,但不能访问
和实例构造器init()不同,不需要显式的调用父类构造器,虚拟机会保证在子类clinit()方法执行之前,父类的已经执行完毕
父类中定义的静态语句块要优先于子类的变量赋值操作
对于类或者接口不是必须的
接口中没有静态语句块,但仍有变量初始化的赋值操作,因此会生成clinit()方法,但与类不同,执行接口的()方法不需要先执行父接口的()方法,只有父接口变量使用时,才初始化虚拟机保证一个类的clinit()方法在多线程环境中被正确加锁、同步
7.4类加载器
7.4.1类与类加载器
类加载器只用于类加载动作
对于任意一个类,都需要由加载他的类加载器和类本身确立其在Java虚拟机中的唯一性
每个类加载器,都有独立的命名空间
比较两个类”相等“,只有在同一个类加载器加载的前提下才有意义
7.4.2双亲委派模型
三种系统提供的类加载器
启动类加载器(Bootstrap ClassLoader)负责存放在/lib目录中,或被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别(仅按照文件名称识别)的类库加载到虚拟机内存中无法被java程序直接引用
扩展类加载器(Extension ClassLoader)负责存放在\lib\ext目录中,或者被java.ext.dirs系统变量指定路径中的类库,开发者可以直接使用
应用程序类加载器(Application ClassLoader)加载用户类路径(Classpath)上指定的类库,开发者可以直接使用
如果没有自定义类加载器,一般情况这个就是程序默认类加载器
图中展示的类加载器的这种层次关系,称为类加载器的双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都需要有自己的父类加载器
类加载的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父加载器
加载过程:如果一个类加载收到类加载请求,先不会自己去处理,而是委托父类加载器去完成,依次递归,因此所有加载请求最终都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子类才会去尝试加载
一个显而易见的好处Java类随着类加载器一起具备了一种带有优先级的层次关系
保证java程序的稳定性
解决了各个类加载器基础类的统一问题
7.4.3破坏双亲委派模型
3次大破坏
发生在双亲委派模型出现之前,JDK1.2发布之前
自身缺陷造成(基础类调用用户代码,怎么办?SPI,Service Provider Interface加载,例如JNDI、JDBC、JCE、JAXB、JBI),线程上线文类加载器
由于用户追求程序的动态性(代码热替换、模块热部署)造成的
参考文献:
[1] 深入理解Java虚拟机 第二版 --周志明
网友评论