之前在写关于JVM的时候提到过类加载机制,类加载机制也是在Java面试中被经常问道的一个问题,在这篇博客中就来了解一下关于类加载的知识。
在JVM执行Java程序的时候实际上执行的编译好的class 文件,我们知道Java语言的夸平台特性其实实际上是由不同平台的虚拟机来完成的,那么整个JVM又是怎样执行这些操作的呢?就不得不提一个类加载问题,在不同平台的机器上可以运行同样的Class文件,这就说明JVM是提供了一套统一的调用过程而这个过程就是类加载过程。
首先类加载过程分为三个大的阶段,在这个三个阶段执行不同的操作。
1. 加载阶段
主要负责查找并且加载类的二进制文件,也就是class文件。
2. 连接阶段
a. 验证 :确保类的完整性
b. 准备 :为类中的变量分配内存,初始化默认值
c.解析 :把符号引用变为直接引用。
3. 初始化阶段
将类的静态变量赋予正确的初始值
当JVM在使用java 命令执行class文件之后会执行的过程就是这些了。那是不是每个类都需要执行初始化操作呢呢?不是。JVM对类的初始化是有一个懒加载的机制,也就是说在这个类首次被使用的时候才会被初始化,对于这个问题,在后面我们在细说。介绍的下一个概念就是关于类的加载使用问题。
在Java虚拟机规范中提到,每个类或者是接口被Java程序首次使用的时候整个类才会被初始化。当然这个不包括动态编译实现的那些操作。
类主动加载的场景
也就是说除了上面所列举出来的场景,其他出现的场景都是被动加载
例如
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);
}
}
分析上面代码不难发现,这个就是一个饿汉式的单例实现方式。那么上面代码的输出结果是什么呢?为什么会出现这样的效果呢?如果我们对于代码发生变化之后又会是什么样的结果呢!
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);
}
}
会发现第二次的结果和第一次的结果是不一样的。那么怎么会出现这样的问题呢?
如果将代码改为下面的样子会有什么结果
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);
}
}
会发现这种代码执行的效果是跟之前的结果又是不一样的。如果将代码改为如下的样子
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);
}
}
会发现四种方式每种方式的结果都是不一样的,那么为什么会出现上面的四种方式的不同呢?
类加载就是将二进制的class文件读取到内存中,将字节流表示的静态结构转换为运行时的数据结构,并在内存中生成一个java.lang.Class对象,作为访问方法区数据结构的入口。
类加载的最终产物就是在堆内存中的class对象,对于同一个类加载器来说,无论多少次同一个类加载到堆内存中,所产生的对象都是同一个。在使用的时候通过类的全类名来获取二进制数据流。那么二进制数据流的来源都有哪些呢?
在类加载的第一个阶段,所有的二进制数据都会被加载到内存中,并没有形成实际的可以操作的数据结构。
1.验证
在不同JVM上所使用的class文件规范是不一样的,验证就是为了保证这个class文件是符合对应JVM的规范的。如果不符合对应的虚拟机规范的字节流出现的时候就抛出VerifyError的异常。
作为验证来说需要验证的信息有以下的几个
当然对于类加载验证来说并不只是做这些工作,对于我们需要加载的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这四中类型的常量。
经过的千辛万苦终于到了类的初始化阶段,也就是我们上面抛出的第一个问题,为什么四个结果是不一样的。在这之前首先要澄清一个事实,就是这里所说的初始化方法和构造函数是有所不同的,在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的位置和时机不同,导致了两次的结果不一样。
实际上两段代码的不同也只不过是调用 private static TestSingleton instance = new TestSingleton(); 方法的位置不同。但是结果相差还是很大的。
结合实际的例子通过反编译查看class文件的方式了解了整个Java类加载过程中的细节性的问题,当然,在整个分析过程中都是围绕着类加载的三个阶段锁展开的。类加载的整个过程包括三个大的阶段和三个小阶段,三个大阶段包括加载连接初始化,小阶段包括,验证准备加解析。了解者三个过程,对于以后的面试以及工作会有很大的帮助。