從字節(jié)碼角度分析一波 Spring 源碼中隱藏知識點!
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來,我們一起精進(jìn)!你不來,我和你的競爭對手一起精進(jìn)!
編輯:業(yè)余草
blog.csdn.net/w605283073
推薦:https://www.xttblog.com/?p=5263
一、背景
這周一我面試了一個 Java 程序員,工作了 6 年了。我問的第一個問題就是“Redis 能用來做什么?你們系統(tǒng)中使用了 Redis 的哪些功能?”,他回答說是緩存。
緩存確實是 Redis 使用最多的領(lǐng)域。可當(dāng)我再進(jìn)一步問“還有呢?”,他想了一會說:“分布式鎖”。然后我就分布式鎖再深入問下去,他開始搖頭:我們項目里面 Redis 的鎖方法都是別人(應(yīng)該是架構(gòu)師)封裝好的,拿過來直接使用,內(nèi)部細(xì)節(jié)沒有去了解過,也沒有必要了解。
然后我和他說了,Redis 有很多使用場景,參考《面試官:說出16個Redis常見的使用場景!》。
然后我和他講了庖丁解牛和蠻夫剁骨頭的故事,他認(rèn)為船到橋頭自然直,他沒有用和學(xué) Redis 的這些功能是因為項目中沒用使用的場景和必要。
可能很多人會覺得沒必要,因為平時開發(fā)用不到,而且不學(xué)這個也沒耽誤學(xué)習(xí)。
但是這里分享一點感悟,即人總是根據(jù)自己已經(jīng)掌握的知識和技能來解決問題的。
這里有個悖論,有時候你覺得有些技術(shù)沒用恰恰是因為你沒有熟練掌握它,遇到可以使用它的場景你根本想不到用。
從生活的角度來講
如果你是一個非計算機(jī)專業(yè)的學(xué)生,你老師給你幾張圖書的拍照,大概 3000 字,讓你打印成文字。
你打開電腦,噼里啪啦一頓敲,搞了一下午干完了。
如果你知道語音輸入,那么你可能采用語音輸入的方式,30 分鐘搞定。
如果你了解 OCR 圖片文字識別,可能 5 分鐘搞定。
不同的方法,帶來的效果完全不同。然而最可怕的是,你不會語音輸入或者 OCR,你不會覺得自己少了啥。
OCR 識別絕對不是你提高點打字速度可以追趕上的。
學(xué)習(xí) Java 的角度
很多人學(xué)習(xí)知識主要依賴百度,依賴博客,依賴視頻和圖書,而且這些資料質(zhì)量參差不齊,而且都是別人理解之后的結(jié)果。
比如你平時不怎么看源碼,那么你就很少能將源碼作為你學(xué)習(xí)的素材,只能依賴博客、圖書、視頻等。
如果你平時喜歡看源碼,你會對源碼有自己的理解,你會發(fā)現(xiàn)源碼對你的學(xué)習(xí)有很多幫助。
如果你平時不怎么用反編譯和反匯編,那么你更多地只能依賴源碼,依賴調(diào)試等學(xué)習(xí)知識,而不能從字節(jié)碼層面來學(xué)習(xí)和理解知識。
當(dāng)你慢慢熟練讀懂虛擬機(jī)指令,你會發(fā)現(xiàn)你多了一個學(xué)習(xí)知識的途徑。
昨天的文章
昨天我發(fā)的那篇文章(Java For循環(huán)的十一種優(yōu)化方案!),里面也有很多知識點,但是很多人看不上,因為單個優(yōu)化的效果并不明顯。但是你要知道 HikariCP 之所以成為最快的數(shù)據(jù)庫,就是因為作者的重點細(xì)小優(yōu)化做到的。
LongAdder 之所以性能牛叉(面試官:為什么LongAdder性能比long還快),也是因為作者不但的研究和突破,分而治之的通過細(xì)微的調(diào)整,將 AtomicLong 遠(yuǎn)遠(yuǎn)甩在身后,而作者的論文也在業(yè)界享有盛譽!
人總是不愿意離開舒適區(qū)的
很多人在學(xué)習(xí)新知識時,總是本能地抵觸。會找各種理由不去學(xué),“比如暫時用不到”,“學(xué)了沒啥用”,“以后再說”。
甚至認(rèn)為這是在浪費時間。
為什么要學(xué)習(xí)字節(jié)碼?
這幾年我 Watch 了不少知名開源框架,通過它們,我學(xué)習(xí)了一些 JVM 字節(jié)碼的知識(深入理解JVM方法調(diào)用的內(nèi)部機(jī)制),雖然不算精通,但是讀字節(jié)碼起來已經(jīng)不太吃力。
因此,我認(rèn)為學(xué)會字節(jié)碼可以從比源碼更深的層面去學(xué)習(xí) Java 相關(guān)知識。
雖然不可能所有問題都用字節(jié)碼的知識來解決,但是它給你一個學(xué)習(xí)的途徑。
比如通過字節(jié)碼的學(xué)習(xí)你可以更好地理解 Java中各種語法和語法糖背后的原理,更好地理解多態(tài)等語言特性。
語法糖
下面通過舉一個簡單的例子,來說明學(xué)習(xí)字節(jié)碼的作用。
public class ForEachDemo {
public static void main(String[] args) {
List<String> data = new ArrayList<>();
data.add("a");
data.add("b");
for (String str : data) {
System.out.println(str);
}
}
}
編譯:javac ForEachDemo.java
反匯編:javap -c ForEachDemo
public class com.imooc.basic.learn_source_code.local.ForEachDemo {
public com.imooc.basic.learn_source_code.local.ForEachDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String a
11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: aload_1
18: ldc #6 // String b
20: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
25: pop
26: aload_1
27: invokeinterface #7, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
32: astore_2
33: aload_2
34: invokeinterface #8, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
39: ifeq 62
42: aload_2
43: invokeinterface #9, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
48: checkcast #10 // class java/lang/String
51: astore_3
52: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
55: aload_3
56: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
59: goto 33
62: return
}
我們可以清晰地看到 foreach 循環(huán)底層用到了迭代器實現(xiàn),甚至可以逆向腦補出對應(yīng)的 Java 源碼(大家可以嘗試根據(jù)字節(jié)碼寫出等價的源碼)。
讀源碼遇到的一個問題
我們在讀源碼時經(jīng)常會遇到類似下面的這種寫法。
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#startWebServer中的源碼:
private WebServer startWebServer() {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.start();
}
return webServer;
}
在函數(shù)中聲明一個和成員變量同名的局部變量,然后將成員變量賦值給局部變量,再去使用。
看似很小的細(xì)節(jié),隱含著一個優(yōu)化思想。
可能有些人讀過某些文章有提到(為什么我們總得看到一個文章學(xué)會一個知識?如果沒看到怎么辦?),更多的人可能并不能理解有什么優(yōu)化。
模擬
普通的語法糖這里就不做過多展開,重點講講第二個優(yōu)化的例子。
模仿上述寫法的例子:
public class LocalDemo {
private List<String> data = new ArrayList<>();
public void someMethod(String param) {
List<String> data = this.data;
if (data != null && data.size() > 0 && data.contains(param)) {
System.out.println(data.indexOf(param));
}
}
}
編譯:javac LocalDemo.java
反匯編:javap -c LocalDemo
public class com.imooc.basic.learn_source_code.local.LocalDemo {
public com.imooc.basic.learn_source_code.local.LocalDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/util/ArrayList
8: dup
9: invokespecial #3 // Method java/util/ArrayList."<init>":()V
12: putfield #4 // Field data:Ljava/util/List;
15: return
public void someMethod(java.lang.String);
Code:
0: aload_0
1: getfield #4 // Field data:Ljava/util/List;
4: astore_2
5: aload_2
6: ifnull 41
9: aload_2
10: invokeinterface #5, 1 // InterfaceMethod java/util/List.size:()I
15: ifle 41
18: aload_2
19: aload_1
20: invokeinterface #6, 2 // InterfaceMethod java/util/List.contains:(Ljava/lang/Object;)Z
25: ifeq 41
28: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
31: aload_2
32: aload_1
33: invokeinterface #8, 2 // InterfaceMethod java/util/List.indexOf:(Ljava/lang/Object;)I
38: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
41: return
}
此時局部變量表中 0 為 this,1 為 param 2 為 局部變量 data。

