虚拟机类加载机制

来源:互联网 发布:易视云手机客户端软件 编辑:程序博客网 时间:2024/06/05 09:03

0 概述

类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。本文讲述虚拟机是如何加载这些Class文件,最终形成可以被虚拟机直接使用的类型。

1 类加载原理

下图中给出了我们编写的java 源程序从编译到最终到运行的一个过程。从图中可以看出java程序编译生成.class 文件,然后再通过类装载器进行装载。那么java 虚拟机是如何加载这些class文件的呢?
java 编程环境
1.1 虚拟机如何加载class文件
类从被加载到虚拟机内存中开始,到卸载内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。

类加载过程

  • 加载
    在加载阶段,虚拟机需要完成以下三件事情
    1)通过一个类的全限定名来获取定义此类的二进制字节流
    2)将这个字节流所代表的静态存储结构转换为方方法区运行时结构
    3)在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。虚拟机规范中规定了有且只有5种情况必须立即对类进行类初始化(加载、验证、准备、解析自然需要在此之前开始):1)遇到了new,getstatic, put static, invokestatic字节码指令。2)java.lang.reflect包的方法对类进行反射调用。3)父类没初始化,先初始化父类(一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父类接口时候才初始化)。4)虚拟机启动时加载主类。5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化。除了上面提到之外,所有引用类的方式都不会触发类初始化,称为被动引用。

    被动引用实例分析

public class SuperClass {    static {        System.out.println("superClass init");    }    public static int value=12;}public class SubClass extends SuperClass {    static {        System.out.println("SubClass init");    }    public static int test=124;    public static final    String CONSTANT_VALUE="test";}public class StaticTest {    public static void main(String[] args) {        /*通过子类引用父类的静态字段,不会导致子类初始化         * 输出:         * superClass init         * 12         */        System.out.println(SubClass.value);        /**         * 通过数组定义来引用类,不会触发类初始化         */        SubClass[] subClassArray=new SubClass[100];        /**         * 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化         */        System.out.println(SubClass.CONSTANT_VALUE);    }}
  • 验证
    验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流符合当前虚拟机的要求并且不会危害虚拟机的自身安全。
    1)文件格式的验证
    2)元数据的验证
    3)字节码的验证
    4)符合引用的验证
    往往由于应用中包含了同一系列jar包的不同版本,引起jar包冲突,常常抛出:java.lang.NoSuchMethodError、java.lang.NoSuchFieldError等错误。使用如下命令查看:mvn dependency:tree -Dverbose
  • 准备
    准备阶段是正式为类变量(static 修饰的)分配内存并设置类变量的初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。值得强调是这个时候进行内存分配的仅包括类变量,不包括实例变量,实例变量将会在对象实例化时候随着对象一起分配在java 堆内存中;这里说的初始值“通常情况”下是数据类型的零值。
  /**     * 在准备阶段值为test值为0     */    public static int test=124;    /**     * 在准备阶段值为bool值为false     */    public static boolean bool=true;    /**     * 在准备阶段值为aLong值为null(reference 引用类型)     */    public static Long aLong=1L;
  • 解析
    解析阶段就是虚拟机将常量池内的符号引用替换为直接引用的过程。1)类或者接口的解析2)字段解析3)类方法解析4)接口方法解析

  • 初始化
    类初始化阶段时类加载过程的最后一步,初始化阶段才是真正开始执行字节码。在准备阶段变量已经赋过了一个零值,而在初始化阶段,则根据程序员定制的主观计划去初始化类变量和其它资源;初始化阶段就是执行<clinit>()方法的过程。
    <clinit>()方法是编译器自动收集类中所有变量的赋值动作和静态语句块中的语句合并产生的,编译器收集顺序是由语句在源文件中出现顺序所决定的。静态语句块中只能访问到定义在静态语句块之间的变量,定义在它之后的变量,在前面的的静态语句块只可以赋值、但是不能访问,见如下实例。

public class StaticTest {    static{        i=0;        //编译报错:非法的前向引用        System.out.println(i);    }    static int i=0;    public static void main(String[] args) {    }}

<clinit>() 方法与类的实例构造函数(<init>()方法)不同,他不需要显示的调用父类构造器,虚拟机会保证子类 <clinit>() 方法执行之前,父类的 <clinit>()已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
虚拟机会保证一个类 的<clinit>()方法在线程环境中被正确同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个<clinit>()方法,其它的线程需要阻塞等待,直到活动线程执行<clinit>()方法完毕。

public class Test {    public static Test test=new Test();    static {        System.out.println("static block");    }    public Test()    {        System.out.println("constructor test");    }    {        System.out.println("init");    }    public static String str="test";}public class StaticTest {    public static void main(String[] args) {        //触发Test类初始化        System.out.println(Test.str);    }}输出:initconstructor teststatic blocktest

输出结果分析:
执行:
1.System.out.println(Test.str); 触发Test类初始化,也就是执行<clinit>() 方法。
2.执行public static Test test=new Test();
这时候发现Test类已经存在(不会再触发初始化),会执行<init>()方法。
3.执行
{
System.out.println(“init”);

}
4.执行构造函数
5.执行静态语句块。

2 类加载器

类加载器虽然只用于实现类加载动作,但是它在java程序中起到的作用远远不限于类加载阶段。JVM判定两个class 是否相同,不仅要判断两个类名相同(全路径),而且还需要判断是否由一个类加载器加载,只有两者都满足的情况下,JVM才认为这个两个class 是相等的。由此也可以看出类加载器可有效实现隔离,可以定义不同的类加载器解决二方包冲突问题。

双亲委派模型
1.启动类加载器它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类
2.扩展类加载器主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库
3.应用类加载器 AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器

  • 双亲委派模型
    1.当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
    2.当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.
    3.当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

参考文献
[1] 深入理解java 虚拟机(第二版),周志明著

原创粉丝点击