出處:
Java位元組碼技術(一)static、final、volatile、synchronized關鍵字的位元組碼展現
static、final、volatile關鍵字
static:static修飾的變量被所有類執行個體共享,靜态變量在其所在類被加載時進行初始化,靜态方法中不能引用非靜态變量或函數
final:final修飾的變量不可修改(基本類型值不能修改,引用類型引用不可修改),final修飾的方法,不可重寫、不可繼承
volatile:volatile修飾的成員變量在每次被線程通路時,都從主記憶體中重新讀取該成員變量的值。而且,當成員變量發生變化時,強迫線程将變化值回寫到主記憶體
synchronized:Synchronized關鍵字就是用于代碼同步,用于控制多線程同步通路同一變量或方法
這些Java關鍵字的作用,大家或多或少都聽過,但是為什麼會有這種效果呢?本文從Java位元組碼層面做簡單分析
那麼什麼又是位元組碼呢?
什麼是位元組碼
Java之是以可以“一次編譯,到處運作”,一是因為JVM針對各種作業系統、平台都進行了定制,二是因為無論在什麼平台,都可以編譯生成固定格式的位元組碼(.class檔案)供JVM使用。是以,也可以看出位元組碼對于Java生态的重要性。之是以被稱之為位元組碼,是因為位元組碼檔案由十六進制值組成,而JVM以兩個十六進制值為一組,即以位元組為機關進行讀取。
.class檔案就是Java代碼編譯後産生的位元組碼檔案,看下具體執行個體

