您的位置:首页 > 编程语言 > Java开发

java虚拟机类加载机制

2016-03-11 23:24 344 查看
Java虚拟机类加载器结构简述
JVM三种预定义类型类加载器

类装载器

类加载双亲委派机制

类的加载的步骤
1加载

2验证

准备

解析

初始化

1 Java虚拟机类加载器结构简述

JVM三种预定义类型类加载器

当一个 JVM启动的时候,Java缺省开始使用如下三种类型类装入器:

启动(Bootstrap)类加载器:

引导类装入器是用本地代码实现的类装入器,它负责将 /lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

扩展(Extension)类加载器:

扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

系统(System)类加载器:

系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。

当我们在命令行输入java Xxx(某个类)时候,java内部会做些什么动作呢?

流程如下:
1. 找到JRE;
2. 找到JVM.dll;
3. 启动JVM,并进行初始化;
4. 产生Bootstrap Loader;
5. 载入ExtClassLoader;(Ext – Extended)
6. 载入AppClassLoader;
7. 加载Xxx类。

类加载的时候遵循一个原则:“类加载器会依类的继承体系从上至下依次加载”。举个例子:“如果C继承了B并实现了接口I,而B有继承自A”,则类加载器在加载C时,加载的次序会是A B I C,(注:interface会如同class一样被Java编译器编译为独立的.class文件)

类装载器:

JVM使用一个类,首先将该类的字节码装载进来,类装载器负责根据一个类的名称来定位,并生成类的字节码数据后返回给JVM。

最常见的类加载器是将要加载的类名转换成一个.class文件名,然后从文件系统中找到该文件并读取其中的内容

这种类装载器也不是直接将.class文件中的内容原封不动地返回给java虚拟机,它需要将.class文件中的内容转换成Java虚拟机使用的类字节码,例如,Java程序中的字符串编译成.class文件后是以UTF-8编码存在的,而装载进java虚拟机后要被转换成Unicode编码。类装载器本身也是一个Java类,允许开发人员自己编写自己的类装载器,以便通过其他各种特殊方式来产生类字节码。

当一个类被加载后,JVM将其编译为可执行代码存储在内存中,并将索引信息存储进一个HashTable中,其索引关键字在HashTable中查找相应的信息,如果该代码已经存在,虚拟机直接从内存里调用该可执行代码,反之则调用类装载器并进行加载和编译。

类装载器装载某个类的字节码的过程实际上就是在创建Class类的一个实例对象,可以采用下面三种方式 获得:

1、类名.class , 例如:System.class

2、对象.getClass(), 例如:new Date().getClass()

3、Class.forName(“类名”), 例如:Class.forName(“java.util.Date”);

类装载器本身也是一个java类,java类库中提供了一个java.lang.ClassLoader来作为类装载器的基类,JVM和程序都调用ClassLoader的子类。Class类中定义了一个getClassLoader方法,用于返回它所描述的类的类装载器对象,这个返回对象的类型就是ClassLoader.

类加载双亲委派机制

JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。



我们可以判定系统类加载器(AppClassLoader)父加载器标准扩展类加载器(ExtClassLoader),但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,就是说标准扩展类加载器本身强制设定父类加载器为null如果父加载器为null,则会调用本地方法进行启动类加载(Bootstrap)尝试

