九、JAVA多线程:类的加载过程

         ClassLoader的主要职责就是负责加载各种class文件到jvm中,ClassLoader是一个抽象的class,给定一个class的二进制文件名,ClassLoader会尝试加载并且在JVM中生成构成这个类的各个数据结构,然后使其分布在JVM对应的内存区域中.

类的加载过程简介

分为三个比较大的阶段,分别是加载阶段,连接阶段和初始化阶段.

1️⃣、加载阶段:主要负责查找并且加载类的二进制数据文件,其实就是class文件

2️⃣、连接阶段(三部分):

     1.验证: 主要是确保类文件的正确性,比如class版本,class文件的魔术因子是否正确.

     2.准备: 为类的静态变量分配内存,并且为其初始化默认值.

     2.解析: 把类中的符号引用转换为直接引用

3️⃣、初始化阶段: 为类的静态变量赋予正确的初始值(代码编写阶段给定的值).

      JVM对类的初始化是一个延迟的机制,即使用的是lazy的方式,当一个类在首次使用的时候才会被初始化,在同一个运行时包下,一个Class只会被初始化一次,在同一个运行包下,一个Class只会被初始化一次.

类的主动使用和被动使用

JVM虚拟机规范规定了,每个类或者接口被Java程序首次主动使用的时候,才会对其进行初始化。

随着JIT技术越来越成熟,JVM运行期间的编译也越来越智能,不排除JVM在运行期间提前预判并且初始化某个类。

6种主动使用类的场景:

    1.通过new关键字会导致类的初始化(最常用)

 


    2.访问类的静态变量,包括读取和更新会导致类的初始化

 


    3.访问类的静态方法,会导致初始化

 


    4.对某个类进行反射操作,也会导致类的初始化

 


    5.初始化子类会导致父类的初始化 , 通过子类使用父类的静态变量只会导致父类的初始化,子类不会被初始化。

public class AcitveLoadTest {

    public static void main(String[] args) {
        System.out.println(Child.y);
    }

}



class Parent {
    public static int y = 10 ;
    static {

        System.out.println("The parent is initialized ....");

    }
}

class Child extends Parent {

    static {

        System.out.println("The child is initialized ....");

    }

    public static int x = 10 ;
}

    6.启动类: 执行main函数所在的类会导致该类的初始化


两种被动使用类的场景:

     1.构造某个类的数组时并不会导致该类的初始化


     2.引用类的静态常量不会导致类的初始化

类的加载过程详解

思考 代码输出结果:

ublic class Singleton {

    private static Singleton singleton = new Singleton();

    private static int x = 0 ;

    private static  int y ;



    private  Singleton(){
        x++ ;
        y++ ;
    }

    public static Singleton getInstance(){
        return singleton;
    }

    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton.x);
        System.out.println(singleton.y);
    }
}

代码输出结果:

0

1

类加载阶段

       类加载器将class文件中的二进制数据读取到内存之中,将该字节流锁代表的静态存储结构转换为方法区中运行时的数据结构

并在堆内存中生成一个该类的java.lang.Class对象,作为访问方法区数据结构的入口。

类的加载同一个全限定名:通过包名+类名来获取二进制流

除此之外:

              运行时动态生成 (代理: java.lang.Proxy)

              通过网络获取 

              通过读取zip文件获取类的二进制字节流,比如jar,war

             将类的二进制数据存储在数据库的BLOB字段类型

             运行时生成class文件,并且动态加载

        这里所说的加载是指类加载过程还在弄的第一个阶段,并不代表着整个类已经加载完成了,在某个类完成加载阶段之后。

虚拟机会将这些二进制字节流按照虚拟机所需要的格式存储在方法区中,然后按照特定的数据结构,

随之在堆内存中实例化一个java.lang.Class对象,在类加载的整个生命周期中,加载过程还没有结束

连接阶段是可以交叉工作的,比如连接阶段验证字节流信息的合法性,

但是总体来讲,加载阶段肯定是出现在连接阶段之前。

类的连接阶段

1.验证

       确保class文件的字节流锁包含的内容符合当前的jvm的规范要求,并且不会出现危害jvm自身安全的代码,当字节流的信息不符合要求时,则会抛出VerifyError这样的一异常或者子异常

(1)验证文件格式:

      验证文件头部的魔术因子,该因子决定了这个文件到底是什么类型,class文件的魔术因子是0XCAFEBFBE.

      主次版本号,查看当前的class文件版本是否符合jdk所处理的范围.

      构成文件的字节流是否存在残缺或者其他附加信息,主要查看class的MD5指纹.

     常量池中的常量是否存在不被支持的变量类型,比如int64

     指向常量中的引用收付知道了不存在的常量或者该常量的类型不被支持 .

 (2)元数据验证

元数据的验证其实是对class的字节流进行语义分析的过程,整个语义分析就是为了确保class字节流符合JVM规范的要求.

    1.检查这个类是否存在父类,是否继承了某个接口,这些父类和接口是否合法,或者是否真实存在.

    2.检查改类收付继承了被final修饰的类,被final修饰的类是不允许继承并且期中的方法是不允许被override的

    3.检查该类是否为抽象类,如果不是抽象类,是否实现了父类的抽象方法或者接口中的所有方法

    4.检查方法重载的合法性,比如相同的方法名称,相同的参数,但是返回类型不同,这都是不被允许的

    5.其他语义验证