用Sublime Text以文本檔案打開,顯示如下
Javap指令檢視位元組碼檔案
先寫一段如下代碼,非常簡單
定義一個抽象類JavaTestController
變量a為靜态成員變量(int)
變量b為普通成員變量(int)
變量c為volatile修飾的變量(int)
變量d為final修飾的變量(String)
變量s為字元串(String)
變量o為Object類型(Object)
public abstract class JavaTestController {
public static int a = 1;
public int b = 2;
public volatile int c = 3;
public final int d = 4;
private String s = "5";
private Object o = new Object();
public void test() {
System.out.println("1");
}
}
那麼問題來了,文本形式看到.class檔案全是十六進制的代碼,有沒更人性化的展示呢?
javap是jdk自帶的反解析工具。它的作用就是根據class位元組碼檔案,反解析出目前類對應的code區(彙編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等等資訊
指令如下
javap -verbose class檔案路徑
看下這段代碼使用javap指令輸出的的位元組碼
Classfile /Users/chenyin/IdeaProjects/spring-boot-api-project-seed/target/classes/com/company/project/biz/controller/JavaTestController.class
Last modified 2019-9-19; size 883 bytes
MD5 checksum 9ac63f28ebe7c6a65dd6c5a12913e064
Compiled from "JavaTestController.java"
public abstract class com.company.project.biz.controller.JavaTestController
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT
Constant pool:
#1 = Methodref #7.#36 // java/lang/Object."<init>":()V
#2 = Fieldref #13.#37 // com/company/project/biz/controller/JavaTestController.b:I
#3 = Fieldref #13.#38 // com/company/project/biz/controller/JavaTestController.c:I
#4 = Fieldref #13.#39 // com/company/project/biz/controller/JavaTestController.d:I
#5 = String #40 // 5
#6 = Fieldref #13.#41 // com/company/project/biz/controller/JavaTestController.s:Ljava/lang/String;
#7 = Class #42 // java/lang/Object
#8 = Fieldref #13.#43 // com/company/project/biz/controller/JavaTestController.o:Ljava/lang/Object;
#9 = Fieldref #44.#45 // java/lang/System.out:Ljava/io/PrintStream;
#10 = String #46 // 1
#11 = Methodref #47.#48 // java/io/PrintStream.println:(Ljava/lang/String;)V
#12 = Fieldref #13.#49 // com/company/project/biz/controller/JavaTestController.a:I
#13 = Class #50 // com/company/project/biz/controller/JavaTestController
#14 = Utf8 a
#15 = Utf8 I
#16 = Utf8 b
#17 = Utf8 c
#18 = Utf8 d
#19 = Utf8 ConstantValue
#20 = Integer 4
#21 = Utf8 s
#22 = Utf8 Ljava/lang/String;
#23 = Utf8 o
#24 = Utf8 Ljava/lang/Object;
#25 = Utf8 <init>
#26 = Utf8 ()V
#27 = Utf8 Code
#28 = Utf8 LineNumberTable
#29 = Utf8 LocalVariableTable
#30 = Utf8 this
#31 = Utf8 Lcom/company/project/biz/controller/JavaTestController;
#32 = Utf8 test
#33 = Utf8 <clinit>
#34 = Utf8 SourceFile
#35 = Utf8 JavaTestController.java
#36 = NameAndType #25:#26 // "<init>":()V
#37 = NameAndType #16:#15 // b:I
#38 = NameAndType #17:#15 // c:I
#39 = NameAndType #18:#15 // d:I
#40 = Utf8 5
#41 = NameAndType #21:#22 // s:Ljava/lang/String;
#42 = Utf8 java/lang/Object
#43 = NameAndType #23:#24 // o:Ljava/lang/Object;
#44 = Class #51 // java/lang/System
#45 = NameAndType #52:#53 // out:Ljava/io/PrintStream;
#46 = Utf8 1
#47 = Class #54 // java/io/PrintStream
#48 = NameAndType #55:#56 // println:(Ljava/lang/String;)V
#49 = NameAndType #14:#15 // a:I
#50 = Utf8 com/company/project/biz/controller/JavaTestController
#51 = Utf8 java/lang/System
#52 = Utf8 out
#53 = Utf8 Ljava/io/PrintStream;
#54 = Utf8 java/io/PrintStream
#55 = Utf8 println
#56 = Utf8 (Ljava/lang/String;)V
{
public static int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public int b;
descriptor: I
flags: ACC_PUBLIC
public volatile int c;
descriptor: I
flags: ACC_PUBLIC, ACC_VOLATILE
public final int d;
descriptor: I
flags: ACC_PUBLIC, ACC_FINAL
ConstantValue: int 4
public com.company.project.biz.controller.JavaTestController();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_2
6: putfield #2 // Field b:I
9: aload_0
10: iconst_3
11: putfield #3 // Field c:I
14: aload_0
15: iconst_4
16: putfield #4 // Field d:I
19: aload_0
20: ldc #5 // String 5
22: putfield #6 // Field s:Ljava/lang/String;
25: aload_0
26: new #7 // class java/lang/Object
29: dup
30: invokespecial #1 // Method java/lang/Object."<init>":()V
33: putfield #8 // Field o:Ljava/lang/Object;
36: return
LineNumberTable:
line 8: 0
line 10: 4
line 11: 9
line 12: 14
line 13: 19
line 14: 25
LocalVariableTable:
Start Length Slot Name Signature
0 37 0 this Lcom/company/project/biz/controller/JavaTestController;
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #10 // String 1
5: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 17: 0
line 18: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/company/project/biz/controller/JavaTestController;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #12 // Field a:I
4: return
LineNumberTable:
line 9: 0
}
SourceFile: "JavaTestController.java"
大家肯定覺得很長,怎麼解讀呢?
位元組碼檔案解讀
每個位元組碼檔案都是按照如下格式産生的,下面逐個分析
1.魔數
這需要在文本模式的class檔案中檢視,固定的字元串“0XCAFEBABE”,辨別其是一個class檔案,CAFEBABE英文意為咖啡寶貝,與Java圖示對應
2.版本号
minor version: 0
major version: 52
對應文本中的
0034轉化到10進制就是52,52對應Java版本1.8
-
常量池
常量池中存儲兩類常量:字面量與符号引用。字面量為代碼中聲明為Final的常量值,符号引用如類和接口的全局限定名、字段的名稱和描述符、方法的名稱和描述符
整體分為:常量池計數器以及常量池資料區
先看常量池計數器:
002a辨別共有(57-1)=56個常量
對應到javap指令中的常量池,也是56個
再看常量池的資料區,即資料如何展示的
56個常量池資料項,以第一個為例做分析,即下面的資料是如何從16進制轉化而來的
#1 = Methodref #7.#36 // java/lang/Object."<init>":()V
先來看一個結構圖
這是Methodref的常量池資料線位元組碼分布圖,什麼意思呢?
即第一個位元組的16進制标志其tag為10,對應到下圖0a即辨別接下來的常量池tag=10,是methodref類型
接下來的2byte為指向聲明方法的描述符索引項
0007轉化到十進制也是7,即描述符下标為7,對應如圖,辨別其是個Object類型
最後2byte資料為指向名稱及類型描述符的索引項
0024轉化到10進制是36,辨別調用了Object的初始化方法
當然剛才隻是舉例展示了MethodRef常量類型的位元組碼分析,常量類型很多,但思路基本都類似,先通過tag确定其常量類型,後面連續幾個位元組确定其具體的值含義,類型及位元組含義圖如下
通路标志
通路辨別描述了類、接口的通路類型
JVM定義了如下通路标記
目前類全限定名
父類全限定名
如果有父類,後面會緊接着父類的全限定名,指向常量池中索引
接口資訊
描述了該類或父類實作的接口數量。緊接着的n個位元組是所有接口名稱的字元串常量的索引值。例子裡沒有實作接口,是以沒有。
字段表
記錄了目前類所定義的變量的總數量。包括類成員變量和類變量(靜态變量)
方法表
描述了方法名、通路辨別(ACC_PUBLIC) 等方法級别資訊
構造方法如下:執行了各個成員變量的初始化(注意這裡不包括靜态變量a)
test()方法
Code區是具體執行的JVM指令,即Java代碼轉換後的JVM指令,像一些位元組碼增強架構,修改的就是Code區的部分
LineNumber将源碼行号和位元組碼Code區中行号做了映射,比如test()中的code區
其中17代表Java代碼中的輸出Print,0對應Code區中的行号
stack=2, locals=1, args_size=1
0: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #10 // String 1
5: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 17: 0
line 18: 8
LocalVariableTable:本地變量表,包含This和局部變量
附加屬性表
位元組碼的最後一部分,該項存放了在該檔案中類或接口所定義屬性的基本資訊。
static、final、volatile在位元組碼中的展現
Static的位元組碼展現
在上面方法區的解析中,發現了靜态變量a并沒有在構造函數中進行初始化,那麼a在哪裡進行初始化呢?
發現代碼區多了一段static的初始化代碼,其中有a變量的初始化實作,這就是Java中的靜态代碼塊,即靜态變量初始化先于成員變量
特點:随着類的加載而執行,而且隻執行一次
如果靜态方法能調用非靜态成員變量的話,那如果别人通過類名調用靜态方法時執行個體對象可能并不存在,導緻異常出現
這就解釋了,靜态方法中為什麼不能調用非靜态本地成員變量的問題
假設我有一個靜态方法呢?加上靜态方法看看,代碼裡加上
public static void staticMethod() {
System.out.println("static method");
}
看下位元組碼中方法區的解析
再看下普通的test方法的位元組碼解析
差别在哪?靜态方法沒有本地變量表,不持有JavaTestController的本地this指針
故 靜态方法中不能出現this,super等關鍵字
Final、Volatile的位元組碼展現
看下Final、Volatile在位元組碼中的變量定義
那麼Volatile又是具體如何讓變量的修改直接寫回主存的呢?
Final又是如何讓基本類型值不能修改的呢?
其實作原理不在Java層面,而在JIT編譯生成的機器碼層面,這是stack-overflow上的回答
https://stackoverflow.com/questions/16898367/how-to-decompile-volatile-variable-in-java/16898432#16898432?newreg=4366ad45ce3f401a8dfa6b3d21bde635
故位元組碼中無法看到其實作原理,具體實作原理可以百度查
位元組碼層面來了解的話,隻需明白:final和volatile定義的變量會在位元組碼中打上ACC_FINAL、ACC_VOLATILE标簽,在運作時會進行處理和優化
Synchorinized的位元組碼展現
編寫如下測試代碼
分為三個方法
第一個為synchronized修飾普通方法(鎖目前調用對象)
第二個為synchronized、static修飾的靜态方法(鎖類)
第二個為靜态代碼塊(鎖synchronized括号中的對象)
public class JavaTestController {
public synchronized void test() {
System.out.println("1");
}
public static synchronized void test1() {
System.out.println("1");
}
public void test2() {
synchronized (new Object()) {
System.out.println(1);
}
}
}
看下javap解析出來的方法區代碼
synchronized修飾方法
先看test方法,可以看到flags中多了ACC_SYNCHRONIZED修飾符
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String 1
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/company/project/biz/controller/JavaTestController;
再看test1方法,也是多了ACC_SYNCHRONIZED修飾符
public static synchronized void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String 1
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 14: 0
line 15: 8
是以可以看出當synchronized修飾方法時,會在位元組碼中加上ACC_SYNCHRONIZED修飾符
ACC_SYNCHRONIZED是擷取螢幕鎖的一種隐式實作(沒有顯示的調用monitorenter,monitorexit指令)
如果位元組碼方法區中的ACC_SYNCHRONIZED标志被設定,那麼線程在執行方法前會先去擷取對象的monitor對象,如果擷取成功則執行方法代碼,執行完畢後釋放monitor對象
synchronized同步代碼塊
看下test2方法的位元組碼實作
public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: new #5 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: dup
8: astore_1
9: monitorenter
10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iconst_1
14: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
17: aload_1
18: monitorexit
19: goto 27
22: astore_2
23: aload_1
24: monitorexit
25: aload_2
26: athrow
27: return
指令第9行:monitorenter表示擷取對象螢幕鎖
指令第18行:monitorexit表示釋放對象螢幕鎖
指令第24行:monitorexit表示釋放對象螢幕鎖
有人可能會疑問,為什麼擷取了一次螢幕鎖,卻指令中有兩次釋放螢幕鎖的指令?
這是因為第二個monitorexit的位置實際是在抛出異常的時候自動調用的(防止程式異常時,螢幕鎖不會被釋放),athrow指令就是抛出異常的地方
是以當synchronized修飾同步代碼塊時,會顯示調用monitorenter争搶螢幕鎖,同步代碼執行完後調用monitorexit指令釋放螢幕鎖