加载机制描述
虚拟机把描述类的数据从CLass文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:
1. 加载(loading)
2. 验证(Verification)
3. 准备(Preparation)
4. 解析(Resolution)
5. 初始化(Initialization)
6. 使用(Using)
7. 卸载(Unloading)
其中验证、准备、解析3个部分统称为连接(Linking)
类的解析阶段不一定按序进行,在某些情况下可以在初始化阶段之后开始,以支持Java语言的运行时绑定(动态绑定、晚期绑定)。
这些阶段通常都是互相交叉地混合式进行,通常会在一个阶段执行的过程中调用、激活另外一个阶段。
其中虚拟机规范严格规定了有且只有5种情况必须对类进行“初始化”(加载、验证、准备需要在此之前):
1. 使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候(被final修饰,已在编译器把加过放入常量池的静态字段除外),以及调用一个类的静态方法的时候。
2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先出初始化这个类
5. 使用JDK1.7的相关动态语言支持
以上5个场景都是对一个类进行主动引用,除此之外,所有引用类的方式都不会触发初始化,称为被动引用。 包括以下场景
1. 通过子类引用父类的静态字段,不会导致子类初始化,对于静态字段访问,只有直接定义这个字段的类才会被初始化。
2. 同故宫数组定义来引用类,不会触发此类的初始化,如定义:
MyClass [] classes = new MyClass[10];
,这种时候不会真正对类进行初始化
3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。如定义:
public class MyClass{
public static final String TEST = "test";
}
当通过MyClass访问常量TEST时,MyClass不会被初始化。
类加载的过程
主要包括加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。
加载
首先明确:“加载”只是“类加载(Class Loading)”过程的一个阶段。
在加载阶段,虚拟机需要完成一下3件事情
1. 通过一个类的全限定名来获取定义此类的二进制字节流(如从ZIP包读取,构建JAR、WAR等包格式、从网络读取,Applet应用、运行时计算生成,动态代理技术)
2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。
验证
验证是链接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要是为了防止Class文件不合法生成,从而载入了有害的字节流而导致系统崩溃。
具体包括:
文件格式验证
验证字节流是否符合Class文件格式的规范,比如有:
* 是否以魔数(0xCAFEBABE)开头
* 主次版本号是否在当前虚拟机处理范围内
* 常量池是否有不被支持的常量类型
元数据验证
对字节码描述的信息进行语义分析,已保证其描述的信息符合Java语言规范,比如有:
- 这个类是否有合法父类(除了Object外,都应有父类)
- 某个类是否继承了不允许被继承的类(final修饰)
- 如果这个类不是抽象类,是否实现了父类或接口中要求实现的方法
字节码验证
主要目的是通过数据流和控制流分析,确定程序语义是否合法、符合逻辑。比如:
- 保证操作数栈的数据类型与指令代码序列都能配合工作
- 保证跳转指令不会跳转到方法体外的字节码指令上
- 保证方法体中的类型转换都是有效的
符号引用验证
这个校验发声在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在链接的第三阶段,解析阶段,目的是确保解析动作能正常执行。通常会校验
- 符号引用中通过字符描述的全限定名是否能找到对应的类
- 在制定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的访问是否可被当前类访问
准备
转杯阶段是正式为类变量分配内存并设置类变量初始化值的阶段,这些变量所使用的内存都在方法区中进行分配。这里进行内存分配的进包括静态类变量,而不包括实例变量,实例变量将在对象实例化时随对象一起分配在Java堆中。
另一方面,初始值是指数据类型的零值(默认值),如:
public static int value=1
在准备阶段,会被初始化为0,为1的赋值操作会在初始化阶段才执行
而如果类字段的字段属性表中存在ConstantValue属性,即假如上面改为:
public static
final
int value = 1
,则在准备阶段就会被赋值为1
解析
解析是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用以一组符号来描述所引用的目标,它是一个包含足够定位信息的字符串,实际使用时可以找到相应的位置。比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。
运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。
初始化
类加载的初始化阶段对类变量赋予正确的值。主要有两种初始化方式,一种是通过类变量初始化语句;一种是静态初始化语句。
<clinit>()
方法
所有的类变量初始化语句和静态初始化语句都被Java编译器收集在一起,放在一个特殊方法里。对于类而言,该方法称为类初始化方法,对于接口而言,该方法称为接口初始化方法。在Java class文件里,类和接口的初始化方法统一被称作为() 方法。
并且这种方法只能被Java虚拟机调用,Java程序是无法调用的。
并非每个类都拥有()方法,以下三种情况就没有:
1. 类没有申明类变量,也没有任何静态初始化语句;
2. 类申明了类变量,但是没有任何的类变量初始化语句,页没有静态初始化语句进行初始化;
3. 类近包含静态final变量的类变量初始化语句,而且是编译时候的常量;
另一方面,初始化类的过程必须保持同步,如果有多个线程初始化一个类,仅仅允许一个线程执行初始化,其他的线程都需要等待。
类和类加载器
定义
JVM把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到JVM外部实现,使得应用程序自己可以决定如何去获取所需要的类。实现这个动作的代码模块成为“类加载器”。
注意用不同的类加载器加载同一个类,所得到的Class对象是不同的,通过这些Class对象创建的类实例对象,强制转换为另一个类加载器加载的同一Class会报错。
在java.lang包下有一个抽象类ClassLoader,它通过我们给定的类的全限定名,找到对应的Class字节码文件,然后加载它转化为一个java.lang.Classs类的一个实例。
在实际使用中,我们常常看到三类类加载器:
启动类加载器
这个类加载器负责将<JAVA_HOME>\lib
目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,代码是使用C++编写的.是虚拟机自身的一部分.
扩展类加载器(Extendsion ClassLoader):
这个类加载器负责加载<JAVA_HOME>\lib\ext
目录下的类库,用来加载java的扩展库,开发者可以直接使用这个类加载器.
应用程序类加载器(Application ClassLoader)/系统类加载器:
这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器.
下图展示了以上类加载器的层次关系:
双亲委派模型
上面展示的层级关系,体现了类加载器的双亲委派模型(Parent Delegation Model),双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的父子关系往往不会以继承的关系实现,二用组合来复用父加载器的代码。
工作流程
如果一个类加载器受到了类加载请求,会首先把这个请求委托给父类,直到到达最顶层的启动类加载器,只有当父加载器反馈无法完成加载请求时,子加载器才会尝试去加载。
使用这种工作机制的优势在于,每个类的创建层级分明,并且能够唯一找到对应优先级最高的加载器去创建,不会因为同一个类(比如java.lang.Object)通过不同类加载器创建,从而得到多个不同的(Object)类,造成程序混乱。