1javascript
首先具有 Java 環境(能打開此文章,說明你確定具有此環境)。html
能開發代碼的工具(不強求IntelliJ IDEA),而後寫出以下圖 HelloWorld.java 就能夠。java
編譯 HelloWorld.java 源文件,生成對應的字節碼文件。程序員
而後須要一個能查看 class 文件的工具(不強求UltraEdit,只要能查看 16 進制的文件就行,俗稱:Hex Viewer),若是按照默認記事本,打開 class 文件的效果是這樣子的。shell
這打開的方式確定不對,換種開啓的方式,用 UltraEdit(本文統稱 UE) 進行打開。json
雖然不是亂碼,可是仍是看不懂啊,不過仔細瞧。引入眼簾的即是開頭的 CA FE BA BE(咖啡寶貝) ,這個東西叫作魔數。ruby
每一個 class 文件的頭 4 個字節被稱爲魔數(Magic Number),它的惟一做用是肯定這個文件是否爲一個能被虛擬機接受的 class 文件。
如若要是這麼說下去,估計都會完全瘋掉,換種方式進行分解。微信
接下來對 HelloWorld.class 文件進行反編譯,固然推薦可使用工具 ClassPy、JavaClassViewer、jclasslib 查看 class 文件結構,本次就用 jdk 自帶的命令 javap 來查看 class 文件的結構,並把反編譯的內容重定向輸出到文件 hello_javap.txt 中。架構
javap -v HelloWorld.class >> hello_javap.txt
javap 是 Java class 文件分解器,能夠反編譯,也能夠查看 java 編譯器生成的字節碼,用於分解 class 文件,能夠解析出當前類對應的 code 區(彙編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等等信息。oracle
上面的全部的準備工做,皆是爲了獲得 hello_javap.txt 文件。
Classfile /D:/workspace/codeonce/out/production/codeonce/think/twice/code/once/HelloWorld.class Last modified 2020-8-23; size 578 bytes MD5 checksum 20602b9ebb70bbd1247c77f3729ec8d5 Compiled from "HelloWorld.java"public class think.twice.code.once.HelloWorld SourceFile: "HelloWorld.java" minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #6.#20 // java/lang/Object."<init>":()V #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #23 // Hello World! #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #26 // think/twice/code/once/HelloWorld #6 = Class #27 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lthink/twice/code/once/HelloWorld; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 HelloWorld.java #20 = NameAndType #7:#8 // "<init>":()V #21 = Class #28 // java/lang/System #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream; #23 = Utf8 Hello World! #24 = Class #31 // java/io/PrintStream #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V #26 = Utf8 think/twice/code/once/HelloWorld #27 = Utf8 java/lang/Object #28 = Utf8 java/lang/System #29 = Utf8 out #30 = Utf8 Ljava/io/PrintStream; #31 = Utf8 java/io/PrintStream #32 = Utf8 println #33 = Utf8 (Ljava/lang/String;)V{ public think.twice.code.once.HelloWorld(); 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 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lthink/twice/code/once/HelloWorld;
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: ldc #3 // String Hello World! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 5: 0 line 6: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String;}
如此這般,天書同樣,着實讓人頭大... ...心莫慌,再次讓慌亂的心裏平靜下來,跟隨小猿的腳步,一塊兒去分析字節碼文件,嘗試完全搞懂它。
2
一:Classfile 文件信息
Classfile /D:/workspace/codeonce/out/production/codeonce/think/twice/code/once/HelloWorld.class //class文件的路徑 Last modified 2020-8-23; size 578 bytes //最後一次修改時間以及該class文件的大小 MD5 checksum 20602b9ebb70bbd1247c77f3729ec8d5 //該類的MD5值 Compiled from "HelloWorld.java" //編譯自源文件名
這塊感受不用詳細解釋,仔細去看,應該都能懂。
第 1 行:class 文件的路徑第 2 行:最後一次修改時間;該 class 文件的大小。第 3 行:MD5 checksum 值,例以下載文件的場景下會用於檢查文件完整性,檢測文件是否被惡意篡改。第 4 行:編譯自 HelloWorld.java 源文件。
二:類主體部分定義信息
public class think.twice.code.once.HelloWorld //包名及類名 SourceFile: "HelloWorld.java" //源文件名 minor version: 0 //次版本號 major version: 52 //主版本號,52 對應 JDK 1.8 flags: ACC_PUBLIC, ACC_SUPER //該類的權限修飾符(訪問標誌)
重點關注第 三、4 兩行,爲何要重點關注呢?業務開發中估計多數都遇到過 Unsupported major.minor version 的錯誤。其實就是經過高版本的 JDK 進行編譯(例如 JDK 1.8),而後跑在低版本的 JDK 上(JDK 1.5),就會報版本不支持。
爲了使用方便,特地整理一 JDK 各版本圖,請拿走不謝。
三:常量池信息
Constant pool: // 常量池,#數字至關因而常量池裏的一個索引 #1 = Methodref #6.#20 // java/lang/Object."<init>":()V //方法引用 #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; //字段引用 #3 = String #23 // Hello World! #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #26 // think/twice/code/once/HelloWorld //類引用 #6 = Class #27 // java/lang/Object //類引用 #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lthink/twice/code/once/HelloWorld; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 HelloWorld.java #20 = NameAndType #7:#8 // "<init>":()V //返回值 #21 = Class #28 // java/lang/System #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream; #23 = Utf8 Hello World! #24 = Class #31 // java/io/PrintStream #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V #26 = Utf8 think/twice/code/once/HelloWorld #27 = Utf8 java/lang/Object #28 = Utf8 java/lang/System #29 = Utf8 out #30 = Utf8 Ljava/io/PrintStream; #31 = Utf8 java/io/PrintStream #32 = Utf8 println #33 = Utf8 (Ljava/lang/String;)V
#數字至關因而常量池裏的一個索引,例如上面代碼段裏 #1 表明的是一個方法引用,而且該引用由 #6.#20 構成。
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V //方法引用
#6 = Class #27 // java/lang/Object //類引用#27 = Utf8 java/lang/Object
#20 = NameAndType #7:#8 // "<init>":()V //返回值#7 = Utf8 <init> #8 = Utf8 ()V
在 JVM 規範中常量類型定義了不少,本次只彙總遇到的幾個。
四:構造方法信息
public think.twice.code.once.HelloWorld(); descriptor: ()V //方法描述符,這裏的V表示void flags: ACC_PUBLIC //權限修飾符 Code: stack=1, locals=1, args_size=1 0: aload_0 // aload_0 把this裝載到了操做數棧中 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: //行號表 line 3: 0 //源代碼的第 3 行,0 表明字節碼裏的 0 LocalVariableTable: // 本地變量表 Start Length Slot Name Signature 0 5 0 this Lthink/twice/code/once/HelloWorld; // 索引爲0,變量名稱爲 this
descriptor:方法入參和返回描述;
flags:訪問權限控制符爲 public;
stack:方法對應棧幀中的操做數棧的深度爲 1;
locals:本地變量數量爲 1;
args_size:參數數量爲 1;
aload:從局部變量表的相應位置裝載一個對象引用到操做數棧的棧頂;
invokespecial:調用一個初始化方法;
LineNumberTable、LocalVariableTable:前者表明行號表,是爲調試器提供源碼行號與字節碼的映射關係;後者代碼本地變量表,存放方法的局部變量信息,屬於調試信息。
思考一:經過這段字節碼信息,印證了一個準則:在沒有顯示聲明構造的情形下,Java 會默認提供無參構造方法。
思考二:雖然是無參構造器,爲何 args_size 的值是 1 呢?是由於無參構造器和非靜態方法調用會默認傳入 this 變量參數,其中 aload_0 即表示的 this。
五:main 方法的信息
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: ldc #3 // String Hello World! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 5: 0 line 6: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String;
經過 descriptor 、flags 能直觀的可以讀懂 main 方法的入參,返回值以及訪問修飾符;經過 LocalVariableTable 運行時候的局部變量表,可以看到 main 函數的 args 參數保存在了 LocalVariableTable 中。
3
重點關注 main 方法中的以下指令(紅色圈住部分)
(一)指令 getstatic #2
表示從索引位置 2 獲取靜態變量,而 #2 又是引用 #21.#22 構成。
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; //字段引用
#21 = Class #28 // java/lang/System#28 = Utf8 java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;#29 = Utf8 out#30 = Utf8 Ljava/io/PrintStream;
兜了一大圈,其實 getstatic #2 指令就是爲了拿到輸出對象流。
(二)指令 ldc #3
指令 ldc #3 是把常量壓入棧中,#3 對應的是字符串 Hello World。
#3 = String #23 // Hello World!#23 = Utf8 Hello World!
(三)指令 invokevirtual #4
invokevirtual #4 是方法引用,查表過去就是 #24.#25
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#24 = Class #31 // java/io/PrintStream#31 = Utf8 java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V#32 = Utf8 println#33 = Utf8 (Ljava/lang/String;)V
#24 則是類引用 #31 java/io/PrintStream,#25 則是方法 println((Ljava/lang/String;)V) 的引用,這裏實際上是在執行打印操做。
最後,貼一個字節碼裏的指令與源代碼的一個對應關係圖。
4
本次,主要對 Java 字節碼有個簡單的認識,讓你們從字節碼角度看看 HelloWorld,看似很容易的入門程序,背後的原理確實不簡單。但願經過本次分享,你們對 Java 字節碼再也不陌生,也但願你們可以學以至用,可以親自去分析 i++、++i ;字符串拼接效率等諸多場景執行原理。
另外,在 Java 的世界裏,有 Java Language Specificatio、Java Virtual Machine Specification 兩種規範,直譯過來就是 Java 語言規範以及 JVM 規範,本次主要參考 JVM 規範。
閒暇之餘,推薦你們多讀一讀:
https://docs.oracle.com/javase/specs/index.htmlhttps://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf
好了,本次就談到這裏,一塊兒聊技術、談業務、噴架構,少走彎路,不踩大坑。會持續輸出原創精彩分享,敬請期待!
本文分享自微信公衆號 - 一猿小講(yiyuanxiaojiangV5)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。