JVM字節碼

前言

Android開發講道理更應該卷的是dex字節碼,但實際上做應用開發時,插樁流程往往在class2dex的過程中,一些插樁框架最終操作的還是class字節碼。Java世界JVM最終執行字節碼指令,將class字節碼解釋成機器碼執行;Android世界ART最終執行dex,將dex字節碼解釋成機器碼執行。當然ART本身也做了好幾次變更,現在基本上是解釋、JIT、AOT混合的方式執行,見下圖:


Android Runtime

簡單的總結就是,app啟動時將dex加載進內存,首次執行方法時解釋執行,并將多次執行的熱點方法配置到profile文件,機器在充電或其它閑置狀態時執行dex2oat將profile文件配置的熱點函數AOT編譯成.oat文件,.oat本質上就是個elf文件了。后續方法執行時先找.oat,然后判斷是否為熱點函數,熱點函數取JIT Cache,未命中緩存JIT編譯執行并添加到JIT Cache,都沒有就解釋執行。

dex字節碼指令是基于寄存器的,class字節碼是基于棧的,虛擬機層面的寄存器雖然也是內存模擬的,但是同樣的操作基于寄存器的指令相比較于基于棧的class字節碼是要少一些的,指令少就意味著對內存的訪問次數少,執行效率自然就高。一般來說應用開發不太會去操作dex字節碼,除非說做逆向,反編譯dex,拿到smali文件,調試魔改重簽名。雖然運行時也有dexmaker這種框架可以用Java api動態生成dex指令,但實際上還是在class字節碼層面操作更多。

無論是class字節碼還是dex字節碼,都是編譯器前端的產物IR,dex字節碼相當于對class字節碼再轉換了一層,更適用于資源有限的移動平臺機器。

dex文件格式也可以對應class文件格式來看。

JVM內存模型

BB了一堆,回到本文的重點。Java字節碼是一組可以由JVM執行的高度優化的指令,它被記錄在Class文件中,在虛擬機加載Class文件時執行。Class文件并不等于字節碼,Class文件包含字節碼,通俗來說字節碼就是Class文件方法表(methods)中的Code屬性。

先看看JDK8的內存結構,偷張圖:


JDK8

當類被首次加載時,會在堆上創建類的Class對象其實也就是反射會用到的Class對象,同時Class文件中的信息被加載到JVM方法區中,字節碼指令也會裝配到方法區中,為方法的運行提供支持。

虛擬機棧是描述方法的內存模型,屬于線程私有。虛擬機棧中方法調用時會創建棧幀,方法return返回時相應的會將棧幀出棧。棧幀可視為單個方法的內存模型,其又包括局部變量表、操作數棧、動態連接、方法出口。

  • 局部變量表用于儲存方法參數和局部變量。
  • 操作數棧保存求值的中間結果、調用別的方法參數。
  • 動態連接指的是棧幀當前方法指向運行時常量池的一個引用,說白了也就是找到對應方法區的方法然后根據這個引用讀取字節碼。
  • 方法出口記錄被調用的方法退出后回到上層方法的位置。

下面通過例子看一下字節碼指令執行流程。

加載存儲指令

根據我自己的學習方法,重點先關注局部變量表、操作數棧和三個字節碼指令:

  1. 常量入棧指令
    將一個常量加載到操作數棧 :bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
  2. 局部變量壓棧指令
    xload、xload_<n>(其中x為i、l、f、d、a,而n為0~3)
  3. 出棧裝入局部變量表指令
    將一個數值從操作數棧存儲到局部變量表:xstore、xstore_<n>其中x為i、l、f、d、a,而n為0~3);xastore(其中x為i、l、f、d、a、b、c、s),局部變量表可以看做一個數組,對于非靜態方法索引為0的位置默認為this指針。
說起來很抽象,下面看例子
    public void add() {
        int a = 3;
        int b = 6;
        int c = a + b;
    }

對應字節碼,逐行注釋。這里用asm插件查看字節碼,除了大小寫之外基本沒啥區別。

  public add()V
    ICONST_3    //常量3入棧
    ISTORE 1    //常量3出棧存入局部變量表下表為1的位置
    BIPUSH 6    //常量6入棧
    ISTORE 2    //常量6出棧存入局部變量表下表為2的位置
    ILOAD 1     //常量3入棧,load指令將局部變量復制一份到操作數棧
    ILOAD 2     //常量6入棧
    IADD        //3,6出棧執行加法,將結果9入棧
    ISTORE 3    //9出棧存入局部變量表下標為3的位置
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 4

這里有個新的指令iadd表示操作數棧棧頂兩個int類型數據出棧,執行加法運算將相加的結果入棧。與之相似,加減乘除等算數指令都一樣,其實完全沒有必要去背這些字節碼指令,弄懂了同一類指令的執行流程就夠了,遇到不記得的指令對應去查找就好了。

正好借這個簡單的例子看一下dex字節碼,這里借助AS java2smali插件:

# virtual methods
.method public add()V
    .registers 4

    .prologue
    .line 5
    const/4 v0, 0x3

    .line 6
    .local v0, "a":I
    const/4 v1, 0x6

    .line 7
    .local v1, "b":I
    add-int v2, v0, v1

    .line 8
    .local v2, "c":I
    return-void
.end method

其實相對來說更接近匯編的dex字節碼反而更好懂,畢竟計算沒有入棧出棧的過程直接add-int v2, v0, v1,去掉行號使用的指令確實也更少。事實上學會了閱讀class字節碼,dex字節碼肯定也不在話下。本文后面就不再關注dex字節碼了,有興趣的小伙伴可以對照指令集學習dalvik-bytecode

  1. i++
    public void add(){
        int i = 3;
        i = i++;
    }

對應字節碼

  public add()V
    ICONST_3    //將常量3壓入操作數棧
    ISTORE 1    //操作數棧出棧,存入局部變量表下標為1的位置
    ILOAD 1     //局部變量表下標為1的變量加載到操作數棧
    IINC 1 1    //4--->自增指令,IINC后面兩個1,第一個是局部變量表位置,第二個是增加多少。
    ISTORE 1    //3--->操作數棧出棧,存入局部變量表下標為1的位置
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 2

很清晰了啊,i++自增之后會被操作數棧原本的值重新覆蓋,所以還是3。

  1. 經典八股
    public void add() {
        int i = 3;
        i = i++ + ++i;
    }

字節碼,重點關注操作數棧和局部變量表變化,先忽略局部變量表下標為0的位置保存了this。

  public add()V
    ICONST_3    //棧3,表null
    ISTORE 1    //棧null,表3
    ILOAD 1     //棧3,表3
    IINC 1 1    //棧3,表4
    IINC 1 1    //棧3,表5
    ILOAD 1     //棧3、5,表5
    IADD        //棧8,表5
    ISTORE 1    //棧null,表8
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 2
}

看到這里,什么i = i++、i = ++i、i = i++ + ++i、i = ++i + i++還不是手到擒來,站在字節碼的角度看這些迷惑操作可以說是降維打擊了。

  1. 繼續強化一波
    public void add() {
        int i = 3;
        i = ++i + i++;
    }

對應字節碼

  public add()V
    ICONST_3  //棧3,表null
    ISTORE 1  //棧null,表3
    IINC 1 1  //棧null,表4
    ILOAD 1   //棧4,表4
    ILOAD 1   //棧4、4,表4
    IINC 1 1  //棧4、4,表5
    IADD      //棧8,表5
    ISTORE 1  //棧null,表8
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 2
}

過程有點不一樣,但結果還是8,到這肯定能看懂局部變量的聲明和一些算數運算了。那有的同學就會問,你這是只是局部變量啊,那全局變量、靜態變量執行流程也一樣嗎?別慌,拉出來溜溜看。

  1. 全局變量