分析java.lang.ClassLoader中的loadClass(String name)方法的代码就可以分析出虚拟机默认采用的双亲委派机制到底是什么模样:

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {

// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,
//通过调用本地方法native findBootstrapClass0(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}


类的加载的步骤:

加载 loading

验证 verification

准备 preparation

解析 resolution

初始化 initialization

有且只有以下四种情况必须立即对类进行”初始化”(称为对一个类进行主动引用):

遇到new、getstatic、putstatic、invokestatic这四条字节码指令时(使用new实例化对象的时候、读取或设置一个类的静态字段、调用一个类的静态方法)。

使用java.lang.reflet包的方法对类进行反射调用的时候。

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

当虚拟机启动时,虚拟机会初始化主类(包含main方法的那个类)

被动引用:

通过子类引用父类的静态字段,不会导致子类初始化(对于静态字段,只有直接定义这个字段的类才会被初始化)。

通过数组定义类应用类:ClassA [] array=new ClassA[10]。触发了一个名为[LClassA的类的初始化,它是一个由虚拟机自动生成的、直接继承于Object的类,创建动作由字节码指令newarray触发。

常量会在编译阶段存入调用类的常量池。

编译器会为接口生成< clinit>()构造器,用于初始化接口中定义的成员变量。一个接口在初始化时,并不要求其父类接口全部完成了初始化,只有在真正使用到父接口的时候才会初始化。

1、加载

查找路径,导入

1、类名.class , 例如:System.class

2、对象.getClass(), 例如:new Date().getClass()

3、Class.forName(“类名”), 例如:Class.forName(“java.util.Date”);

2、验证

验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

虚拟机规范:如果验证到输入的字节流不符合Class文件的存储格式,就抛出一个java.lang.VerifyError异常或其子类异常。

文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这个阶段的验证时给予字节流进行的,经过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储所以后面的验证阶段都是给予方法区的存储结构进行的。

元数据验证:对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。

字节码验证:进行数据流和控制流分析,对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。

符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),对常量池中的各种符号引用的信息进行匹配性的校验。

3. 准备

给类中的静态变量分配存储空间

public static final int value=122;

4. 解析

在虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

A. 类或接口(对应于常量池的CONSTANT_Class_info类型)的解析:

假设当前代码所处的类为D,需要将一个从未解析过的符号引用N解析为一个类或接口C的直接引用:

如果C不是一个数组类型,虚拟机将会把代表C的全限定名传递给D的类加载器去加载这个类。

如果C是一个数组类型,并且数组的元素类型为对象(N的描述符类似[Ljava.lang.Integer),将会加载数组元素类型(java.lang.Integer),接着由虚拟机生成一个代表此数组维度和元素的数组对象。

如果以上过程没有发生异常,则C在虚拟机中已经成为了一个有效的类和接口了,之后还要进行的是符号引用验证,确认D是否具有对C的访问权限,如果没有,将抛出java.lang.IllegalAccessError异常。

B. 字段(对应于常量池的CONSTANT_Fieldref_info类型)解析:

对字段表中的class_index项中索引的CONSTANT_Class_info符号引用进行解析。用C表示这个字段所属的类或接口。

如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。

否则,如果C实现了接口,则会按照继承关系从下往上递归搜索各个接口和他的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。

否则,如果C不是java.lang.Object类型的话,将会按照继承关系从下往上递归的搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。

否则,查找失败,抛出java.lang.NoSuchFieldError异常。

虚拟机的编译器实现可能会更严格:如果一个同名字段同时出现在C实现的接口和父类中,或者同时在自己或父类的多个接口中出现,编译器将可能拒绝编译。

C. 类方法(对应于常量池的CONSTANT_Methodref_info类型)解析:

对方法表中的class_index项中索引的CONSTANT_Class_info符号引用进行解析。用C表示这个方法所属的类或接口。

类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,则抛出java.lang.IncompatibleClassChangeError。

在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。

否则,在C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。

否则,在C实现的接口列表及它们的父接口中递归的查找是否有简单名称和描述符都与目标相匹配的方法,如果有说明C是个抽象类,查找结束,抛出java.lang.AbstractMethodError异常。

否则,查找失败,抛出java.lang.NoSuchMethodError异常。

如果查找返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对这个方法的访问权限,则抛出java.lang.IllegalAccessError异常。

D. 接口方法(对应于常量池的CONSTANT_InterfaceMethodref_info类型):

对方法表中的class_index项中索引的CONSTANT_Class_info符号引用进行解析。用C表示这个方法所属的类或接口。

如果在接口方法表中发现class_index中索引的C是个类,则抛出java.lang.IncompatibleClassChangeError。

否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。

否则,在接口C的父接口中递归查找,知道java.lang.Object类(包括在内),看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。

否则,查找失败,抛出java.lang.NoSuchMethodError。

5. 初始化

对静态变量和静态代码块执行初始化工作

1、< clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的。静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。

2、 方法与实例构造器< init>()不同,不需要显示的调用父类构造器,虚拟机会保证在子类的< clinit>()方法执行之前,父类的< clinit>()已经执行完毕。

3、 < clinit>()方法对于类或接口来说不是必须的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成< clinit>()方法。

 - 4、 执行接口的< clinit>()不需要先执行父接口的< clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。接口的实现类在初始化时也不会执行接口的< clinit>()方法。

5、 虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确的加锁和同步,如果多个线程同时去初始化一个类,则只会有一个线程去执行这个类的< clinit>()方法,其他线程需要阻塞等待。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: