虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
虚拟机的类加载机制是在运行时期完成的,这种策略会增加一些运行时的性能开销,但是会给java程序提供高的灵活性,java动态扩展的语言特性就是通过运行时动态加载和动态链接实现的。
类加载的过程
类从被加载到虚拟机开始到卸载出内存为止,生命周期一共包含以下几个阶段:
加载、验证、准备、解析、初始化、使用、卸载
其中验证、准备、解析、初始化、称为连接阶段
- 加载
类加载阶段虚拟机需要完成以下三件事情
a. 通过类的全限定名来获取此类的二进制字节流
b. 将字节流代表的静态存储结构转化为方法区的运行时数据结果
c. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口 - 验证
验证阶段是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成下面4个阶段的检验动作:
a. 文件格式校验 b.元数据验证 c字节码验证 d.符号引用验证 - 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
注意:这里初始化的是类(static)变量,而不是实例变量。实例变量是在对象实例化时在堆中分配的。这里的初始值为变量的默认值如int会初始化为0,boolean会初始化为false - 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程 - 初始化
到了初始化阶段,才真正执行类中定义的java程序代码,根据程序员的意愿对类变量进行初始化,如static int a =123;在准备阶段a的值为0,在初始化后a的值为123.
其中解析阶段可以在初始化阶段的之前或之后执行。其他阶段必须严格按照顺序执行。
类加载时机
虚拟机规范严格规定,以下情况必须对类进行初始化:
- 遇到new、getstatic、putstatic、invokestatic时必须对类进行初始化
- 使用java.lang.reflect包的方法对类进行反射的时候,必须初始化
- 初始化一个类的时候,如果父类没有初始化,则需要先初始化父类
- 当虚拟机启动时,main函数所在的类必须初始化
- 如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
类加载器
虚拟机设计团队把类加载阶段中,“通过一个类的全限定名来获取此类的二进制字节流”这个动作放到虚拟机的外部实现,以便应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
类与类加载器
对于任何一个类,都需要加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每个类加载器都拥有一个独立的命名空间。
这里的相等,包括代表类的Class对象的equals方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括instanceof对象所属关系的判断。
双亲委派模型
从java虚拟机的角度来讲,只存在两种不同的加载器:
1.一种是启动类加载器(Bootstrap ClassLoader)这个类加载器使用C++语言实现,是虚拟机的一部分
2.另一种是其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部。并且全部继承自抽象类java.lang.ClassLoader.
从java开发人员的角度看,还可以分的更细一些:
- 启动类加载器(Bootstrap ClassLoader):这个负责将存放在JAVAHOME/lib目录中,或者被Xbootclasspath参数指定的路劲中,可以被虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委托给引导类加载器,直接使用null代替即可。
- 扩展类加载器(Extension ClassLoader)这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JAVA_HOME/lib/ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
- 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器时ClassLoader中的getSystemClassLoader方法的返回值,所以一般称为系统类加载器,它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器。
- 如果有必要开发人员可以定义自己的类加载器。
这些加载器的关系如下图所示
双亲委派模型工作过程:
如果一个类加载器收到类的加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
破坏双亲委派模型
双亲委派模型并不是一个强制性的约束模型,而是java设计者推荐给开发者的类加载器实现方式,java中大部分的类加载器遵循这个模型,但是也有例外,例如OSGI中,类加载器不是双亲模式中的树形结构,而是复杂的网状结构。
OSGI在java中最著名的应用案例就是Eclipse IDE。
OSGI中每个模块与普通的Java类库区别并不太大,两者一般都以JAR格式进行封装,并且内部存储的都是Java Package和class,但是一个Boundle 可以声明它所依赖的JavaPackage,也可以声明它所导出的package,供外界访问,其他的package和class将会隐藏起来。除了精确的模块划分和可见性控制外,引入OSGI的另外一个重要理由是,基于OSGI的程序可以实现模块级的热插拔功能。