直接使用成員變量的例子:
public class ThisDemo {
private List<String> data = new ArrayList<>();
public void someMethod(String param) {
if (data != null && data.size() > 0 && data.contains(param)) {
System.out.println(data.indexOf(param));
}
}
}
編譯:javac ThisDemo.java
反匯編:javap -c ThisDemo
public class com.imooc.basic.learn_source_code.local.ThisDemo {
public com.imooc.basic.learn_source_code.local.ThisDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/util/ArrayList
8: dup
9: invokespecial #3 // Method java/util/ArrayList."<init>":()V
12: putfield #4 // Field data:Ljava/util/List;
15: return
public void someMethod(java.lang.String);
Code:
0: aload_0
1: getfield #4 // Field data:Ljava/util/List;
4: ifnull 48
7: aload_0
8: getfield #4 // Field data:Ljava/util/List;
11: invokeinterface #5, 1 // InterfaceMethod java/util/List.size:()I
16: ifle 48
19: aload_0
20: getfield #4 // Field data:Ljava/util/List;
23: aload_1
24: invokeinterface #6, 2 // InterfaceMethod java/util/List.contains:(Ljava/lang/Object;)Z
29: ifeq 48
32: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
35: aload_0
36: getfield #4 // Field data:Ljava/util/List;
39: aload_1
40: invokeinterface #8, 2 // InterfaceMethod java/util/List.indexOf:(Ljava/lang/Object;)I
45: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
48: return
}
此時局部變量表只有兩個,即 this 和 param。

