深入理解Java虚拟机-7.3 类加载的过程- 高飞网

7.3 类加载的过程

2017-02-17 10:45:16.0

7.3.1 加载

    在加载阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口 。

    虚拟机规范的这三点要求实际上并不具体,因此虚拟机实现与具体应用的灵活度相当大。例如“通过一个类的全限定名获取定义此类的二进制字节流”,并没有指明二进制字节流要从一个Class文件中获取,准备地说是根本没有指明要从哪里获取及怎样获取。因此许多举足轻重的Java技术都建立在这一基础之上:

  1. 从ZIP包中读取。如jar、ear、war格式包。
  2. 从网络中获取。如Applet。
  3. 运行时计算生成。使用最多的就是动态代理技术,在java.lang.reflect中,就用了ProxyGenerator.generateProxyClass来为特定接口生成"$Proxy"的代理类的二进制字节流。
  4. 由其他文件生成。如Jsp应用。
  5. 从数据库中读取。这种场景相对少见些,有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。

    相比于类加载的其他阶段,加载阶段(尤其是获取类的二进制流的动作)是开发期可按性最强的阶段,因为加载阶段既可以使用系统提供的类加载器完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式

7.3.2 验证

    验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。虚拟机规范中对这一步限制和指导很笼统,不同的虚拟机对类验证的实现可能会有所不同,但大致上都会完成下面四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

7.3.3 准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
    public static int value = 123;
    变量value在准备阶段过后初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段都会被执行。特殊情况:如果类字段的字段属性表中存在常量(ConstantValue)属性,在准备阶段变量value就会被初始化为常量所指定的值,假设上面类变量value的定义变为:
    public static final int value = 123;

7.3.4 解析

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。

7.3.5 初始化

    类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户用程序可以通过自定义类加载器参与之外,其余动作完全是由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java代码。

    在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程

  1. <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。
  2. <clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
  3. 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
    public class InitTest{
        static class Parent{
            public static int A = 1;
            static{
                A = 2;
            }   
        }
        static class Sub extends Parent{
            public static int B = A;
        }
        public static void main(String[] args){
            System.out.println(Sub.B);
        }
    }
  4. <clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  5. 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。只有当父类接口定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
  6. 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。如:
    public class DeadLoopClassTest{
        static class DeadLoopClass{
            static{
                //如果不加上这个if语句,编译器将提示"initializer must be able to complete normally"并拒绝编译
                if(true){
                    System.out.println(Thread.currentThread()+"init DeadLoopClass");
                    while(true){}
                }
            }
    
        }
        public static void main(String[] args){
            Runnable r = new Runnable(){
                public void run(){
                    System.out.println(Thread.currentThread()+" start");
                    DeadLoopClass dlc = new DeadLoopClass();
                    System.out.println(Thread.currentThread()+" run over");
                }
            };
            Thread t1 = new Thread(r);
            Thread t2 = new Thread(r);
            t1.start();
            t2.start();
        }
    }
    运行结果为: