大話+圖說:Java字節碼指令——只爲讓你懂

前言

隨着Java開發技術不斷被推到新的高度,對於Java程序員來說愈來愈須要具有對更深刻的基礎性技術的理解,好比Java字節碼指令。否則,可能很難深刻理解一些時下的新框架、新技術,盲目一味追新也會愈來愈感乏力。java

本文既不求照本宣科,亦不求炫技或著文立說,僅力圖以最簡明、最形象生動的方式,結合例子與實戰,讓小白也能搞懂這門看似複雜的技術概念。git

單刀直入

閒言碎語不要講,先表一表,什麼是Java字節碼指令?簡而言之,Java字節碼指令就是Java虛擬機可以聽得懂、可執行的指令,能夠說是Jvm層面的彙編語言,或者說是Java代碼的最小執行單元。
有點Java基礎的人必定都知道,javac命令會將Java源文件編譯成字節碼文件,即.class文件,其中就包含了大量的字節碼指令。所以能夠將javac命令理解爲一個翻譯命令,將源文件翻譯成Jvm能夠執行的指令。
那麼最直觀的探究方法莫過於直接對比翻譯先後的內容。
具體如何對比呢?就不得不用到Java爲咱們一直默默提供的一項利器,javap命令,它能夠解析字節碼,將字節碼內部邏輯以可讀的方式呈現出來。爲了緊貼實戰,咱們直接在新建的Java工程裏,寫這樣一個UserServiceImpl類,裏面包含幾個由簡單到複雜的方法,以及一個名爲serviceType的屬性:程序員

clipboard.png

如圖,以上方法,複雜度由低到高依次爲:getServiceType<setServiceType<genToken<login(以及一個實例代碼塊),後面我也會按照這個順序解讀其字節碼指令的執行邏輯。
下面咱們編譯工程,而後在下圖所示的目錄(gradle編譯工程)找到該類的字節碼文件:github

圖片描述

cd到這個路徑下,運行javap命令:segmentfault

javap -v -p UserServiceImpl

就能夠觀看到翻譯版的Java字節碼的胴體了!這裏的-v意思是囉嗦模式,會輸出全面的字節碼信息,而-p是指涵蓋全部成員。原字節碼信息輸出內容較多,基於本文的目標,取其一方法的內容,整理以下圖:
方法1,getServiceType():app

圖片描述

這個getServiceType的方法應該是再簡單不過的Java代碼,翻譯成字節碼後也變成了三行,咱們先來簡單推理一下:第一句,aload_0不知所云,索性略過;第二行,getfield應該能夠讀懂,後面這個#8彷佛是他的參數(其實是對常量池的引用),//後面註釋的內容是javap給咱們加上的,意思應該是#2的指向是"Field serviceType:Ljava/lang/String;"這個內容。
因此getfield這一行就是取出serviceType這個字段嘍,so easy。areturn確定就是return的意思,a的含義也先略過不表。總之就是取出serviceType字段而後return嘍。框架

那麼如今的問題就是aload_0是什麼意思了,看似多餘,但仔細思考一下,彷佛以前給getfield指令傳入了「Field serviceType:Ljava/lang/String;」這樣一個並不完整的參數,其後半部分的「Ljava/lang/String;」僅僅表示這個serviceType字段的類型是String,也就是說,整個參數裏沒有說是取的誰的serviceType字段啊!到底是get誰的feild呢?dom

由此能夠想到:aload操做必定是在爲getfield指令準備了一個主體。

實際上,再結合下面的局部變量表,aload_0中的0正是局部變量表裏的Slot 0的含義。意思是將局部變量表裏的Slot 0的東西壓入操做數棧,這個Slot 0裏的東西name正是this,也就是UserServiceImpl的實例,即getfield的主體。jvm

clipboard.png

大戲上演

好了,對於小白同窗有些陌生的概念來了,啥是操做數棧?啥是局部變量表?
其實這兩個東西理解好了,關於虛擬機指令就懂了一大半了。
那麼,不妨刪繁就簡,由易入難,先講一個這樣的故事,故事起名叫:工具

Java方法之創世紀

話說Jvm大帝是神之旨意的履行者(Jvm大帝就是虛擬機,神就是開發者,神之旨意是開發者寫好並編譯後的字節碼...),當Jvm大帝帶領Java世界運行進入了一個新的方法後,會爲這個方法在棧內存大陸上創造兩個重要的領域:局部變量表和操做數棧。

要有棧。要有表。神說。

依照神之旨意,jvm大帝創造的局部變量表裏通常會包含this指針(針對實例方法,靜態方法固然無此)、方法的全部傳入參數和方法中所開闢的本地變量。

那麼操做數棧是幹嗎用的呢?

咱們再引入另一個比喻,若是把運行Java方法理解爲拍戲,那麼局部變量表裏的各個局部變量就是這部戲的核心主角,或者說領銜主演,而操做數棧正是這部戲的舞臺。所謂操做數棧搭臺,局部變量唱戲,是也。那麼aload_0就是告訴Jvm導演(大帝已淪落爲導演),請0號演員this同志登臺(壓棧),演後邊的本子。
固然了,這個比喻並不徹底恰當,由於操做數棧並非「舞臺」的結構,而是棧的結構。可是這個比喻能夠很好地說明局部變量表和操做數棧之間的關係,以及aload_0的做用。

下面咱們用一張圖來演示一下getServiceType這個小劇本橋段所導演的故事:

圖片描述

好吧這部劇雖然短的可憐,但已經基本把指令、操做數棧和局部變量表三者的關係演繹了出來。
值得注意的是,getfield這條指令對操做數棧進行了複合操做,其流程能夠示意以下圖:

彈出-&gt;取值-&gt;壓回

後面咱們將要接觸到的許多指令都如此,指令內部執行了彈出—>處理—>壓回的流程。
下面咱們就來分析一個相對複雜一點的方法,setServiceType(String),以下圖:

圖片描述

這裏咱們看到,變化主要有,指令多了一行,多進行了一次aload,getfield變成了putfield,areturn變成了return,僅此而已。另外領銜主演也就是局部變量表裏多了一位,也就是方法的傳入參數serviceType字符串對象了。其情節以下:

圖片描述

這裏,putfield只彈出棧內的操做數,而沒有向操做數棧壓回任何數據,並且執行putfield以前,棧內元素的位置也必須符合「值在上,主體在下」要求。
而最後的return僅表示方法結束,而不會像areturn同樣返回棧頂元素。這也印證了setServiceType(String)方法沒有返回參數。

融會貫通

相信有了以上的講解,你們對指令、操做數棧、局部變量表三者的運做關係有了必定認識,爲了後邊可以分析更復雜的方法,這裏必須歸納性地講解一下更多的Java字節碼指令。雖然Java字節碼指令很是多,但其實經常使用的不外乎幾個類別,先從這幾個經常使用類別入手理解,即可漸入佳境。
關於字節碼指令的分類,能夠從兩個維度進行:一是指令的功能,二是指令操做的數據類型。咱們先從功能提及,指令主要能夠分爲以下幾類:

  1. 存儲和加載類指令:主要包括load系列指令、store系列指令和ldc、push系列指令,主要用於在局部變量表、操做數棧和常量池三者之間進行數據調度;(關於常量池前面沒有特別講解,這個也很簡單,顧名思義,就是這個池子裏放着各類常量,比如片場的道具庫)
  2. 對象操做指令(建立與讀寫訪問):好比咱們剛剛的putfield和getfield就屬於讀寫訪問的指令,此外還有putstatic/getstatic,還有new系列指令,以及instanceof等指令。
  3. 操做數棧管理指令:如pop和dup,他們只對操做數棧進行操做。
  4. 類型轉換指令和運算指令:如add/div/l2i等系列指令,實際上這類指令通常也只對操做數棧進行操做。
  5. 控制跳轉指令:這類裏包含經常使用的if系列指令以及goto類指令。
  6. 方法調用和返回指令:主要包括invoke系列指令和return系列指令。這類指令也意味這一個方法空間的開闢和結束,即invoke會喚醒一個新的java方法小宇宙(新的棧和局部變量表),而return則意味着這個宇宙的結束回收。

以下圖,展現了各種指令的做用:

圖片描述

再從另一個維度,即指令操做的數據類型來說:指令開頭或尾部的一些字母,就每每代表了它所能操做的數據類型:

a對應對象,表示指令操做對象性數據,好比aload和astore、areturn等等。
i對應整形。也就有iload,istore等i系列指令。
f對應浮點型。
l對應long,b對應byte,d對應double,c對應char。
另外地,ia對應int array,aa對應object array,da對應double array。不在一一贅述。

瞭解了以上內容,咱們再去看最後幾個方法,應該就會容易理解不少了。
下面咱們就直搗黃龍genToken這個方法(圖中的顏色暗示了指令和方法調用之間的關係):

clipboard.png

這個過程簡單解讀以下:
1.new一個StringBuilder對象(在堆內存中開闢空間),並將其引用入棧,用於實現加號鏈接字符串功能(至關於C++中的運算符重載);
2.dup複製棧頂的剛剛放入的引用,再次壓棧,這時棧裏有兩個重複的內容,深度爲2;
3.調用並彈出棧頂StringBuilder引用對象的<init>方法,棧深度爲1;
4.(綠色部分)調用UUID.randomUUID()靜態方法,結果壓棧後彈出調用String的toString方法,再壓棧,棧深度爲2;
5.(黃色部分)將"-"和""字符壓棧,此時棧深度爲4,彈出(棧頂3個元素)調用replace方法,結果壓棧,深度爲2;
6.調用StringBuilder對象的append方法,結果壓棧,深度爲1;
7.(藍色部分)將參數user壓棧並調用hashCode方法,結果壓棧,深度爲2;
8.調用StringBuilder對象的append方法(此處和上面的append調用共同完成了加號功能,在圖中爲紅色部分),結果壓棧,深度爲1,再調用toString方法後結果壓棧,深度爲1;
9.areturn返回棧頂對象。

再看這個包含if跳轉的方法login:

clipboard.png

如上圖,圖中已經說明的比較全面了,再也不贅述。值得一提的是,Java的這種基於棧結構的指令,在設計上有一種很是簡潔的美感,指令與指令之間並無較重的依賴,每條指令僅僅與操做數棧等領域內的數據發生關係,充滿着某種平衡與秩序感。所以也必須注意,幾乎每條指令的運行都有其前提,好比在invokevirtual或invokespecial指令執行前,必須保證操做數棧內提早按順序壓入好所需的操做數,不然就會發生問題。
關於最複雜的onCreate方法,就再也不囉嗦解讀了,讀者能夠前往個人github上的對應demo repo,進入tutorial分支,拉取源碼和教程資源,或者本身寫demo體驗這一完整過程。
地址:https://github.com/BryanSharp...

後話

關於實戰,一是能夠學習使用強大開源工具ASM.jar;二是,能夠參考本人的另外一篇文章:Java字節碼修改神器HiBeaver:黑掉你的SDK以及一次Android字節碼插樁實戰,利用hibeaver這個助手,開發者能夠很是靈活地對字節碼進行修改,插入指令,hook代碼,甚至創建一些簡單的AOP框架,對於Java字節碼學習大有裨益。
hibeaver徹底開源,github項目地址:https://github.com/BryanSharp...

祝玩的愉快!
本文若有不妥之處,歡迎交流指正。

另外,本文爲了儘量地簡明生動、直入核心,簡化了不少概念和細節,讀者須知實際狀況的更爲複雜。但相信在理解了本文之後,就能夠抓住Java字節碼指令的核心理念,也就算扣開虛擬機學習的大門並能夠開始讀書精進了。下面盜圖一張(後有出處),可做拓展:

圖片描述

連接:http://blog.csdn.net/luanloui...

關注最新技術分享和資訊:TechHome,技術人之家!

clipboard.png

相關文章
相關標籤/搜索