JVM 之 (12) 类加载机制


1.概述

       Class文件由类装载器装载后,在JVM中将造成一份描述Class结构的元信息对象,经过该元信息对象能够获知Class的结构信息:如构造函数,属性和方法等,Java容许用户借由这个Class相关的元信息对象间接调用Class对象的功能。java

      虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终造成能够被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。数据库


2.工做机制

      类装载器就是寻找类的字节码文件,并构造出类在JVM内部表示的对象组件。在Java中,类装载器把一个类装入JVM中,要通过如下步骤:数组

     (1) 装载:查找和导入Class文件;安全

     (2) 连接:把类的二进制数据合并到JRE中;网络

        (a)校验:检查载入Class文件数据的正确性;数据结构

        (b)准备:给类的静态变量分配存储空间;多线程

        (c)解析:将符号引用转成直接引用;ide

     (3) 初始化:对类的静态变量,静态代码块执行初始化操做


函数


    Java程序能够动态扩展是由运行期动态加载和动态连接实现的;好比:若是编写一个使用接口的应用程序,能够等到运行时再指定其实际的实现(多态),解析过程有时候还能够在初始化以后执行;好比:动态绑定(多态);

3.装载

 在装载阶段,虚拟机须要完成如下3件事情
          (1) 经过一个类的全限定名来获取定义此类的二进制字节流

        (2) 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构oop

        (3) 在Java堆中生成一个表明这个类的java.lang.Class对象,做为方法区这些数据的访问入口。(不一样虚拟机机制不一样,hotsport把Class对象放在方法区中)

    虚拟机规范中并无准确说明二进制字节流应该从哪里获取以及怎样获取,这里能够经过定义本身的类加载器去控制字节流的获取方式,譬如:网络、动态生成、数据库等


4.验证

    验证是链接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
验证阶段大体会完成4个阶段的检验动做:
                (1) 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围以内、常量池中的常量是否有不被支持的类型。
                 (2) 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object以外。
                 (3) 字节码验证:经过数据流和控制流分析,肯定程序语义是合法的、符合逻辑的。
                 (4) 符号引用验证:确保解析动做能正确执行。
验证阶段是很是重要的,但不是必须的,它对程序运行期没有影响,若是所引用的类通过反复验证,那么能够考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

5.准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一块儿分配在堆中。其次,这里所说的初始值“一般状况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=123;
        那变量value在准备阶段事后的初始值为0而不是123.由于这时候还没有开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,因此把value赋值为123的动做将在初始化阶段才会执行。
至于“特殊状况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,因此标注为final以后,value的值在准备阶段初始化为123而非0.

6.解析

        解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info

        符号引用与虚拟机实现的布局无关,引用的目标并不必定要已经加载到内存中。各类虚拟机实现的内存布局能够各不相同,可是它们能接受的符号引用必须是一致的,由于符号引用的字面量形式明肯定义在Java虚拟机规范的Class文件格式中。
        直接引用能够是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。若是有了直接引用,那引用的目标一定已经在内存中存在。    


7.初始化

        初始化阶段是类加载最后一个阶段,前面的类加载阶段以后,除了在加载阶段能够自定义类加载器之外,其它操做都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。
        初始化阶段是执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量的赋值操做和静态语句块中的语句合并而成的。虚拟机会保证<client>方法执行以前,父类的<client>方法已经执行完毕。p.s: 若是一个类中没有对静态变量赋值也没有静态语句块,那么编译器能够不为这个类生成<client>()方法。

    <clinit>()方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块以前的变量,定义在它以后的变量,在前面的静态语句块能够赋值,可是不能访问。以下:
public class Test
{
    static
    {
        i=0;
        System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)
    }
    static int i=1;
}
<clinit>()方法与实例构造器<init>()方法不一样,它不须要显示地调用父类构造器,虚拟机会保证在子类<init>()方法执行以前,父类的<clinit>()方法方法已经执行完毕, 因为父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操做。以下:
public 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);     //2
    }
}
        <clinit>()方法对于类或者接口来讲并非必需的,若是一个类中没有静态语句块,也没有对变量的赋值操做,那么编译器能够不为这个类生产<clinit>()方法。
        接口中不能使用静态语句块,但仍然有变量初始化的赋值操做,所以接口与类同样都会生成<clinit>()方法。但接口与类不一样的是,执行接口的<clinit>()方法不须要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的<clinit>()方法。
        虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其余线程都须要阻塞等待,直到活动线程执行<clinit>()方法完毕。若是在一个类的<clinit>()方法中有好事很长的操做,就可能形成多个线程阻塞,在实际应用中这种阻塞每每是隐藏的。
public class DealLoopTest
{
    static class DeadLoopClass
    {
        static
        {
            if(true)
            {
                System.out.println(Thread.currentThread()+"init DeadLoopClass");
                while(true)
                {
                }
            }
        }
    }
 
    public static void main(String[] args)
    {
        Runnable script = new Runnable(){
            public void run()
            {
                System.out.println(Thread.currentThread()+" start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread()+" run over");
            }
        };
 
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main]init DeadLoopClass
须要注意的是,其余线程虽然会被阻塞,但若是执行<clinit>()方法的那条线程退出<clinit>()方法后,其余线程唤醒以后不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会初始化一次。
将上面代码中的静态块替换以下:
static
        {
            System.out.println(Thread.currentThread() + "init DeadLoopClass");
            try
            {
                TimeUnit.SECONDS.sleep(10);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-1,5,main]init DeadLoopClass (以后sleep 10s)
Thread[Thread-1,5,main] run over
Thread[Thread-0,5,main] run over
虚拟机规范严格规定了有且只有5中状况(jdk1.7)必须对类进行“初始化”(而加载、验证、准备天然须要在此以前开始):

      (1) 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,若是类没有进行过初始化,则须要先触发其初始化。生成这4条指令的最多见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

      (2) 使用java.lang.reflect包的方法对类进行反射调用的时候,若是类没有进行过初始化,则须要先触发其初始化。

      (3) 当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化。

      (4) 当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

      (5) 当使用jdk1.7动态语言支持时,若是一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,而且这个方法句柄所对应的类没有进行初始化,则须要先出触发其初始化。

只有上述这五种状况会触发初始化,也称为对一个类进行主动引用,除此之外,全部其余方式都不会触发初始化,称为被动引用.

注意如下几种状况不会执行类初始化

  • 经过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,本质上并无直接引用定义常量的类,不会触发定义常量所在的类。
  • 经过类名获取Class对象,不会触发类的初始化。
  • 经过Class.forName加载指定类时,若是指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  • 经过ClassLoader默认的loadClass方法,也不会触发初始化动做。
  • final变量不会触发此类的初始化,由于在编译阶段就存储在常量池中.
相关文章
相关标签/搜索