标签:
JVM把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成JVM可以直接使用的Java类型的过程就是类加载机制。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称连接。顺序如下:
加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始,而解析阶段不一定;它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性。值得注意的是:这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。
什么时候需要开始类加载过程的第一个阶段:加载。虚拟机规范中并没有强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机是严格规定了有且只有四种情况必须立即对类进行初始化(而加载、验证、准备自然需要在此之前开始):
对于这四种情况会触发类进行初始化的场景,虚拟机规范中使用了一个很强类的限定词:“有且只有”,这四种场景中的行为被称为一个类进行主动引用。除此之外所有引用类的方式,都不会触发初始化,被称为被动引用。下面举三个例子来说明被动引用:
package org.fenixsoft.classloading;
/**
* 被动使用类字段演示一:
* 通过子类引用父类的静态字段,不会导致子类初始化
**/
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
/**
* 非主动使用类字段演示
**/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
运行结果:
SuperClass init!
123
package org.fenixsoft.classloading;
/**
* 被动使用类字段演示二:
* 通过数组定义来引用类,不会触发此类的初始化
**/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
package org.fenixsoft.classloading;
/**
* 被动使用类字段演示三:
* 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
**/
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
/**
* 非主动使用类字段演示
**/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
运行结果:
hello world
通过一个类的全限定名来获取定义此类的二进制流,并没有指明二进制字节流要从一个Class文件中获取,准确的说根本没有指明要从哪里获取及怎么样获取。相对于类加载过程的其他阶段,加载阶段的获取类的二进制流是开发期可控性最强的阶段,因为加载阶段既可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器去完成。加载阶段与连接阶段的部分内若能(如一部分字节码文件格式的验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载之中进行的动作,仍然属于连接阶段的内容,这两个阶段仍然保持着固定的顺序。
验证
验证是连接阶段的第一步,目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。该阶段的工作量在虚拟机的类加载子系统中占了很大一部分。大致会完成下面四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
准备
是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注意两点:
public static int value = 123;
那么变量value在准备阶段过后的初始值为0,而不是123,因为这个时候并未开始执行java的任何方法,而把value赋值为123的putstatic指令是程序在编译后,存放于类构造器方法之中,所以把value赋值为123的动作在初始化阶段才会被执行。
如果类字段的字段属性表中存在ConstantValue属性,那么在准备阶段就会被初始化为ConstantValue属性所指定的值。假设:
public static final int value = 123;
编译时Javac将会为value生成Constant属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
解析
是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析阶段发生的具体时间,只要求在执行anewarray、checkcat、getfield、getstatic、instanceof、invokeinterface、invokespcial、invoketatic、invokecirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前,先对他们所使用的符号引用进行解析。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。
类或接口的解析
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析的过程需要包括以下3个步骤:
1) 如果C不是一个数组类型,那虚拟机将会把代表N的权限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又将可能触发其它相关类的加载动作,例如加载这个类的父类或实现的接口。一旦出现任何异常将宣告失败。
2) 如果C是一个数组类型,并且数组的元素类型为对象,也就是N描述符会类似”[Ljava.lang.Integer”的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象。
3) 如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经称为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认C是否具备对D的访问权限。如果发现不具备访问权限,将抛出IllegalAccessError异常。
字段解析
首先将会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析失败。如果解析成功,拿奖这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索:
1) 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
2) 否则,如果在C中实现了接口,将会按照继承关系从上往下递归搜索各个接口和它的父接口,如果找到则返回这个字段的直接引用,查找结束。
3) 否则,如果C不是Object话,将会按照继承关系从上往下递归搜索其父类,如果在父类中包含,则查找结束。
4) 否则查找失败。抛出NoSuchFieldError异常。
如果有一个同名字段同时出现的C的接口和父类中,或者同时出现在自己或父类的多个接口中出现,那边一起将拒绝编译。
类方法解析
首先解析出所属的类,如果解析成功,用C表示该类,虚拟机会进行如下步骤:
1) 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,直接抛异常。
2) 在类C中查找是否有简单名称和描述符斗鱼目标相匹配的方法,如果有则返回这个方法的直接引用,结束。
3) 否则,在类C的父类中查找。
4) 在类C实现的接口列表及他们的父接口之中查找是否有简单名称和描述符都与目标相匹配的方法,如果匹配,说明类是一个抽象类,抛出异常。
5) 否则,抛出NoSuchMethodError。
最后进行权限验证,如果不具备权限,抛出非法访问异常。
接口方法解析
解析所属的类或接口的符号引用,用C表示这个接口:
1) 如果在接口方法表中发现class_index项中的索引C是个类而不是接口,那就直接跑异常。
2) 否在在C中查找方法。
3) 否则在C的父接口中递归查找直到Object类。
4) 否则宣告失败。
由于接口中的所有方法都是默认是public的,所以不存在访问权限问题。
初始化
是类加载的最后一个阶段,前面的类加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正执行类中定义的Java程序代码(或者说字节码)
程序初始化阶段是执行类构造器方法的过程。
/**
* 类加载器与instanceof关键字演示
*
* @author zzm
*/
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("org.fenixsoft.classloading.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest);
}
}
运行结果:
class org.fenixsoft.classloading.ClassLoaderTest
false
由结果可以看出,检查所属类型时,返回了false,原因是在虚拟机中存在了两个ClassLoaderTest类,一个是由系统应用程序类加载器加载的,另外一个是由我们自定义的类加载器加载的,虽然都来同一个Class文件,但依然是两个独立的类。
双亲委派模型
站在Java虚拟机的角度讲,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类ClassLoader。
从Java开发人员的角度看,类加载器还可以分的更细致一点,绝大部分Java程序都会使用到一下三种系统提供的类加载器:
类加载器之间的关系:
java中采用双亲委派模型(Parents Delegation Model)来实现类的加载模式。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,此处的父子关系不以继承来实现,而是采用组合来利用父加载器。
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会自己去加载。
双亲委派模型的实现:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 则说明父类加载器无法完成加载请求
}
if (c == null) {
// 父类加载器无法加载对象的时候
// 在调用本身的findClass方法来进行类的加载
long t1 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
标签:
原文地址:http://blog.csdn.net/chun0801/article/details/51902731