概述
本章内容: 虚拟机如何加载这些class文件? Class文件的信息进入到虚拟机后会发生什么变化?
虚拟机吧描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
运行期动态加载和动态连接
类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括下图几个阶段。
从加载、验证、准备、初始化和卸载的顺序是确定的,类加载过程必须按照这种顺序按部就班的开始。而解析阶段不一定:某情况下可以在初始化阶段之后在开始这是为了支持java语言的运行时绑定(也成为动态绑定或晚期绑定)
什么情况下需要开始类加载过程的第一个阶段:加载吗?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把我。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5中情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
1、遇到new、getstatic、putstatic或 invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被finall修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
3、当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
5、当使用jdk1.7的动态语言支持是,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
“有且只有”这5中场景中的行为成为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,成为被动引用。
package com.imooc.concurrent;
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value=123;
}
class SubClass extends SuperClass{
static {
System.out.println("subClass init!");
}
}
class NotInitialization{
public static void main(String[] args) {
//对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字
//只有触发父类的初始化而不会触发子类的初始化
//至于是否要触发子类的加载和验证,在虚拟机规范中并未明确规定,这点取决于虚拟机的具体实现
//对于Sun Hotspot虚拟机来说,可以使用-XX:+TraceClassLoading参数观察到此操作会导致
//子类的加载。
System.out.println(SubClass.value); //指挥输出Superclass init和123
//不会输出 subclass init
//SuperClass[] cas=new SuperClass[10];
}
}
package com.imooc.concurrent;
public class ConstClass {
static {
System.out.println("constclass init");
}
public static final String HELLOWORLD="hello world";
}
class notfind{
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
运行结果:
结果分析: 虽然在java源码中引用了constClass类中的常量HELLOWORD,但其实在编译阶段通过常量传播优化,已经将此常量的“hello world”存储到了notfind类的常量池中,以后notfind对常量ConstClass.HELLOWORLD的引用实际上都被转化为notifind类对自身常量池的引用了,也就是说,实际上notfind的Class文件中并没有ConstClass类的符号音符口,这两个类在编译成Class之后就不存在任何联系了。
类加载的过程
加载
“加载”是“类加载(Class Loading)”过程的一个阶段。在加载阶段,虚拟机需要完成以下3件事情:
1、通过一个类的全限定名来获取定义此类的二进制字节流
2、将这个字节流锁掉膘的静态存储结构转化为方法区的运行时的数据结构
3、在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口
一个数组类创建过程遵循以下规则:
1、如果数组的组件类型(Component type,指的是数组去掉一个维度的类型)是引用类型,那就地柜采用本节中定义的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识(重要,一个类必须与类加载器一起确定唯一性)
2、如果数组的组件类型不是引用类型(例如int[] 数组),java虚拟机将会把数组C标记为与引导类加载器关联
3、数组类的可见性与它的组件类型的可见性一直,如果组件类型不是引用类型,那数组的类的可见性将默认为public。
加载阶段完成之后,虚拟机外部的二进制字节流就按照虚拟机锁需的格式存储在方法区之中,方法区中的数据存储格式化由虚拟机实现自定义,虚拟机规范未规定此区域的具体数据结构。然后再内存中实例化一个java.lang.class类的对象(存放在方法区中),这个对象将作为程序访问方法区中的这些类型数据的外部接口。
加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的。加载尚未完成,连接阶段可能已经开始,但这些夹在加载阶段进行的动作,任然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
验证
厅是连接阶段的第一步,这一阶段的目的是为哦了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
验证是虚拟机自身保护的一项重要工作。
验证阶段非常重要,这个阶段是否严谨直接决定了Jave虚拟机是否能承受恶意代码的攻击。
从执行性能来讲,验证阶段的工作量在虚拟机的类加载子系统总占了相当大一部分
如果验证到输入的字节流不符合Class文件格式的约束,虚拟机就会抛出一个java.lang.VerifyError异常或其子类异常
验证大致会完成:文件格式验证、元数据验证、字节码验证、符号引用验证
1、文件格式验证
第一阶段就要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理这一阶段可能包括以下验证点:
①是否以魔数0xCAFEBABE开头
②主、次版本号是否在当前虚拟机处理范围之内
③常量池的常量中是否有不被支持的常量类型(检验常量tag标志)
④ 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
⑤CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
⑥Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
… 还有很多这只是一小部分
2、源数据验证
第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。这个阶段肯恩更包括的验证点:
① 这个类是否有父类(除了java.lang.object,所有的类都应当有父类)
②这个类的父类是否继承了不允许继承的类(被final修饰的类)
③如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
④类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
这一阶段主要目的是对类的源数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。
3、字节码验证
本阶段是验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。 在第二阶段对源数据信息做了校验后,本解读那将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
① 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
②保证调整转指令不会跳转到方法体以为的字节码指令上
③保证方法体中的类型转换是有效的。
4、符号引用验证
最后一个阶段校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段——解析阶段中发生。符号引用验证可以看做是对类自身以为(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验一下内容:
① 符号殷勇忠通过字符串描述的全限定名是否能找到对应的类
②在指定类中是否存在符合方法的字段描述以及简单名称锁描述的方法和字段
③ 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
符号殷勇验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSuchFiledError,java.lang.NoSuchMethodError等。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量锁使用的内存都将在方法区中进行分配。
这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中
解析
①符号引用(Symbolic References):符号引用以一组符号来描述锁引用的目标。可以使以任何形式的字面量。和虚拟机的内存布局无关
② 直接引用(Direct References):直接引用可以使直接指向目标的指针、相对偏移量或是一个能简介定位到目标的句柄。、
动态调用点限定符:
1、类或接口的解析
当前代码所处的类D,如果要把一个从未接戏过的符号引用N解析为一个类或接口C的直接引用,虚拟机完成解析过程需要5个步骤:
①、C不是一个数组类型,那虚拟机会拔代表N的全限定名传递给D的类加载器去加载这个类C。类加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口,一旦这个加载过程出现了任何异常,解析过程就宣告失败
②C是一个数组类型,并且数组的元素类型为对象,N的描述符会是类似“【Ljava/lang/Integer”的形式,那将会按照第一点的规则加载数组元素类型。如果N的描述符如前面锁假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象。
③如果上面步骤没有出现任何异常,那么C在虚拟机中实际上已经成了一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具备对C的访问权健,如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常
2、字段解析
3、类方法解析
首先应该解析出类方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用c表示,接下来虚拟机将会按照如下步骤进行后续的类方法搜索。
① 类方法和接口方法符号引用的常量类型定义是分开的,如果在类风阀表中发现class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
②如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
③否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有就返回这个方法的直接引用,查找结束
④否则,在类C实现的接口列表及它们的父接口之中递归查找是否具有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这是查找结束,抛出java.lang.AbstractMethodError异常。
⑤否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备此方法的访问权限,将抛出java.lang.IllegalAccessError异常,
4、接口方法解析
接口方法也需要先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行接口方法搜索:
①与类方法解析不同,如果在接口方法中发现class_index中的索引C是一个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常
②否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,付过有则返回这个方法的直接引用,查找结束
③否则,在接口C的服接口中递归查找,知道java.lang.Object类(查找范围会包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法。
④否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError
由于接口中的所有方法默认都是public的,所以不存在访问权限的问题,因此接口的符号解析应当不会抛出java.lang.IllegalAccessError异常
初始化
类初始化是类加载过程的最后一步
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序定制的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达
类加载器
类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己确定如何去获取锁需要的类。实现这个动作的代码模块称为“类加载器”
类层次划分、OSGi、热部署、代码加密等领域
类和类加载器
类加载器虽然只用于实现类的加载动作,但它在java程序中的作用却远远不限于类加载阶段。
package com.imooc.concurrent;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myloader=new ClassLoader() {
@Override
public ClassloadClass(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 object=myloader.loadClass("com.imooc.concurrent.ClassLoaderTest").newInstance();
System.out.println(object.getClass());
System.out.println(object instanceof ClassLoaderTest);
}
}
结果
原因:因为虽然都是classloadertest类,一个是通过代码new的,一个是通过类加载器加载的,虽然都来源于同一个class文件,但是依然是两个独立的类,做对象所属类型检查时结果自然为false。
双亲委派模型
java虚拟机角度,只存在两种不同的类加载器:一种是穷类加载器(bootstrap ClassLoader)(C++实现,虚拟机自身的一部分)
另一种:其他的类加载器由java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
从java开发人员的角度来看,类加载器还可以划分的更加细致,绝大部分java程序都会用到一下3中系统提供的类加载器:
启动类加载器(Bootstrap Classloader):将放在lib目录中的,或者被-Xbootclassjpath参数锁指定的路径中的,并且虚拟机识别的(仅按照文件名识别)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接是用null代替即可。
扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExitClassLoader实现,它负责加载libext目录中的,或者被java.ext.dirs系统变量锁指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。
双亲委派模型
破坏双亲委派模型
第一次被破坏是因为jdk1.2之前,由于双亲委派模型在JDK1.2之后才引入,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0就已经存在,面对已经存在的用户自定义类加载器的实现代码,java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK1.2之后的java.lang.classloader添加了一个新的protected和findclass()方法
第二次“被破坏”是由于模型本身的缺陷所导致。
第三四是由于用户对程序动态性的追求而导致的。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(OSGi中成为bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换
OSGi按照下面顺序来进行类搜索:
1、将以java.*开头的类委派给父类加载器加载
2、否则,将委派列表名单内的类委派给父类加载器加载
3、否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载
4、否则,查找当前Bunle的ClassPath,使用自己的类加载器加载
5、否则,查找类收费在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
6、否则,查找Dynamic Import列表的Bundel,委派给对应的Bundle,委派给对应Bundle的类加载器加载
7、否则,类查找失败