大家也可以通過 javap -c -v 來查看更詳細(xì)信息,本例截圖中用到 IDEA 插件為jclasslib bytecode viewer。
分析
通過源碼其實我們并不能很好的理解到底優(yōu)化了哪里。
我們分別對兩個類進(jìn)行編譯和反匯編后可以清晰地看到:第一個例子代碼多了一行,反而反編譯后的字節(jié)碼更短。
第二個例子反編譯后的字節(jié)碼比第一個例子長在哪里呢?
我們發(fā)現(xiàn)主要多在:getfield #4 // Field data:Ljava/util/List;這里。
即每次獲取 data 對象都要先aload_0然后再getfield指令獲取。
第一個例子通過astore_2將其存到了局部變量表中,每次用直接aload_2直接從局部變量表中加載到操作數(shù)棧。
從而不需要每次都從 this 對象中獲取這個屬性,因此效率更高。
這種思想有點像寫代碼中常用的緩存,即將最近要使用的數(shù)據(jù)先查一次緩存起來,使用時優(yōu)先查緩存。
「本質(zhì)上體現(xiàn)了操作系統(tǒng)中的時間局部性和空間局部性的概念(不懂的話翻下書或百度下)」。
「因此通過字節(jié)碼的分析,通過聯(lián)系實際的開發(fā)經(jīng)驗,通過聯(lián)系專業(yè)知識,這個問題我們就搞明白了。」
另外也體現(xiàn)了用空間換時間的思想。
知識只有能貫穿起來,理解的才能更牢固。
此處也體現(xiàn)出專業(yè)基礎(chǔ)的重要性。
另外知識能聯(lián)系起來、思考到本質(zhì),理解才能更深刻,記憶才能更牢固,才更有可能靈活運用。
四、總結(jié)
這只是其中一個非常典型的例子,學(xué)習(xí) JVM 字節(jié)碼能夠給你一個不一樣的視角,讓你多一個學(xué)習(xí)的途徑。
可能很多人說自己想學(xué)但是無從下手,這里推薦大家先看《深入理解Java虛擬機(jī)》,然后結(jié)合《Java虛擬機(jī)規(guī)范》,平時多敲一下 javap 指令,慢慢就熟悉了,另外強力推薦jclasslib bytecode viewer插件,該插件可以點擊指令跳轉(zhuǎn)到 Java 虛擬機(jī)規(guī)范對該指令的介紹的部分,對學(xué)習(xí)幫助極大。
很多人可能會說,學(xué)這個太慢。
的確,急于求成怎么能學(xué)的特別好呢?厚積才能薄發(fā),耐不住寂寞怎么能學(xué)有所成呢。
本文通過這其中一個例子讓大家理解,JVM字節(jié)碼可以幫助大家理解Java的一些語法(篇幅有限,而且例子太多,這里就不給出了,感興趣的同學(xué)自己嘗試),甚至幫助大家學(xué)習(xí)源碼。
試想一下,如果你認(rèn)為學(xué)習(xí)字節(jié)碼無用,甚至你都不了解,你怎么可能用它來解決問題呢?
「你所掌握的知識幫助你成長由限制了你的成長,要敢于突破舒適區(qū),給自己更多的成長機(jī)會。」
歡迎點贊、評論、轉(zhuǎn)發(fā),你的鼓勵,是我創(chuàng)作的動力。