(3)字节码验证

       主要验证程序的控制流程、比如循环、分支

       1.保证当前线程在程序计数器中的指令不会跳转到不合法的字节码指令当中。

       2.保证类的转换是合法的,A声明的引用,不能使用B进行强制类型转换。

       3.任意时刻,虚拟机占中的操作栈类型与指令代码都能正确的被执行,比如压栈的时候,传入的是A类型的引用,使用的时候却将B类型载入本地变量表。

 (4).符号引用验证

            主要作用就是验证符号引用转换为直接引用时的合法性

            通过符号引用描述的字符串全限定名称是否能够顺利的找到相关的类

           符号引用中的类.字段,方法,收付对当前类可见,比如不能访问引用类的私有方法

           其他验证

           符号引用的验证目的是为了保证解析动作的顺利进行,比如,如果某个类的字段不存在,则会抛出NosuchfieldError,若该方法不存在时,则抛出nosuchmethoderroe等,我们在使用反射的时候也会遇到这样的异常信息.
 

准备

       为对象的类变量也是就是静态变量,分配内存并且设置初始值了,类变量的内存会被分配到方法区内,不同实例变量会分配到堆内存中

        所谓初始值,其实就是为相应的类变量给定一个相关类型在没有被设置值时的默认值.

解析

所谓解析就是在常量池中寻找类,接口,字段和方法的符号引用,并且将这些符号引用替换成直接引用的过程.

          虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。

          1.类、接口的解析          CONSTANT_Class_Info

          2.字段解析          CONSTANT_Fieldref_Info

          3.类方法解析          CONSTANT_Methodef_Info

          4.接口方法解析          CONSTANT_InterfaceMethoder_Info

类的初始化阶段:

       class initialize:

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

类初始化是类加载过程的最后一个阶段,到初始化阶段,才真正开始执行类中的 Java 程序代码。虚拟机规范严格规定了有且只有四种情况必须立即对类进行初始化:

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类还没有进行过初始化,则需要先触发其初始化。生成这四条指令最常见的 Java 代码场景是:使用 new 关键字实例化对象时、读取或设置一个类的静态字段(static)时(被 static 修饰又被 final 修饰的,已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法时。
  • 使用 Java.lang.refect 包的方法对类进行反射调用时,如果类还没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类。

虚拟机规定只有这四种情况才会触发类的初始化,称为对一个类进行主动引用,除此之外所有引用类的方式都不会触发其初始化,称为被动引用。下面举一些例子来说明被动引用。

通过子类引用父类中的静态字段,这时对子类的引用为被动引用,因此不会初始化子类,只会初始化父类:

class Father{  
    public static int m = 33;  
    static{  
        System.out.println("父类被初始化");  
    }  
}  

class Child extends Father{  
    static{  
        System.out.println("子类被初始化");  
    }  
}  

public class StaticTest{  
    public static void main(String[] args){  
        System.out.println(Child.m);  
    }  
}  

执行后输出的结果如下:

父类被初始化
    33

对于静态字段,只有直接定义这个字段的类才会被初始化,因此,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

常量在编译阶段会存入调用它的类的常量池中,本质上没有直接引用到定义该常量的类,因此不会触发定义常量的类的初始化:

class Const{  
    public static final String NAME = "我是常量";  
    static{  
        System.out.println("初始化Const类");  
    }  
}  

public class FinalTest{  
    public static void main(String[] args){  
        System.out.println(Const.NAME);  
    }  
}  

执行后输出的结果如下:

我是常量

虽然程序中引用了 const 类的常量 NAME,但是在编译阶段将此常量的值“我是常量”存储到了调用它的类 FinalTest 的常量池中,对常量 Const.NAME 的引用实际上转化为了 FinalTest 类对自身常量池的引用。也就是说,实际上 FinalTest 的 Class 文件之中并没有 Const 类的符号引用入口,这两个类在编译成 Class 文件后就不存在任何联系了。

通过数组定义来引用类,不会触发类的初始化:

class Const{  
    static{  
        System.out.println("初始化Const类");  
    }  
}  

public class ArrayTest{  
    public static void main(String[] args){  
        Const[] con = new Const[5];  
    }  
}  

执行后不输出任何信息,说明 Const 类并没有被初始化。

但这段代码里触发了另一个名为“LLConst”的类的初始化,它是一个由虚拟机自动生成的、直接继承于java.lang.Object 的子类,创建动作由字节码指令 newarray 触发,很明显,这是一个对数组引用类型的初初始化,而该数组中的元素仅仅包含一个对 Const 类的引用,并没有对其进行初始化。如果我们加入对 con 数组中各个 Const 类元素的实例化代码,便会触发 Const 类的初始化,如下:

class Const{  
    static{  
        System.out.println("初始化Const类");  
    }  
}  

public class ArrayTest{  
    public static void main(String[] args){  
        Const[] con = new Const[5];  
        for(Const a:con)  
            a = new Const();  
    }  
}  

这样便会得到如下输出结果:

初始化Const类

根据四条规则的第一条,这里的 new 触发了 Const 类。

最后看一下接口的初始化过程与类初始化过程的不同。

接口也有初始化过程,上面的代码中我们都是用静态语句块来输出初始化信息的,而在接口中不能使用“static{}”语句块,但编译器仍然会为接口生成类构造器,用于初始化接口中定义的成员变量(实际上是 static final 修饰的全局常量)。

二者在初始化时最主要的区别是:当一个类在初始化时,要求其父类全部已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量),才会初始化该父接口。这点也与类初始化的情况很不同,回过头来看第 2 个例子就知道,调用类中的 static final 常量时并不会 触发该类的初始化,但是调用接口中的 static final 常量时便会触发该接口的初始化。

本文来源于:

《Java高并发编程详解:多线程与架构设计》 --汪文君

猜你喜欢

转载自blog.csdn.net/zhanglong_4444/article/details/86138581