public class Num {
    int i;

    public void add() {
        i = 3;
        i = ++i + i++;
    }
}

說實話,下面這段我建議大家自己跟一遍,沒看字節碼之前無法想象這么點代碼居然會生成這么多指令。

  public add()V
    ALOAD 0   //0是this,局部變量表的this加載到操作數棧
    ICONST_3  //常量3入棧
    //賦值給全局變量i,其實就等同于this.i = 3
    PUTFIELD com/chenxuan/code/Num.i : I
    ALOAD 0   //this入棧
    ALOAD 0   //this入棧
    DUP       //復制棧頂this,此時棧3個this
    //this出棧,獲取全局變量i入棧
    GETFIELD com/chenxuan/code/Num.i : I
    ICONST_1  //常量1入棧,此時棧this、this、i、1
    IADD      //1,i出棧執行加法,加法運算結果4入棧,此時棧this、this、4
    DUP_X1    //復制棧頂4,插入下標1,此時棧this、4、this、4
    //全局變量賦值,this.i = 4,此時棧this、4
    PUTFIELD com/chenxuan/code/Num.i : I
    ALOAD 0   //this入棧
    DUP       //復制this,此時棧this、4、this、this
    //全局變量i入棧,此時棧this、4、this、i,需注意i的值已經是4了
    GETFIELD com/chenxuan/code/Num.i : I
    DUP_X1    //復制棧頂i插入下標1,此時棧this、i、4、this、i
    ICONST_1  //常量1入棧,此時棧this、i、4、this、i、1
    IADD      //加法運算,此時棧this、i、4、this、5
    //this.i = 5,此時棧this、i、4,需注意棧內的i還是4
    PUTFIELD com/chenxuan/code/Num.i : I
    IADD      //加法運算,此時棧this、8
    //this.i = 8,操作數棧空了
    PUTFIELD com/chenxuan/code/Num.i : I
    RETURN
    //棧的最大深度,對應前面this、i、4、this、i、1
    MAXSTACK = 6
    MAXLOCALS = 1

GETFIELD、PUTFIELD很好理解,全局變量取值賦值;DUP、DUP_X1詳見下圖:

dup & pop

上述全局變量get、put都是通過this進行調用,而this默認保存在局部變量表的第零項。

  1. 方法調用
public class Num {
    public void a() {
        b();
    }

    public void b() {

    }
}

看下字節碼,方法內調用原類方法也是通過this,只不過這個this編譯器幫忙生成了。

  public a()V
    ALOAD 0  //this
    INVOKEVIRTUAL com/chenxuan/code/Num.b ()V
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 1

  public b()V
    RETURN
    MAXSTACK = 0
    MAXLOCALS = 1
方法調用指令
  1. invokevirtual
    用于調用對象的實例方法,支持多態。
  2. invokeinterface
    用于調用接口方法,運行時找到實現類的接口方法進行調用。
  3. invokespecial
    構造方法,私有方法,父類方法。
  4. invokestatic
    靜態方法或者說是類方法。
  5. invokedynamic
    Java8 lambda使用,在運行時動態解析出調用點限定符所引用的方法,并執行該方法。

this.b()就是對象實例方法了,所以調用指令為invokevirtualinvokevirtual、invokeinterface調用的方法都有可能會被重寫,其實也就是多態,子類方法表首先會繼承父類的方法,子類重寫父類方法時,在方法表中用自己的實現替代父類方法。調用這些方法時,先找實例類,然后查找方法表中對應的方法進行調用。invokespecial、invokestatic調用的方法都是靜態綁定的,不會被重寫。

android平臺的lambda有一個脫糖的過程,這也是插樁的老生常談了,簡單來說就是將invokedynamic的實現改為了匿名內部類的方式。

  1. 對象創建指令
public class Num {

    public void a() {
        Inner inner = new Inner();
    }

