Java高并发编程详解系列-类加载

之前在写关于JVM的时候提到过类加载机制,类加载机制也是在Java面试中被经常问道的一个问题,在这篇博客中就来了解一下关于类加载的知识。

类加载

  在JVM执行Java程序的时候实际上执行的编译好的class 文件,我们知道Java语言的夸平台特性其实实际上是由不同平台的虚拟机来完成的,那么整个JVM又是怎样执行这些操作的呢?就不得不提一个类加载问题,在不同平台的机器上可以运行同样的Class文件,这就说明JVM是提供了一套统一的调用过程而这个过程就是类加载过程。

类加载过程

  首先类加载过程分为三个大的阶段,在这个三个阶段执行不同的操作。
Java高并发编程详解系列-类加载_第1张图片
1. 加载阶段
  主要负责查找并且加载类的二进制文件,也就是class文件。
2. 连接阶段
a. 验证 :确保类的完整性
b. 准备 :为类中的变量分配内存,初始化默认值
c.解析 :把符号引用变为直接引用。
3. 初始化阶段
  将类的静态变量赋予正确的初始值

  当JVM在使用java 命令执行class文件之后会执行的过程就是这些了。那是不是每个类都需要执行初始化操作呢呢?不是。JVM对类的初始化是有一个懒加载的机制,也就是说在这个类首次被使用的时候才会被初始化,对于这个问题,在后面我们在细说。介绍的下一个概念就是关于类的加载使用问题。

类的主动加载和被动加载

  在Java虚拟机规范中提到,每个类或者是接口被Java程序首次使用的时候整个类才会被初始化。当然这个不包括动态编译实现的那些操作。

类主动加载的场景

  1. 通过new关键字初始化对象时。
  2. 通过类名直接访问静态变量或者静态方法的时候
  3. 通过反射操作获取到类对象的时候
  4. 初始化子类的时候导致父类初始化
  5. 如果这个类作为启动类来使用调用Main函数时

也就是说除了上面所列举出来的场景,其他出现的场景都是被动加载
例如

  1. 构造某个对象数组的时候
  2. 使用类的静态常量不会导致类的初始化,因为常量的存储在常量池中等等操作
单例模式详解类加载过程
public class Singleton {

    private static int a = 0;

    private static int b ;

    private static Singleton instance = new Singleton();

    private Singleton(){
        a++;
        b++;
    }

    public static Singleton getInstance(){
        return instance;
    }

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

分析上面代码不难发现,这个就是一个饿汉式的单例实现方式。那么上面代码的输出结果是什么呢?为什么会出现这样的效果呢?如果我们对于代码发生变化之后又会是什么样的结果呢!
Java高并发编程详解系列-类加载_第2张图片

public class Singleton {

    private static Singleton instance = new Singleton();

    private static int a = 0;

    private static int b ;
    
    private Singleton(){
        a++;
        b++;
    }

    public static Singleton getInstance(){
        return instance;
    }

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

Java高并发编程详解系列-类加载_第3张图片
会发现第二次的结果和第一次的结果是不一样的。那么怎么会出现这样的问题呢?
如果将代码改为下面的样子会有什么结果

public class Singleton {

    private static int a = 0;

    private static int b ;

    //private static Singleton instance = new Singleton();
    private static Singleton instance;

    private Singleton(){
        a++;
        b++;
    }

    public static Singleton getInstance(){
        if (instance !=null){
            instance = new Singleton();
        }
        return instance;
    }

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

Java高并发编程详解系列-类加载_第4张图片

会发现这种代码执行的效果是跟之前的结果又是不一样的。如果将代码改为如下的样子

public class Singleton {

    static {
        System.out.println("执行了静态代码块");
    }

    private static int a = 1;

    private static int b  ;

    //private static Singleton instance = new Singleton();
    private static Singleton instance;

    private Singleton(){
        System.out.println("执行初始化方法");
        a++;
        b++;
    }

