前不久《深刻理解Java虛擬機》第三版發佈了,趕忙買來看了看新版的內容,這本書更新了不少新版本虛擬機的內容,還對之前的部份內容進行了重構,仍是值得去看的。本着複習和鞏固的態度,我決定來編譯一個簡單的類文件來分析Java的字節碼內容,來幫助理解和鞏固Java字節碼知識,但願也對閱讀本文的你有所幫助。java
說明:本次採用的環境是OpenJdk12python
首先咱們須要寫個簡單的小程序,1+1的程序,學習就要從最簡單的1+1開始,代碼以下:小程序
package top.luozhou.test;
/**
* @description:
* @author: luozhou
* @create: 2019-12-25 21:28
**/
public class TestJava {
public static void main(String[] args) {
int a=1+1;
System.out.println(a);
}
}
複製代碼
寫好java類文件後,首先執行命令javac TestJava.java
編譯類文件,生成TestJava.class
。 而後執行反編譯命令javap -verbose TestJava
,字節碼結果顯示以下:bash
Compiled from "TestJava.java"
public class top.luozhou.test.TestJava
minor version: 0
major version: 56
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // top/luozhou/test/TestJava
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 TestJava.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 top/luozhou/test/TestJava
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
{
public top.luozhou.test.TestJava();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 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: iconst_2
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: iload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 10: 0
line 11: 2
line 12: 9
}
複製代碼
1.基礎信息學習
上述結果刪除了部分不影響解析的冗餘信息,接下來咱們便來解析字節碼的結果。優化
minor version: 0 次版本號,爲0表示未使用
major version: 56 主版本號,56表示jdk12,表示只能運行在jdk12版本以及以後的虛擬機中
複製代碼
flags: ACC_PUBLIC, ACC_SUPER
複製代碼
ACC_PUBLIC
:這就是一個是不是public類型的訪問標誌。ui
ACC_SUPER
: 這個falg是爲了解決經過 invokespecial
指令調用 super 方法的問題。能夠將它理解成 Java 1.0.2 的一個缺陷補丁,只有經過這樣它才能正確找到 super 類方法。從 Java 1.0.2 開始,編譯器始終會在字節碼中生成 ACC_SUPER 訪問標識。感興趣的同窗能夠點擊這裏來了解更多。this
2.常量池編碼
接下來,咱們將要分析常量池,你也能夠對照上面總體的字節碼來理解。spa
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
複製代碼
這是一個方法引用,這裏的#5
表示索引值,而後咱們能夠發現索引值爲5的字節碼以下
#5 = Class #20 // java/lang/Object
複製代碼
它表示這是一個Object
類,同理#14
指向的是一個"<init>":()V
表示引用的是初始化方法。
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
複製代碼
上面這段表示是一個字段引用,一樣引用了#15
和#16
,實際上引用的就是java/lang/System
類中的PrintStream
對象。其餘的常量池分析思路是同樣的,鑑於篇幅我就不一一說明了,只列下其中的幾個關鍵類型和信息。
NameAndType
:這個表示是名稱和類型的常量表,能夠指向方法名稱或者字段的索引,在上面的字節碼中都是表示的實際的方法。
Utf8
:咱們常用的是字符編碼,可是這個不是隻有字符編碼的意思,它表示一種字符編碼是Utf8
的字符串。它是虛擬機中最經常使用的表結構,你能夠理解爲它能夠描述方法,字段,類等信息。 好比:
#4 = Class #19
#19 = Utf8 top/luozhou/test/TestJava
複製代碼
這裏表示#4
這個索引下是一個類,而後指向的類是#19
,#19
是一個Utf8
表,最終存放的是top/luozhou/test/TestJava
,那麼這樣一鏈接起來就能夠知道#4
位置引用的類是top/luozhou/test/TestJava
了。
3.構造方法信息
接下來,咱們分析下構造方法的字節碼,咱們知道,一個類初始化的時候最早執行它的構造方法,若是你沒有寫構造方法,系統會默認給你添加一個無參的構造方法。
public top.luozhou.test.TestJava();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
複製代碼
descriptor: ()V
:表示這是一個沒有返回值的方法。
flags: ACC_PUBLIC
:是公共方法。
stack=1, locals=1, args_size=1
:表示棧中的數量爲1,局部變量表中的變量爲1,調用參數也爲1。
這裏爲何都是1呢?這不是默認的構造方法嗎?哪來的參數?其實Java語言有一個潛規則:在任何實例方法裏面均可以經過this
來訪問到此方法所屬的對象。而這種機制的實現就是經過Java編譯器在編譯的時候做爲入參傳入到方法中了,熟悉python
語言的同窗確定會知道,在python
中定義一個方法總會傳入一個self
的參數,這也是傳入此實例的引用到方法內部,Java只是把這種機制後推到編譯階段完成而已。因此,這裏的1都是指this
這個參數而已。
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
複製代碼
通過上面這個分析對於這個構造方法表達的意思也就很清晰了。
aload_0
:表示把局部變量表中的第一個變量加載到棧中,也就是this
。
invokespecial
:直接調用初始化方法。
return
:調用完畢方法結束。
LineNumberTable:
這是一個行數的表,用來記錄字節碼的偏移量和代碼行數的映射關係。line 8: 0
表示,源碼中第8行對應的就是偏移量0
的字節碼,由於是默認的構造方法,因此這裏並沒有法直觀體現出來。
另外這裏會執行Object
的構造方法是由於,Object
是全部類的父類,子類的構造要先構造父類的構造方法。
4.main方法信息
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: iconst_2
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: iload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 10: 0
line 11: 2
line 12: 9
複製代碼
有了以前構造方法的分析,咱們接下來分析main
方法也會熟悉不少,重複的我就略過了,這裏重點分析code
部分。
stack=2, locals=2, args_size=1
:這裏的棧和局部變量表爲2,參數仍是爲1。這是爲何呢?由於main
方法中聲明瞭一個變量a
,因此局部變量表要加一個,棧也是,因此他們是2。那爲何args_size
仍是1呢?你不是說默認會把this
傳入的嗎?應該是2啊。注意:以前說的是在任何實例方法中,而這個main方法是一個靜態方法,靜態方法直接能夠經過類+方法名訪問,並不須要實例對象,因此這裏就不必傳入了。
0: iconst_2
:將int
類型2推送到棧頂。
1: istore_1
:將棧頂int
類型數值存入第二個本地變量。
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
:獲取PrintStream
類。
5: iload_1
: 把第二個int
型本地變量推送到棧頂。
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
:調用println
方法。這裏的println
方法就會把棧頂的元素做爲本身的入參來執行,最終也會輸出2。
9: return
:調用完畢結束方法。
這裏的LineNumberTable
是有源碼的,咱們能夠對照下我前面描述是否正確:
line 10: 0
: 第10行表示0: iconst_2
字節碼,這裏咱們發現編譯器直接給咱們計算好了把2推送到棧頂了。
line 11: 2
:第11行源碼對應的是2: getstatic
獲取輸出的靜態類PrintStream
。
line 12: 9
:12行源碼對應的是return
,表示方法結束。
這裏我也畫了一個動態圖片來演示main
方法執行的過程,但願可以幫助你理解:
這篇文章我從1+1的的源碼編譯開始,分析了生成後的Java字節碼,包括類的基本信息,常量池,方法調用過程等,經過這些分析,咱們對Java字節碼有了比較基本的瞭解,也知道了Java編譯器會把優化手段經過編譯好的字節碼體現出來,好比咱們的1+1=2,字節碼字節賦值一個2給變量,而不是進行加法運算,從而優化了咱們的代碼,提搞了執行效率。