    static class Inner {

    }
}

字節碼

  public a()V
    //堆上創建對象實例,引用入棧
    NEW com/chenxuan/code/Num$Inner
    DUP       //復制引用到棧頂
    //調用構造方法,引用出棧
    INVOKESPECIAL com/chenxuan/code/Num$Inner.<init> ()V
    ASTORE 1  //引用出棧,存入局部變量表
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 2

對象創建的過程:先在堆上創建實例,對象引用push到操作數棧;dup復制一份引用,用來調用對象的構造方法;最后將剩余的引用存入局部變量,注意位置,0是當前對象this。

  1. 類型檢查指令
    public void a(View view) {
        if (view instanceof TextView) {
            ((TextView) view).setText("TextView");
        }
    }

字節碼

  public a(Landroid/view/View;)V
    ALOAD 1
    INSTANCEOF android/widget/TextView
    IFEQ L0
    ALOAD 1
    CHECKCAST android/widget/TextView
    LDC "TextView"
    INVOKEVIRTUAL android/widget/TextView.setText (Ljava/lang/CharSequence;)V
   L0
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 2

對應指令就是INSTANCEOF、CHECKCAST,比較好理解,此處條件指令IFEQ不滿足條件跳轉L0,方法return返回。思考一下kotlin的智能轉換,看一下kotlin的字節碼,為啥不需要在代碼中顯示as向下轉型。

  1. 自動轉型
    fun text(view: View) {
        if (view is TextView) {
            view.text = "TextView"
            view.textSize = 16f
        }
    }

字節碼,很容易想到編譯器幫忙加了CHECKCAST

  public final text(Landroid/view/View;)V
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
    ALOAD 1
    LDC "view"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
    ALOAD 1
    INSTANCEOF android/widget/TextView
    IFEQ L0
    ALOAD 1
    CHECKCAST android/widget/TextView
    LDC "TextView"
    CHECKCAST java/lang/CharSequence
    INVOKEVIRTUAL android/widget/TextView.setText (Ljava/lang/CharSequence;)V
    ALOAD 1
    CHECKCAST android/widget/TextView
    LDC 16.0
    INVOKEVIRTUAL android/widget/TextView.setTextSize (F)V
   L0
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 2

有個比較神奇的ab值交換,這個說實話,反編譯成Java也看不明白。

  1. ab交換
    public fun ab() {
        var a = 3;
        var b = 4;
        a = b.also {
            b = a
        }
    }

字節碼

  public final ab()V
    ICONST_0
    ISTORE 1
    ICONST_3
    ISTORE 1
    ICONST_0
    ISTORE 2
    ICONST_4
    ISTORE 2
    ILOAD 2
    ISTORE 3
    ILOAD 3
    ISTORE 4
    ICONST_0
    ISTORE 5
    ILOAD 1
    ISTORE 2
    NOP
    ILOAD 3
    ISTORE 1
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 6
}

MAXLOCALS為6,里面一堆操作,最終局部變量表1的值變成了4,2的值變成了3完成交換。

總結

說實話,八股歸八股,這些i++的例子拋開惡心不談,確實也讓我有所收獲,只能說記憶深刻。卷到這里,再回過頭去看kotlin的編譯產物、ASM插樁等,應該也是得心應手了。當然,最重要的是,技術對線不再掛機,大佬們在高談闊論的時候,我也可以插個嘴嘴=。=

那實用主義的同學可能就會說了,你卷這些有個雞兒用,對咱們的項目有一點幫助嗎?別急,看下boosterByteX這么多ASM處理對項目的優化是實實在在的,總歸要知其然知其所以然吧,總不能引入一下后續修改都無從下手吧。那booster、ByteX沒有的一些功能,項目中的一些痛點自己下來也能擼了,還不是美滋滋。

相關鏈接:
jit-compiler
dalvik-bytecode
JVM-bytecode
dex-format
class-format

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容