    public static Singleton getInstance(){
        System.out.println("对象实例开始创建");
        if (instance !=null){
            System.out.println("类对象没有被初始化");
            instance = new Singleton();
        }
        System.out.println("创建对象完成");
        return instance;
    }

    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton.a);
        System.out.println(singleton.b);
        System.out.println("=====通过类名直接调用====");
        System.out.println(Singleton.a);
        System.out.println(Singleton.b);
    }
}

Java高并发编程详解系列-类加载_第5张图片

会发现四种方式每种方式的结果都是不一样的,那么为什么会出现上面的四种方式的不同呢?

类加载阶段

  类加载就是将二进制的class文件读取到内存中,将字节流表示的静态结构转换为运行时的数据结构,并在内存中生成一个java.lang.Class对象,作为访问方法区数据结构的入口。
Java高并发编程详解系列-类加载_第6张图片
  类加载的最终产物就是在堆内存中的class对象,对于同一个类加载器来说,无论多少次同一个类加载到堆内存中,所产生的对象都是同一个。在使用的时候通过类的全类名来获取二进制数据流。那么二进制数据流的来源都有哪些呢?

  1. 运行时动态生成,例如使用动态代理来生成代理类二进制字节流。
  2. 通过网络获取二进制流
  3. 通过Zip文件获取,或者通过Jar包获取
  4. 将二进制文件存储到数据库中,从数据库中进行获取
  5. 使用动态加载的方式获取到二进制数据流

在类加载的第一个阶段,所有的二进制数据都会被加载到内存中,并没有形成实际的可以操作的数据结构。

类的连接阶段

1.验证

  在不同JVM上所使用的class文件规范是不一样的,验证就是为了保证这个class文件是符合对应JVM的规范的。如果不符合对应的虚拟机规范的字节流出现的时候就抛出VerifyError的异常。
  作为验证来说需要验证的信息有以下的几个

