虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。与那些在编译时需要进行连接工作的语言不用,在java里,类型的加载连接和初始化过程都是在程序运行期间完成的。这种策略虽然会令令加载时稍微增加一些性能开销,但是会为java应用程序提供高度的灵活性,java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的。如编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类。
类加载的机制
类的整个生命周期包括:加载,连接(包括验证,准备,解析),初始化,使用和卸载。
在上面的全部过程中,除了解析阶段,其他阶段必须按顺序开始(但不一定顺序进行或完成,通常是互相交叉地混合式进行的)。解析阶段在某些情况下可以在初始化阶段之后再开始,这是为了支持java语言的运行时绑定(也称为动态绑定或晚期绑定)。
Java虚拟机规范中规定有且只有对类进行主动引用时,才会触发类的初始化。主动引用包含5中情形。被动引用不会触发类的初始化,如以下情况:
(1) 通过子类调用其继承于父类的静态字段(SubClass.val),只有父类会被初始化。
对于静态的字段,只会初始化直接定义该字段的类。
(2) 通过数组定义来引用类,不会触发类的初始化。
(3) 常量在编译阶段会存入调用类(注意:非定义类,通过常量传播优化传播进来的)的常量池中,本质上并没有直接引用到定义常量的类。在编译阶段通过常量传播优化,定义常量的类中的常量池会被传播到调用类中。编译完之后定义常量的类和调用类已经没有关系了,在调用类中通过定义类来引用常量实际上是对调用类自身常量池的引用。
类加载过程
加载
加载主要是把类加载到内存中,主要有三个步骤,分别是:通过全限定名定位字节码文件;将类的静态存储结构转换为方法区的运行时数据结构;在内存中生成一个Class对象。
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息复合当前虚拟机的要求,并且不会危害虚拟机自身的安全。虽然java语言是相对安全的语言,但是Class文件不一定是用java源码编译而来。所以必须进行安全性检查。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量。同时,这里所说的初始值通常是数据类型的零值,也就是默认值。当然也有特殊情况,那就是常量,常量会在准备阶段就赋值。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
类初始化阶段是类加载过程的最后一步,前面的类夹杂过程中,除了在加载阶段用户可以通过自定义加载器参与之后,其余动作完全有虚拟机主导。到了初始化阶段才真正开始执行类中定义的Java程序代码。类初始化实际上是类构造器()的执行过程(注意这里不是构造函数)。
类加载器
虚拟机设计团队把类加载过程中的“通过一个类的全限定名来获取描述类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让程序自己决定如何去获取需要的类。实现这个动作的代码模块称为类加载器。类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在虚拟机中的唯一性。也就是说,比较两个类是否相等,只有在这两个类都是由同一个类加载器加载的前提下才有意义,否则即使是同一个Class文件,只要不是由同一个类加载器加载,那这两个类就必定不相同。
双亲委派模型
从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(BootStrap ClassLaoder),这个类加载器使用c++实现,是虚拟机自身的一部分;另一个就是所有其他类的类加载器,这些类加载器都是由Java语言实现的,独立于虚拟机外部,并继承自抽象类java.lang.ClassLoader。
从开发人员角度看,绝不部分Java程序会使用到一下3种系统提供的类加载器。
(1) 启动类加载器。
这个类加载器负责将存放在jre的lib目录下的或者被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不合符的类库即使放在lib目录中也不会被加载)类加载到虚拟机中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null即可。
(2) 扩展类加载器(Ext ClassLoader)
这个加载器负责加载lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
(3) 应用程序加载器(Application ClassLoader)
这个类加载时ClassLoader中getSystemClassLoader()方法的返回值,所以一般也称为系统类加载器。负责加载用户类路径ClassPath上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下就是这个就是程序中默认的类加载器。
如果有必要,还可以自定义类加载器。类加载器之间的关系为:
类加载器的这种层次关系称为类的双亲委派模型。双亲委派模型要求除了顶层启动类加载器外,其余的类加载器都应该有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会去尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有当父类加载器自己无法完成这个加载请求时,子加载器才会尝试自己去加载。所以即使自己写一个java.lang.String类,可以编译但是无法加载,因为父类加载器已经加载了该类,再次尝试加载时会直接返回已经加载的系统的String类。