这一章主要讲了2部分内容:
- 其一是:类加载的整个过程
- 其二,几种类加载器及其工作原理
1 综述
- 类加载机制:概括的说就是,虚拟机
将class文件中描述类的数据加载到内存当中
,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的java类型
。 - java中,类型的加载、连接和初始化过程都是在程序
运行期间完成
的。java的可动态扩展的特性就是依赖运行期动态加载和动态连接这个特点的实现的。例如,面向接口的程序等到运行时在指定实际的实现类。 - 本章所说的class文件并不是存在磁盘中的文件,而是一串二进制的字节流,无论以何种形式存在都可以。
2 类加载的时机
整个的类生命周期包括7个阶段:加载、验证、准备、解析、初始化、使用、卸载。其中有5个阶段的顺序是确定的
,这5个阶段开始顺序必须依次为:加载、验证、准备、初始化、卸载。
所谓开始时间顺序是确定的
,但不是说各自的结束时间也是顺次确定的
。事实上,在执行过程中,这几个阶段的执行通常都是有交叉的进行的。
什么时候开始进行类加载?虚拟机规范没有明确规定。
什么时候必须进行类初始化
?有且仅有5个情况(称为对类的“主动引用”)
- 遇到new、getstatic、putstatic、invokestatic这4个字节码指令时,
如果对应的类没有进行初始化,则需要先触发这个类的初始化
。如:new实例化对象
;读取或设置类的静态字段
;调用类的静态方法
。 - 使用java.lang.reflect包
进行类的反射调用时
。如果该类没有进行初始化,则必须先触发这个类的初始化。 - 初始化一个类时,
如果其父类还没有初始化
,则需要先触发其父类的初始化。 - 虚拟机启动时,用户会指定一个要执行的主类(
包含main方法的类
),虚拟机会先初始化这个主类
。 - 使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果ref_getStatic, ref_putStatic, ref_invokeStatic的方法句柄(引用),并且这个方法句柄对应的类没有进行初始化,则需要先触发这个类的初始化。
被动引用:除以上之外,所有引用类的方式都称为“被动引用”,自然也不会触发对类对初始化。
被动引用的3个小例子:
通过子类引用父类的静态字段
,不会引起子类的初始化,只是父类的实例化。数组的类型为一个类,声明数组时,不会触发此类的初始化
,而是由newarray
指令触发"[…SuperClass"的初始化。- 被调用类中的
常量在编译阶段会被存入调用类的常量池
,并没有直接引用到定义该常量的类
。因此也不会触发定义该常量的类的初始化。
什么时候进行接口的初始化
?和类的5种情况中只有第3种不一样。即为一个接口进行初始化时,不要求其父接口都已经完成了初始化
。只有用到其父接口时,才对其父接口进行初始化。
3 类加载的过程
3.1 加载
★本阶段完成的工作
(3个):
根据常量池
中的CONSTANT_Class_info常量,来获取该类的全限定名
。根据全限定名,获取定义此类的二进制字节流
。- 将这个静态的二进制字节流
转化为方法区的运行时数据结构
。 在内存中生成一个代表这个类的java.lang.Class对象
,作为方法区这个类的各种数据的访问入口
。
注1:
读取到的类的二进制字节流不一定是从Class文件中读取的,还有其他几种可以获取二进制字节流的方式。比如,从zip包读取
;从网络中获取
(Applet);运行时计算生成,动态代理技术
。
★类加载是否通过类加载器?分2种情况讨论
-
对于非数组类
:既可以由系统提供的
引导类加载器加载,也可以由用户自定义的
类加载器加载(即重写类加载器的loadClass( )方法
)。 -
对于数组类
:数组本身是有虚拟机直接创建的
,不由类加载器创建
。但要注意的是:数组类的元素类型是必须由类加载器进行创建的
。
★数组创建过程遵循的原则有3个:
- 如果数组的组件类型(指去掉一个维度的类型)为引用类型,则递归调用上面的
加载过程
去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识(一个类必须要和类加载器一起确定唯一性)。 - 如果数组的组件类型不是引用类型(如int[ ]),则虚拟机将会把数组标记为与引导类加载器关联。
- 数组类可见性和他的组件类型一致。如果组件类型不是引用类型,则数组类型默认为public。
注1:
加载完成后,字节流将被存储在方法区
,然后在内存中实例化
java.lang.Class类的对象
,它被存储在方法区
,虽然它是对象,但并不存在堆中,它就是程序访问方法区中这些类型数据的外部接口
。
注2:
加载和连接交叉进行,但是仍保持先后顺序。
3.2 验证
本阶段功能
:确保Class文件的字节流中包含的信息符合当前虚拟机的要求
,且不会危害虚拟机自身的安全
。
验证内容:二进制字节流,验证成功后,进入方法区存储
,后面的3个验证阶段是基于方法区的存储结构进行的
,不会在直接操作字节流。
地位:非常重要,但非必须。这一阶段直接决定了java虚拟机是否能承受恶意代码的攻击。验证阶段的工作量在虚拟机的类加载子系统中占相当大一部分。
验证的4个阶段:
★ 文件格式验证:这一阶段是后面3个验证阶段的基础
- 是否符合Class文件格式(
是否以魔数0*CAFEBABE开始
); 是否能被当前版本
的java虚拟机加载。
★ 元数据验证
- 目的:
对字节码进行语义分析
,保证符合java语言规范
。 - 可能的验证点
除Object
之外的类,都必须有父类
- 类
是否继承了被final修饰的类
- 如果类不是抽象的,是否实现类父类或接口中
要求实现的所有方法
- 类中
字段、方法是否与父类产生矛盾
(覆盖类父类的final字段
;不符合规则的方法重载
)
★ 字节码验证
- 地位:整个验证过程最复杂的阶段
- 目的:通过验证数据流和控制流,来确保程序语义是合法、符合逻辑的。
注1:
第二阶段对元数据信息中的数据类型
做校验后,这个阶段对类的方法体
做校验,保证方法运行时不会做出危害虚拟机的事件
。
如:
♦ 保证操作数栈的数据类型与指令代码序列配合工作,不会出现操作数栈有个int类型的数据,使用时却按long类型来加载入本地变量表
♦ 保证跳转指令不会跳转到方法体以外
的字节码指令上
♦ 保证方法体中的类型转换有效
由于数据流验证的高复杂性,此处是有一个优化要注意,Code属性的属性表里面有个属性StackMapTable,这项属性描述了方法体的所有基本块(按控制流拆分的代码块)开始时本地变量表和操作数栈应用的状态。类型检查替代了类型推导,节省时间。
★ 符号引用验证
- 功能:
确保解析操作能正常执行
。解析阶段要完成的是:将符号引用转为直接引用。符号引用验证是对类自身以外的信息进行匹配性校验 - 要检查内容:
- 根据类的全限定名
能否找到指定的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称所对应的字段和方法java.lang.
NoSuchFieldError
, java.lang.NoSuchMethodError
- 符号引用中的类、字段、方法的访问性,是否可以被当前类访问java.lang.IllegalAccessError
- 根据类的全限定名
3.3 准备
功能
:正式为类变量分配内存并设置初始值,这些变量被分配的内存是方法区。
注1:
进行内存分配的仅包括被static修饰的类变量
,不包括实例变量
,实例变量将在对象实例化时随对象一起分配在java堆中。
注2:
初始值“通常”
指数据类型的零值。
public static int value = 123 ;
value在准备阶段被赋值0
。把value=123的putstatic指令被程序编译后,存放于类构造器< clinit>()方法中
,所以value=123将在初始化阶段才会执行
。
初始值“通常”
指数据类型的零值,那特殊情况如下,准备阶段value会被赋值为字段属性表中ConstantValue属性所指的值:123
public static final int value = 123 ;
3.4 解析
功能
:将常量池内的符号引用转化为直接引用
。
解析时间:根据需求判断是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析。
符号引用:任何形式字面量都可以,只要能定位到目标
,引用的目标不一定已经加载到内存中
。
直接引用:直接指向目标的指针
or 相对偏移量 or 能间接定位到目标的句柄。引用的目标一定在内存中
。
缓存功能
:对同一个符号引用进行解析是常见的
。所以对于第一次解析结果会进行缓存
(运行时常量池中记录直接引用),从而避免重复操作
。但除了invokedynamic指令,它所对应的引用称为“动态调用点限定符”,动态就是指程序实际执行这条指令时,解析动作才进行。相对的,其余的解析就是“静态”的,可以在加载阶段进行解析。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类符号引用进行。后面3种与动态语言相关。
★ 类或接口的解析
★ 字段解析
★ 类方法解析
★ 接口方法解析
3.5 初始化
地位:类加载过程的最后一步
。真正开始执行类定义的java代码。
功能:根据程序员的代码,为类变量和其他资源进行初始化
。虽然准备阶段已经为类变量进行过初始化了。
实质:执行类构造器< clinit>方法的过程
。
< clinit>方法的特点:
- 特点1
A、内容:类中所有的类变量
、静态语句块
(static{})中的语句组成。
B、语句的组合收集
工作由编译器自动完成。
C、收集顺序:源文件中出现的顺序
。
D、静态语句块中:只能访问在静态语句块之前的变量,定义在它后面的变量,静态语句块只能为其赋值,但不能访问。
static{
i=0;//正确
System.out.print(i);//编译器提示错误
}
static int i=1;
-
特点2
< clinit>方法与类的构造函数(实例构造器< init>()方法)不同
,它不需要显示调用父类构造器
,虚拟机会保证执行子类的< clinit>()方法前,父类的< clinit>()方法已经执行完毕。因此虚拟机中最先执行的是object的< clinit>方法。 -
特点3
由于父类的< clinit>先执行,所以父类中定义的静态语句块要优于子类的变量赋值操作,下面字段B的值是2而不是1。
static class Parent{ public static int A=1; static { A=2; } } static class Sub extends Parent{ public static int B=A; } public static void main(String[] args){ System.out.println(Sub.B); }
-
特点4
如果一个类或接口没有静态语句块
,也没有对变量的赋值操作
,那么编译器可以不为类生成< clinit>方法
。 -
特点5
接口中不能使用静态块
,但仍有变量初始化的赋值操作
,所以接口和类一样,会生成< clinit>方法。接口与类不同的是,执行接口的< clinit>方法不必先执行父接口的< clinit>方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时不会执行接口的< clinit>方法。 -
特点6
虚拟机保证< clinit>方法在多线程中被正确加锁、同步。多个线程同时初始化类时,只有一个线程执行< clinit>方法,其他线程阻塞
。
4 类加载器
定义:是实现了“根据类的全限定名来获取此类的二进制字节流”功能的代码模块。
功能:
实现类的加载动作
。- 和类本身,共同确立了java虚拟机中唯一的一个类。也就是说,如果想知道两个类是否相同,
首先他们的类加载器必须是一致的
,才能继续比下去。
4.1 分类:
★ java虚拟机角度:
- 启动类加载器(Bootstrap ClassLoader),由c++实现,虚拟机的一部分
- 所有的其他类加载器,由java语言实现,独立于虚拟机外部,继承自抽象类java.lang.ClassLoader。
★ java程序员角度:大部分程序都会用到系统提供的3种类加载器。
-
启动类加载器
(Bootstrap ClassLoader)
负责:将存放在< java_home>\lib中的,或者被-Xbootclasspath参数所指定的路径中的,且可以被虚拟机识别的类库
加载到虚拟机中。java程序无法直接引用启动类加载器
。 -
扩展类加载器
(Extension ClassLoader)
负责:加载< java_home>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。开发者可以直接使用扩展类加载器
。 -
应用程序类加载器
(Application ClassLoader,系统类加载器)
♦ 负责:加载用户类路径上所指定的类库
,开发者可以直接使用
这个类加载器。
4.2 类加载器之间的层次关系(类加载器的双亲委派模型)
-
特点:除了启动类加载器外,
其余加载器都要有父类加载器
。加载器之间的父子关系不是继承,而是组合
,使用组合关系来复用父加载器的代码
。组合:鸟和翅膀的强组合关系,生命周期一样。 -
工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此。因此所有的加载请求最终
都应该传送到顶层的启动类加载器中
,只有当父加载器反馈自己无法完成这个请求时
,子加载器才会尝试自己去加载
。 -
优点:
★
java类
随着它的类加载器
一起具备了一种带有优先级的层次关系。例如类java.lang.Object
,无论哪个类加载器加载这个类
,最终都是委派给启动类加载器进行加载
,因为Object类 在程序的 各个类加载器环境中 都是同一个类。★双亲委派模型对于保证java程序的稳定运作很重要。
实现代码的逻辑:
先检查是否被加载过,若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父加载器。若父类加载失败
,抛出ClassNotFoundException异常后
,在调用自己的fingClass()方法进行加载
。 -
缺点:
效率低
4.3 双亲委派模型的3次破坏
注1:破坏,并不带有贬义色彩,这只是根据实际情况的需要,适当的改变了双亲委派模型的设计而已。
-
第一次破坏:在双亲委派模型出现之前,即JDK1.2发布之前,为了向前兼容。
-
第二次破坏:因设计本身缺陷导致。双亲委派解决了各个类加载器的基础类的统一的问题,基础类总是作为被用户代码调用的API,但是如果基础类要调用用户的代码,怎么办?线程上下文加载器(Thread Context ClassLoader)可以让父类加载器请求子类加载器去完成类加载器的动作。Java中所有设计SPI的加载动作基本上都采用这种方式,如JNDI、JDBC等。
-
第三次破坏:是由于用户对程序动态性的追求而导致的。 动态性指:代码热替换、模块热部署。应用程序不用重启就能立即使用。OSGI实现模块化热部署的关键是它自定义的类加载机制的实现。每个程序模块都有自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。OSGI收到类加载请求时,现在符合双亲委派规则下查找,再在平级的加载器中进行查找。