  1. 验证文件格式。对于文件来说每个文件都有自己的格式,例如.exe,.txt,.doc.等等都有一个决定文件类型的头信息,对于我们虚拟机需要执行class文件来说也是有头信息的0xCAFEBABE,在Class文件中还包含了主次版本号例如在我们使用高版本的JDK编译完成Class文件之后,使用低版本的JDK去执行,就会出现小版本好不兼容的问题,也就是在类加载验证的时候出现了超过JDK检查范围的情况。
  2. 元数据验证,在学习编译原理时候有个概念就是词法分析和语义分析,对于类加载验证来说其实就是JVM对于java语言的本身的一个语法分析和语义分析的过程。通过语法分析和语义分析,了解父类信息,了解final,了解抽象等等信息。
  3. 字节码验证,通过语义分析过程之后要对接收到的字节码进行进一步的验证,这个验证就包括了对于Java基本语法的验证,包括对于分支、循环等以及程序计数器的指令执行过程、数据类型转换、是否满桌子happen-before原则等等。
  4. 符号引用验证,在类加载结束阶段,就是对于所有的符号引用的验证,也就是说对于public、protected、private等等一些权限的验证,这个也是保证了动作的解析执行顺序,例如会有出现ClassNotFind的异常等等。

当然对于类加载验证来说并不只是做这些工作,对于我们需要加载的Class文件来源是比较广泛的,所以说class的验证也是包含了很多的东西,但是最为主要的就是保证程序运行。

2.准备
  当一个类经过验证过程之后,也就是说它符合Java语言规范以及JVM的规范。这样的话它就可以被准备变量的分配对应的虚拟机内存了,当然我们知道在Java中如果你对一个变量没有进行赋值的话,到这一步也会对其进行默认的赋值。
3.解析
  经过验证和准备两个阶段之后,对于Java程序所需要的内存结构和需要的数据进行了准备,解析过程就是通过上面两个过程得到的引用进行重新的组装,形成将符号引用换成直接引用的过程。可以使用javap -verbose + class 文件的方式进行查看内容

F:\developersrc\GIT\JavaHighConcurrency\JavaHC\target\classes\com\example\charp09>javap -verbose Singleton.class
Classfile /F:/developersrc/GIT/JavaHighConcurrency/JavaHC/target/classes/com/example/charp09/Singleton.class
  Last modified 2019-5-10; size 1214 bytes
  MD5 checksum 68d4b078c523db7729b46aeeb6cfb2af
  Compiled from "Singleton.java"
public class com.example.charp09.Singleton
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #17.#40        // java/lang/Object."":()V
   #2 = Fieldref           #41.#42        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #43            // 执行初始化方法
   #4 = Methodref          #44.#45        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Fieldref           #10.#46        // com/example/charp09/Singleton.a:I
   #6 = Fieldref           #10.#47        // com/example/charp09/Singleton.b:I
   #7 = String             #48            // 对象实例开始创建
   #8 = Fieldref           #10.#49        // com/example/charp09/Singleton.instance:Lcom/example/charp09/Singleton;
   #9 = String             #50            // 类对象没有被初始化
  #10 = Class              #51            // com/example/charp09/Singleton
  #11 = Methodref          #10.#40        // com/example/charp09/Singleton."":()V
  #12 = String             #52            // 创建对象完成
  #13 = Methodref          #10.#53        // com/example/charp09/Singleton.getInstance:()Lcom/example/charp09/Singleton;
  #14 = Methodref          #44.#54        // java/io/PrintStream.println:(I)V
  #15 = String             #55            // =====通过类名直接调用====
  #16 = String             #56            // 执行了静态代码块
  #17 = Class              #57            // java/lang/Object
  #18 = Utf8               a
  #19 = Utf8               I
  #20 = Utf8               b
  #21 = Utf8               instance
  #22 = Utf8               Lcom/example/charp09/Singleton;
  #23 = Utf8               <init>
  #24 = Utf8               ()V
  #25 = Utf8               Code
  #26 = Utf8               LineNumberTable
  #27 = Utf8               LocalVariableTable
  #28 = Utf8               this
  #29 = Utf8               getInstance
  #30 = Utf8               ()Lcom/example/charp09/Singleton;
  #31 = Utf8               StackMapTable
  #32 = Utf8               main
  #33 = Utf8               ([Ljava/lang/String;)V
  #34 = Utf8               args
  #35 = Utf8               [Ljava/lang/String;
  #36 = Utf8               singleton
  #37 = Utf8               <clinit>
  #38 = Utf8               SourceFile
  #39 = Utf8               Singleton.java
  #40 = NameAndType        #23:#24        // "":()V
  #41 = Class              #58            // java/lang/System
  #42 = NameAndType        #59:#60        // out:Ljava/io/PrintStream;
  #43 = Utf8               执行初始化方法
  #44 = Class              #61            // java/io/PrintStream
  #45 = NameAndType        #62:#63        // println:(Ljava/lang/String;)V
  #46 = NameAndType        #18:#19        // a:I
  #47 = NameAndType        #20:#19        // b:I
  #48 = Utf8               对象实例开始创建
  #49 = NameAndType        #21:#22        // instance:Lcom/example/charp09/Singleton;
  #50 = Utf8               类对象没有被初始化
  #51 = Utf8               com/example/charp09/Singleton
  #52 = Utf8               创建对象完成
  #53 = NameAndType        #29:#30        // getInstance:()Lcom/example/charp09/Singleton;
  #54 = NameAndType        #62:#64        // println:(I)V
  #55 = Utf8               =====通过类名直接调用====
  #56 = Utf8               执行了静态代码块
  #57 = Utf8               java/lang/Object
  #58 = Utf8               java/lang/System
  #59 = Utf8               out
  #60 = Utf8               Ljava/io/PrintStream;
  #61 = Utf8               java/io/PrintStream
  #62 = Utf8               println
  #63 = Utf8               (Ljava/lang/String;)V
  #64 = Utf8               (I)V
{
  public static com.example.charp09.Singleton getInstance();
    descriptor: ()Lcom/example/charp09/Singleton;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #7                  // String 对象实例开始创建
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: getstatic     #8                  // Field instance:Lcom/example/charp09/Singleton;
        11: ifnull        32
        14: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        17: ldc           #9                  // String 类对象没有被初始化
        19: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        22: new           #10                 // class com/example/charp09/Singleton
        25: dup
        26: invokespecial #11                 // Method "":()V
        29: putstatic     #8                  // Field instance:Lcom/example/charp09/Singleton;
        32: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        35: ldc           #12                 // String 创建对象完成
        37: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        40: getstatic     #8                  // Field instance:Lcom/example/charp09/Singleton;
        43: areturn
      LineNumberTable:
        line 30: 0
        line 31: 8
        line 32: 14
        line 33: 22
        line 35: 32
        line 36: 40
      StackMapTable: number_of_entries = 1
        frame_type = 32 /* same */

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: invokestatic  #13                 // Method getInstance:()Lcom/example/charp09/Singleton;
         3: astore_1
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: aload_1
         8: pop
         9: getstatic     #5                  // Field a:I
        12: invokevirtual #14                 // Method java/io/PrintStream.println:(I)V
        15: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: aload_1
        19: pop
        20: getstatic     #6                  // Field b:I
        23: invokevirtual #14                 // Method java/io/PrintStream.println:(I)V
        26: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        29: ldc           #15                 // String =====通过类名直接调用====
        31: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        34: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        37: getstatic     #5                  // Field a:I
        40: invokevirtual #14                 // Method java/io/PrintStream.println:(I)V
        43: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        46: getstatic     #6                  // Field b:I
        49: invokevirtual #14                 // Method java/io/PrintStream.println:(I)V
        52: return
      LineNumberTable:
        line 40: 0
        line 41: 4
        line 42: 15
        line 43: 26
        line 44: 34
        line 45: 43
        line 46: 52
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      53     0  args   [Ljava/lang/String;
            4      49     1 singleton   Lcom/example/charp09/Singleton;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #16                 // String 执行了静态代码块
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: iconst_1
         9: putstatic     #5                  // Field a:I
        12: return
      LineNumberTable:
        line 13: 0
        line 16: 8
}
SourceFile: "Singleton.java"

通过上面分析我们可以看到,其实在JVM中虚拟机规范规定了一些符号引用的操作指令,anewarray、checkcast、getfield、getstatic、instanceof、invokeinterface、invokespecial、invokerstatic、invokevirtual、multianewarray、new、putfield、putstatic等。这些都是操作符号引用的命令,在执行符号引用的字节码指令之前,必须要对所有的符号引用提前解析。解析的过程主要针对类接口、字段、类方法、和接口方法四种类型的数据进行操作。分别使用的常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、Constant_Methodref_info和Constant_InterfaceMethodred_info这四中类型的常量。

  1. 接口类解析
    也就是对于类的解析,各种类结构的解析,包括数组、集合等数据结构,在内存中使用合适的方式进行内存的分配
  2. 字段解析
    在接口或者类中定义了很多的字段,当需要解析对应的类或者接口的时候对应的字段不存在就会出现异常。例如没有找到对应的属性异常
  3. 类方法解析
    类方法可以直接使用类对象进行调用,完成对于类的解析的时候就已经完成了解析。
  4. 接口方法解析
    与类方法不同的是接口不能做对象的实例化操作,所以只能等到对应的类实现进行解析的时候才会被解析。同样也会有类方法所出现的问题。
类的初始化阶段

  经过的千辛万苦终于到了类的初始化阶段,也就是我们上面抛出的第一个问题,为什么四个结果是不一样的。在这之前首先要澄清一个事实,就是这里所说的初始化方法和构造函数是有所不同的,在Java虚拟机中会保证父类的init方法是最先执行,所以说父类中的静态变量最先被赋值。但是这个与双亲委派机制还是有点不一样的。双亲委派机制是对于类加载来说的,与类的初始化并没有关系,下面的例子才是与类的初始化相关的,希望不要混淆两个概念。例如

public class Test {

    //在父类中与静态变量value
    static class Parent{
        static  int value = 10;
        static {
            value = 20;
        }
    }

    static class Child extends Parent{
        static int i = value;
    }

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

使用javap -verbose Test.class命令

F:\developersrc\GIT\JavaHighConcurrency\JavaHC\target\classes\com\example\charp09>javap -verbose Test.class
Classfile /F:/developersrc/GIT/JavaHighConcurrency/JavaHC/target/classes/com/example/charp09/Test.class
  Last modified 2019-5-11; size 669 bytes
  MD5 checksum 8f63ac13577d305756bdf9898531cf75
  Compiled from "Test.java"
public class com.example.charp09.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#25         // java/lang/Object."":()V
   #2 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Fieldref           #7.#28         // com/example/charp09/Test$Child.i:I
   #4 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #5 = Class              #31            // com/example/charp09/Test
   #6 = Class              #32            // java/lang/Object
   #7 = Class              #33            // com/example/charp09/Test$Child
   #8 = Utf8               Child
   #9 = Utf8               InnerClasses
  #10 = Class              #34            // com/example/charp09/Test$Parent
  #11 = Utf8               Parent
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               Lcom/example/charp09/Test;
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               args
  #22 = Utf8               [Ljava/lang/String;
  #23 = Utf8               SourceFile
  #24 = Utf8               Test.java
  #25 = NameAndType        #12:#13        // "":()V
  #26 = Class              #35            // java/lang/System
  #27 = NameAndType        #36:#37        // out:Ljava/io/PrintStream;
  #28 = NameAndType        #38:#39        // i:I
  #29 = Class              #40            // java/io/PrintStream
  #30 = NameAndType        #41:#42        // println:(I)V
  #31 = Utf8               com/example/charp09/Test
  #32 = Utf8               java/lang/Object
  #33 = Utf8               com/example/charp09/Test$Child
  #34 = Utf8               com/example/charp09/Test$Parent
  #35 = Utf8               java/lang/System
  #36 = Utf8               out
  #37 = Utf8               Ljava/io/PrintStream;
  #38 = Utf8               i
  #39 = Utf8               I
  #40 = Utf8               java/io/PrintStream
  #41 = Utf8               println
  #42 = Utf8               (I)V
{
  public com.example.charp09.Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/example/charp09/Test;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: getstatic     #3                  // Field com/example/charp09/Test$Child.i:I
         6: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 25: 0
        line 26: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
}
SourceFile: "Test.java"
InnerClasses:
     static #8= #7 of #5; //Child=class com/example/charp09/Test$Child of class com/example/charp09/Test
     static #11= #10 of #5; //Parent=class com/example/charp09/Test$Parent of class com/example/charp09/Test

特别注意者几行名

#1 = Methodref          #6.#25         // java/lang/Object."":()V
#6 = Class              #32            // java/lang/Object
#25 = NameAndType        #12:#13        // "":()V
#12 = Utf8               <init>
#13 = Utf8               ()V

实际上在类加载过程中Java虚拟机会自动调用一个初始化的方法
分析其子类

F:\developersrc\GIT\JavaHighConcurrency\JavaHC\target\classes\com\example\charp09>javap -verbose Test$Child.class
Classfile /F:/developersrc/GIT/JavaHighConcurrency/JavaHC/target/classes/com/example/charp09/Test$Child.class
  Last modified 2019-5-11; size 490 bytes
  MD5 checksum ce5998b840029681a2c3a6ff986350cb
  Compiled from "Test.java"
class com.example.charp09.Test$Child extends com.example.charp09.Test$Parent
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#20         // com/example/charp09/Test$Parent."":()V
   #2 = Fieldref           #4.#21         // com/example/charp09/Test$Child.value:I
   #3 = Fieldref           #4.#22         // com/example/charp09/Test$Child.i:I
   #4 = Class              #24            // com/example/charp09/Test$Child
   #5 = Class              #25            // com/example/charp09/Test$Parent
   #6 = Utf8               i
   #7 = Utf8               I
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Child
  #15 = Utf8               InnerClasses
  #16 = Utf8               Lcom/example/charp09/Test$Child;
  #17 = Utf8               <clinit>
  #18 = Utf8               SourceFile
  #19 = Utf8               Test.java
  #20 = NameAndType        #8:#9          // "":()V
  #21 = NameAndType        #27:#7         // value:I
  #22 = NameAndType        #6:#7          // i:I
  #23 = Class              #28            // com/example/charp09/Test
  #24 = Utf8               com/example/charp09/Test$Child
  #25 = Utf8               com/example/charp09/Test$Parent
  #26 = Utf8               Parent
  #27 = Utf8               value
  #28 = Utf8               com/example/charp09/Test
{
  static int i;
    descriptor: I
    flags: ACC_STATIC

  com.example.charp09.Test$Child();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method com/example/charp09/Test$Parent."":()V
         4: return
      LineNumberTable:
        line 20: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/example/charp09/Test$Child;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #2                  // Field value:I
         3: putstatic     #3                  // Field i:I
         6: return
      LineNumberTable:
        line 21: 0
}
SourceFile: "Test.java"
InnerClasses:
     static #14= #4 of #23; //Child=class com/example/charp09/Test$Child of class com/example/charp09/Test
     static #26= #5 of #23; //Parent=class com/example/charp09/Test$Parent of class com/example/charp09/Test

注意以下的命令的调用

#1 = Methodref          #5.#20         // com/example/charp09/Test$Parent."":()V
#5 = Class              #25            // com/example/charp09/Test$Parent
#25 = Utf8               com/example/charp09/Test$Parent
#20 = NameAndType        #8:#9          // "":()V
#8 = Utf8               <init>
#9 = Utf8               ()V

  从这里可以看出,子类在调用的过程中是父类的初始化方法先得到了执行,所以说调用的i的值是20而不是子类的10。但是这个过程中并不是所有的class都会被创建init方法,例如一个类中既没有静态代码块,也没有静态变量,那么就没有初始化的必要了,在接口中也是一样的,对于一个接口来说本身就不支持这样的操作所以说只有在被继承并且有变量被初始的时候才会生成init方法。
  对于init方法来说在虚拟机中是真是存在的。多个线程对于同一个对象使用init方法的时候会不会引起线程安全问题。

public class Init {
    static {
        try{
            System.out.println("The init static code block will be invoke.");
            TimeUnit.MINUTES.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        IntStream.range(0,5).forEach(i->new Thread(Init::new));
    }
}

  上面代码中运行结果显示同一时间内只有一个线程执行了静态代码块中的内容,并且保证了静态代码块只会被执行一次,也就是说在多线程中是单实例的。保证了多线程同步操作。

结果不一样实现的分析
第一种方式实现分析
public class TestSingleton {

        private static int a = 0;

        private static int b ;

        private static TestSingleton instance = new TestSingleton();

        private TestSingleton(){
            a++;
            b++;
        }

        public static TestSingleton getInstance(){
            return instance;
        }

        public static void main(String[] args) {
            TestSingleton singleton = TestSingleton.getInstance();
            System.out.println(singleton.a);
            System.out.println(singleton.b);
        }
}
F:\developersrc\GIT\JavaHighConcurrency\JavaHC\target\classes\com\example\charp09>javap -verbose TestSingleton.class
Classfile /F:/developersrc/GIT/JavaHighConcurrency/JavaHC/target/classes/com/example/charp09/TestSingleton.class
  Last modified 2019-5-11; size 880 bytes
  MD5 checksum 3a736ae3e2f234962fc4d01bd06a0ca5
  Compiled from "TestSingleton.java"
public class com.example.charp09.TestSingleton
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #10.#32        // java/lang/Object."":()V
   #2 = Fieldref           #8.#33         // com/example/charp09/TestSingleton.a:I
   #3 = Fieldref           #8.#34         // com/example/charp09/TestSingleton.b:I
   #4 = Fieldref           #8.#35         // com/example/charp09/TestSingleton.instance:Lcom/example/charp09/TestSingleton;
   #5 = Methodref          #8.#36         // com/example/charp09/TestSingleton.getInstance:()Lcom/example/charp09/TestSingleton;
   #6 = Fieldref           #37.#38        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Methodref          #39.#40        // java/io/PrintStream.println:(I)V
   #8 = Class              #41            // com/example/charp09/TestSingleton
   #9 = Methodref          #8.#32         // com/example/charp09/TestSingleton."":()V
  #10 = Class              #42            // java/lang/Object
  #11 = Utf8               a
  #12 = Utf8               I
  #13 = Utf8               b
  #14 = Utf8               instance
  #15 = Utf8               Lcom/example/charp09/TestSingleton;
  #16 = Utf8               <init>
  #17 = Utf8               ()V
  #18 = Utf8               Code
  #19 = Utf8               LineNumberTable
  #20 = Utf8               LocalVariableTable
  #21 = Utf8               this
  #22 = Utf8               getInstance
  #23 = Utf8               ()Lcom/example/charp09/TestSingleton;
  #24 = Utf8               main
  #25 = Utf8               ([Ljava/lang/String;)V
  #26 = Utf8               args
  #27 = Utf8               [Ljava/lang/String;
  #28 = Utf8               singleton
  #29 = Utf8               <clinit>
  #30 = Utf8               SourceFile
  #31 = Utf8               TestSingleton.java
  #32 = NameAndType        #16:#17        // "":()V
  #33 = NameAndType        #11:#12        // a:I
  #34 = NameAndType        #13:#12        // b:I
  #35 = NameAndType        #14:#15        // instance:Lcom/example/charp09/TestSingleton;
  #36 = NameAndType        #22:#23        // getInstance:()Lcom/example/charp09/TestSingleton;
  #37 = Class              #43            // java/lang/System
  #38 = NameAndType        #44:#45        // out:Ljava/io/PrintStream;
  #39 = Class              #46            // java/io/PrintStream
  #40 = NameAndType        #47:#48        // println:(I)V
  #41 = Utf8               com/example/charp09/TestSingleton
  #42 = Utf8               java/lang/Object
  #43 = Utf8               java/lang/System
  #44 = Utf8               out
  #45 = Utf8               Ljava/io/PrintStream;
  #46 = Utf8               java/io/PrintStream
  #47 = Utf8               println
  #48 = Utf8               (I)V
{
  public static com.example.charp09.TestSingleton getInstance();
    descriptor: ()Lcom/example/charp09/TestSingleton;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #4                  // Field instance:Lcom/example/charp09/TestSingleton;
         3: areturn
      LineNumberTable:
        line 24: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: invokestatic  #5                  // Method getInstance:()Lcom/example/charp09/TestSingleton;
         3: astore_1
         4: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: aload_1
         8: pop
         9: getstatic     #2                  // Field a:I
        12: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
        15: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: aload_1
        19: pop
        20: getstatic     #3                  // Field b:I
        23: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
        26: return
      LineNumberTable:
        line 28: 0
        line 29: 4
        line 30: 15
        line 31: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  args   [Ljava/lang/String;
            4      23     1 singleton   Lcom/example/charp09/TestSingleton;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: iconst_0
         1: putstatic     #2                  // Field a:I
         4: new           #8                  // class com/example/charp09/TestSingleton
         7: dup
         8: invokespecial #9                  // Method "":()V
        11: putstatic     #4                  // Field instance:Lcom/example/charp09/TestSingleton;
        14: return
      LineNumberTable:
        line 12: 0
        line 16: 4
}
SourceFile: "TestSingleton.java"
第二种方式的实现分析
public class TestSingleton {

    private static TestSingleton instance = new TestSingleton();

    private static int a = 0;

    private static int b;


    private TestSingleton() {
        a++;
        b++;
    }

    public static TestSingleton getInstance() {
        return instance;
    }

    public static void main(String[] args) {
        TestSingleton singleton = TestSingleton.getInstance();
        System.out.println(singleton.a);
        System.out.println(singleton.b);
    }
}
F:\developersrc\GIT\JavaHighConcurrency\JavaHC\target\classes\com\example\charp09>javap -verbose TestSingleton.class
Classfile /F:/developersrc/GIT/JavaHighConcurrency/JavaHC/target/classes/com/example/charp09/TestSingleton.class
  Last modified 2019-5-11; size 880 bytes
  MD5 checksum ee8a547c6b9237352c3131fc4c777a4e
  Compiled from "TestSingleton.java"
public class com.example.charp09.TestSingleton
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #10.#32        // java/lang/Object."":()V
   #2 = Fieldref           #8.#33         // com/example/charp09/TestSingleton.a:I
   #3 = Fieldref           #8.#34         // com/example/charp09/TestSingleton.b:I
   #4 = Fieldref           #8.#35         // com/example/charp09/TestSingleton.instance:Lcom/example/charp09/TestSingleton;
   #5 = Methodref          #8.#36         // com/example/charp09/TestSingleton.getInstance:()Lcom/example/charp09/TestSingleton;
   #6 = Fieldref           #37.#38        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Methodref          #39.#40        // java/io/PrintStream.println:(I)V
   #8 = Class              #41            // com/example/charp09/TestSingleton
   #9 = Methodref          #8.#32         // com/example/charp09/TestSingleton."":()V
  #10 = Class              #42            // java/lang/Object
  #11 = Utf8               instance
  #12 = Utf8               Lcom/example/charp09/TestSingleton;
  #13 = Utf8               a
  #14 = Utf8               I
  #15 = Utf8               b
  #16 = Utf8               <init>
  #17 = Utf8               ()V
  #18 = Utf8               Code
  #19 = Utf8               LineNumberTable
  #20 = Utf8               LocalVariableTable
  #21 = Utf8               this
  #22 = Utf8               getInstance
  #23 = Utf8               ()Lcom/example/charp09/TestSingleton;
  #24 = Utf8               main
  #25 = Utf8               ([Ljava/lang/String;)V
  #26 = Utf8               args
  #27 = Utf8               [Ljava/lang/String;
  #28 = Utf8               singleton
  #29 = Utf8               <clinit>
  #30 = Utf8               SourceFile
  #31 = Utf8               TestSingleton.java
  #32 = NameAndType        #16:#17        // "":()V
  #33 = NameAndType        #13:#14        // a:I
  #34 = NameAndType        #15:#14        // b:I
  #35 = NameAndType        #11:#12        // instance:Lcom/example/charp09/TestSingleton;
  #36 = NameAndType        #22:#23        // getInstance:()Lcom/example/charp09/TestSingleton;
  #37 = Class              #43            // java/lang/System
  #38 = NameAndType        #44:#45        // out:Ljava/io/PrintStream;
  #39 = Class              #46            // java/io/PrintStream
  #40 = NameAndType        #47:#48        // println:(I)V
  #41 = Utf8               com/example/charp09/TestSingleton
  #42 = Utf8               java/lang/Object
  #43 = Utf8               java/lang/System
  #44 = Utf8               out
  #45 = Utf8               Ljava/io/PrintStream;
  #46 = Utf8               java/io/PrintStream
  #47 = Utf8               println
  #48 = Utf8               (I)V
{
  public static com.example.charp09.TestSingleton getInstance();
    descriptor: ()Lcom/example/charp09/TestSingleton;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #4                  // Field instance:Lcom/example/charp09/TestSingleton;
         3: areturn
      LineNumberTable:
        line 25: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: invokestatic  #5                  // Method getInstance:()Lcom/example/charp09/TestSingleton;
         3: astore_1
         4: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: aload_1
         8: pop
         9: getstatic     #2                  // Field a:I
        12: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
        15: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: aload_1
        19: pop
        20: getstatic     #3                  // Field b:I
        23: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
        26: return
      LineNumberTable:
        line 29: 0
        line 30: 4
        line 31: 15
        line 32: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  args   [Ljava/lang/String;
            4      23     1 singleton   Lcom/example/charp09/TestSingleton;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #8                  // class com/example/charp09/TestSingleton
         3: dup
         4: invokespecial #9                  // Method "":()V
         7: putstatic     #4                  // Field instance:Lcom/example/charp09/TestSingleton;
        10: iconst_0
        11: putstatic     #2                  // Field a:I
        14: return
      LineNumberTable:
        line 12: 0
        line 14: 10
}
SourceFile: "TestSingleton.java"

  通过对于上面两个例子的查看,可以看到,两种方式调用instance的位置和时机不同,导致了两次的结果不一样。
Java高并发编程详解系列-类加载_第7张图片

实际上两段代码的不同也只不过是调用 private static TestSingleton instance = new TestSingleton(); 方法的位置不同。但是结果相差还是很大的。

总结

结合实际的例子通过反编译查看class文件的方式了解了整个Java类加载过程中的细节性的问题,当然,在整个分析过程中都是围绕着类加载的三个阶段锁展开的。类加载的整个过程包括三个大的阶段和三个小阶段,三个大阶段包括加载连接初始化,小阶段包括,验证准备加解析。了解者三个过程,对于以后的面试以及工作会有很大的帮助。

你可能感兴趣的:(高并发,Java高并发,java,类加载)