效能優(yōu)化筆記 JVM引擎執(zhí)行總結(jié)

前言

本文繼續(xù)總結(jié)輝哥的第二周二三節(jié)課的總結(jié)。這兩節(jié)課主要還是對(duì)JVM引擎執(zhí)行,Gradle進(jìn)行了初步的介紹和學(xué)習(xí)。

注意正文中所有的對(duì)虛擬機(jī)的討論都是基于Android 7.0 art虛擬機(jī)。

如果遇到什么問題歡迎來到本文:http://www.itdecent.cn/p/084b636c5cc4 互相討論

正文

JVM 引擎執(zhí)行

JVM引擎是如何執(zhí)行的。這里我們以art虛擬機(jī)為例子。先來看看整個(gè)核心原理圖。

首先JVM執(zhí)行代碼分為如下兩種:

  • 1.解釋執(zhí)行,通俗一點(diǎn)的話語就是一遍執(zhí)行一遍翻譯字節(jié)碼。常常我們會(huì)把這種執(zhí)行方式和JIT(Just In time)聯(lián)系起來說。JIT是解釋執(zhí)行的核心機(jī)制,當(dāng)某個(gè)方法執(zhí)行的次數(shù)達(dá)到了閾值(也就是閾值)就會(huì)翻譯成機(jī)器碼

  • 2.機(jī)器碼執(zhí)行 直接是機(jī)器碼進(jìn)行本地執(zhí)行。其中一個(gè)核心的機(jī)制,也就是我們常說的AOT(Ahead Of Time), 運(yùn)行前編譯。也就是在安裝的時(shí)候,把代碼經(jīng)過PMS的DexManager 跨進(jìn)程執(zhí)行dex2oat翻譯成優(yōu)化過的本地機(jī)器碼,可以被機(jī)器識(shí)別。

對(duì)于ART虛擬機(jī)來說,其實(shí)整個(gè)過程較為復(fù)雜,但是值得稱贊的是,整個(gè)流程的封裝設(shè)計(jì)做的不錯(cuò),不糾結(jié)底層的細(xì)節(jié)理解起來沒有想象的困難。

說起方法執(zhí)行,就必須提及一個(gè)概念,棧幀。

ShadowFrame棧幀

棧幀(Stack Frame)是用于支持虛擬機(jī)方法執(zhí)行和調(diào)用的數(shù)據(jù)結(jié)構(gòu),他是虛擬機(jī)運(yùn)行時(shí)在數(shù)據(jù)區(qū)中虛擬機(jī)棧的棧元素。棧幀保存了方法的局部變量表,操作數(shù)棧,動(dòng)態(tài)鏈接和方法返回地址和一些額外信息。

當(dāng)一個(gè)方法從調(diào)用到運(yùn)行完畢,實(shí)際上是一個(gè)棧幀從入棧到出棧的過程。

在編譯執(zhí)行過程中,會(huì)在Class文件中保存好Code字段中記錄好這個(gè)方法需要多少個(gè)局部變量,多少操作棧,方法的參數(shù)個(gè)數(shù)。通過這些數(shù)據(jù)就能決定了在當(dāng)前虛擬機(jī)環(huán)境下,當(dāng)前棧幀內(nèi)存大小。

一個(gè)線程中虛擬機(jī)棧的棧幀可能很長,只有在棧頂?shù)臈攀腔钴S的狀態(tài),成為當(dāng)前棧幀。與這個(gè)棧幀關(guān)聯(lián)的方法稱為當(dāng)前方法。

單單這么說概念,可能會(huì)不太明白。這里我們就挑出Android 7.0對(duì)應(yīng)的虛擬機(jī)中的對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)來進(jìn)一步闡述棧幀為何物:

機(jī)器碼執(zhí)行相關(guān)的邏輯較為復(fù)雜和抽象,這里來聊聊解釋執(zhí)行中棧幀的原理。

在ART虛擬機(jī)中,棧幀指代的數(shù)據(jù)結(jié)構(gòu)為ShadowFrame。

 private:
  ShadowFrame(uint32_t num_vregs, ShadowFrame* link, ArtMethod* method,
              uint32_t dex_pc, bool has_reference_array)
      : link_(link), method_(method), result_register_(nullptr), dex_pc_ptr_(nullptr),
        code_item_(nullptr), number_of_vregs_(num_vregs), dex_pc_(dex_pc) {
    // TODO(iam): Remove this parameter, it's an an artifact of portable removal
    DCHECK(has_reference_array);
    if (has_reference_array) {
      memset(vregs_, 0, num_vregs * (sizeof(uint32_t) + sizeof(StackReference<mirror::Object>)));
    } else {
      memset(vregs_, 0, num_vregs * sizeof(uint32_t));
    }
  }

...
  // Link to previous shadow frame or null.
  ShadowFrame* link_;
  ArtMethod* method_;
  JValue* result_register_;
  const uint16_t* dex_pc_ptr_;
  const DexFile::CodeItem* code_item_;
  LockCountData lock_count_data_;  // This may contain GC roots when lock counting is active.
  const uint32_t number_of_vregs_;
  uint32_t dex_pc_;
  int16_t cached_hotness_countdown_;
  int16_t hotness_countdown_;

  // This is a two-part array:
  //  - [0..number_of_vregs) holds the raw virtual registers, and each element here is always 4
  //    bytes.
  //  - [number_of_vregs..number_of_vregs*2) holds only reference registers. Each element here is
  //    ptr-sized.
  // In other words when a primitive is stored in vX, the second (reference) part of the array will
  // be null. When a reference is stored in vX, the second (reference) part of the array will be a
  // copy of vX.
  uint32_t vregs_[0];

  DISALLOW_IMPLICIT_CONSTRUCTORS(ShadowFrame);
};
art-ShadowFrame家族.png

能看到,ShadowFrame 本質(zhì)上是被ManagerStack 管理所有的棧幀。

class PACKED(4) ManagedStack {
 public:
  ManagedStack()
      : top_quick_frame_(nullptr), link_(nullptr), top_shadow_frame_(nullptr) {}

  void PushManagedStackFragment(ManagedStack* fragment) {
    // Copy this top fragment into given fragment.
    memcpy(fragment, this, sizeof(ManagedStack));
    // Clear this fragment, which has become the top.
    memset(this, 0, sizeof(ManagedStack));
    // Link our top fragment onto the given fragment.
    link_ = fragment;
  }

  void PopManagedStackFragment(const ManagedStack& fragment) {
    DCHECK(&fragment == link_);
    // Copy this given fragment back to the top.
    memcpy(this, &fragment, sizeof(ManagedStack));
  }

 ...

 private:
  ArtMethod** top_quick_frame_;
  ManagedStack* link_;
  ShadowFrame* top_shadow_frame_;
};

ManagerStack 托管著一個(gè)當(dāng)前正在運(yùn)行的ShadowFrame。并能通過ManagedStack中的link 對(duì)象知道上一次線程切換時(shí)候的幀布局,從而進(jìn)行切換。

棧幀的創(chuàng)建與虛擬機(jī)的內(nèi)存劃分

上文說到棧幀屬于哪個(gè)數(shù)據(jù)區(qū)域。 我們回顧一下c++ 編程中的內(nèi)存四驅(qū)模型:

  • 1.棧區(qū):由編譯器自動(dòng)分配釋放,存放函數(shù)參數(shù),局部變量。
  • 2.堆區(qū):由編程者手動(dòng)分配釋放,如果不釋放則造成內(nèi)存泄露,只有進(jìn)程被銷毀時(shí),才被OS回收
  • 3.數(shù)據(jù)區(qū):存放全局變量,靜態(tài)變量,常量字符等。只能被OS回收
  • 4.代碼區(qū)域:存放代碼的區(qū)域

在java 虛擬機(jī)中內(nèi)存的劃分如下:

  • 1.方法區(qū)
  • 2.虛擬機(jī)棧
  • 3.本地方法棧
  • 4.堆
  • 5.程序計(jì)數(shù)器

這一塊我們通常稱為運(yùn)行時(shí)數(shù)據(jù)區(qū),也就是JVM內(nèi)存。

在這一塊內(nèi)存中又分為共享內(nèi)存,和線程私有內(nèi)存:

  • 共享內(nèi)存: 方法區(qū),堆
  • 線程私有內(nèi)存:虛擬機(jī)棧,本地方法棧,程序計(jì)數(shù)
art-java內(nèi)存劃分.png

我們現(xiàn)在只討論JVM在Android的行為。實(shí)際上JVM的內(nèi)存劃分必定是建立在c++編程中四驅(qū)模型的架構(gòu)上。

為了徹底弄懂棧幀的的內(nèi)存申請(qǐng),讓我們把JVM和四驅(qū)模型聯(lián)系起來吧。

apk編譯產(chǎn)物

先聊聊apk安裝編譯產(chǎn)物,當(dāng)dexManager通過dex2oat進(jìn)程把dex轉(zhuǎn)化為如下幾種文件:

art-dex2oat生成的產(chǎn)物.png
  • dex 是把jar包(內(nèi)含class文件)經(jīng)過壓縮后的產(chǎn)物

  • odex 則是在Android 5.0之前常用的,是App安裝時(shí)通過dex2opt對(duì)apk包中dex文件進(jìn)行了優(yōu)化產(chǎn)物,之后運(yùn)行App就加載odex文件,加速App響應(yīng)時(shí)間

  • oat 是在Android 5.0之后,安裝時(shí)候會(huì)對(duì)dex文件中的數(shù)據(jù)通過dex2oat翻譯成本地機(jī)器碼保存起來。本質(zhì)上是一個(gè)elf格式的文件

  • art 是Android安裝后生成的可選對(duì)象,一旦生成了則會(huì)獲取odex中部分常用內(nèi)容(長方法等)存儲(chǔ)到elf文件中。并保存到ImageSpace中,通過這種方式加速App的運(yùn)行效率。

  • vdex 是Android 8.0之后的產(chǎn)物。當(dāng)dex經(jīng)過了編譯的校驗(yàn)后,則會(huì)把校驗(yàn)后代碼方法都緩存到vdex中。

而這幾個(gè)文件,最終都會(huì)加載到內(nèi)存中,對(duì)于Android高版本中,我們來討論oat文件以及art文件。

art文件的內(nèi)存從屬

來看看art文件通過JVM加載到內(nèi)存是一個(gè)什么形式:


art-class_linker加載內(nèi)存布局與art文件的解析.png
art-Art2ImageSpace.png

art文件實(shí)際上在App應(yīng)用啟動(dòng)初期進(jìn)行初始化JVM時(shí)候,會(huì)加載到一個(gè)為ImageSpace的內(nèi)存空間中。這個(gè)內(nèi)存實(shí)際上是屬于c/c++四驅(qū)的堆,也就是說是通過malloc的方式為ImageSpace申請(qǐng)內(nèi)存。

當(dāng)然,ImageSpace中存在兩個(gè)十分重要字段:mem_maplive_bitmap。這兩個(gè)實(shí)際上是通過mmap系統(tǒng)調(diào)用把a(bǔ)rt文件中重要的數(shù)據(jù)字段映射到內(nèi)存中。

其中mem_map 映射了art中的方法,屬性,類表等核心對(duì)象。

當(dāng)生成了ImageSpace后,會(huì)把這個(gè)對(duì)象托付給JVM Heap對(duì)象進(jìn)行管理,而這個(gè)對(duì)象就是JVM中堆:


art-Heap.png

而堆本質(zhì)上也是通過malloc 從內(nèi)存中申請(qǐng)出來的,也是屬于c/c++四驅(qū)模型的堆一份子。

oat文件的內(nèi)存從屬

art文件只是作為輔助的作用,大部分的核心代碼都是由oat文件進(jìn)行管理。從apk安裝產(chǎn)物一圖可以得知,oat文件的結(jié)構(gòu)粗略是可以分2個(gè)區(qū)域:象征Oat文件頭部的OatHeader,以及象征Oat文件的內(nèi)容單元OatDexFile。OatDexFile在Oat中是數(shù)組的單元對(duì)象,他象征著被壓縮到oat文件中的dex文件。

art-Oat文件.png

這里面的結(jié)構(gòu)稍微有點(diǎn)復(fù)雜,但是還是可以一窺究竟:

  • 1.OatHeader 帶了Oat文件的魔數(shù),版本號(hào)等數(shù)據(jù),當(dāng)然也指向了Trampoline code 也就是只有boot.oat(framework.jar對(duì)應(yīng)的oat)文件才有的直接調(diào)準(zhǔn)方法

  • 2.OatDexFile 則是Dex文件本身經(jīng)過翻譯后,壓縮到文件中的數(shù)據(jù)。可以從圖中可以看到內(nèi)含了dex文件對(duì)應(yīng)的偏移量,class對(duì)應(yīng)的偏移量

  • 3.DexFile OatDexFile 指向的dex偏移量就是指的DexFile對(duì)象,這個(gè)對(duì)象其實(shí)指代了dex文件

  • 4.TypeLookupTable 是一個(gè)哈希表,這個(gè)哈希表目的是快速通過類名找到dex對(duì)應(yīng)class數(shù)組的偏移索引

  • 5.ClassOffsets 這是這個(gè)class二維偏移數(shù)組是指,第幾個(gè)dex的第幾個(gè)class

  • 6.OatClass 則是ClassOffsets 指向的class對(duì)象。其中包含了當(dāng)前的class的狀態(tài)(加載,校驗(yàn),準(zhǔn)備,解析,初始化),當(dāng)前class的編譯類型(是全部轉(zhuǎn)化成機(jī)器碼,還是部分轉(zhuǎn)化機(jī)器碼,還是壓根沒經(jīng)過轉(zhuǎn)化),class中的方法等等

  • 7.VmapTable 是指CompileMethod 中的 vmap_table_ 數(shù)組。CompileMethod 是dex2oat 進(jìn)程處理dex中java方法的結(jié)果對(duì)象。其中 CompileMethodvmap_table_ 定長數(shù)組是一個(gè)映射表,反映了機(jī)器碼用到的物理寄存器到dex中虛擬寄存器的映射關(guān)系.

  • 8.OatQuickMethodHeader數(shù)組 指向所有的Oat文件的方法,內(nèi)含當(dāng)前方法所需要的棧幀大小,vmap的偏移量,代碼段數(shù)據(jù)結(jié)構(gòu)的偏移量等信息。

這些所有的數(shù)據(jù)最終都會(huì)加載到四驅(qū)模型的堆內(nèi)存中。

art和oat的關(guān)系
art-Oat文件和art文件的關(guān)聯(lián).png

能看到實(shí)際上在上一片文章提到過的ArtMethod在這里可以看到關(guān)聯(lián)。首先art文件加載進(jìn)來的ArtMethod結(jié)構(gòu)體內(nèi)含一個(gè)核心的字段ptr_sized_fieldsentry_point_from_quick_compiled_code_ 指向真正保存代碼段的數(shù)據(jù)結(jié)構(gòu),也就是加載到內(nèi)存的Oat文件中的OatQuickMethodcode數(shù)組字段。

通過這樣層層遞進(jìn),才能找到方法中真正的執(zhí)行邏輯。

方法區(qū)

方法區(qū)和堆是可以進(jìn)行JVM內(nèi)跨線程共享,那么肯定是一些運(yùn)行時(shí)不變的數(shù)據(jù)。

方法區(qū)是用于存放class二進(jìn)制文件數(shù)據(jù),內(nèi)含虛擬機(jī)的類信息,靜態(tài)常量,常量數(shù)據(jù),編譯后代碼數(shù)據(jù)。

有了上一節(jié)的基礎(chǔ)知識(shí)后,就能清晰的明白方法區(qū)實(shí)際上就是指加載到四驅(qū)模型的堆中。

而實(shí)際上在JVM中,我們常說的堆并非是c/c++編程的堆,而是從四驅(qū)模型的堆中申請(qǐng)一塊對(duì)象名為Heap的才是Java堆。

對(duì)于Java語言來說,堆特指的是JVM虛擬機(jī)生成的Heap對(duì)象。JVM全局有且只有一個(gè)。所有的對(duì)象都會(huì)保存在堆的位圖中。每一個(gè)對(duì)象實(shí)際上都是屬于四驅(qū)模型中的堆。

肯定有的人覺得疑惑,那在方法內(nèi)申請(qǐng)出來的對(duì)象不應(yīng)該屬于在棧中嗎?從我們編程的角度看來似乎如此。實(shí)際上在解析字節(jié)碼并不會(huì)聯(lián)系代碼段的上下文,而是一個(gè)指令進(jìn)行一種操作。

當(dāng)發(fā)現(xiàn)new的操作的時(shí)候,就是NEW_INSTANCENEW_ARRAY申請(qǐng)對(duì)象和數(shù)組時(shí)候,都會(huì)調(diào)用不同內(nèi)存管理器的Alloc方法,從提前malloc出來的內(nèi)存區(qū)域獲取對(duì)象。

一個(gè)虛擬機(jī)會(huì)根據(jù)啟動(dòng)配置而生成不同的內(nèi)存管理器,而不同的內(nèi)存管理器就是會(huì)有不同的內(nèi)存回收策略(GC),如標(biāo)記-清除算法(MarkAndSwap),標(biāo)記-壓縮算法等。

關(guān)于內(nèi)存分配與回收這部分細(xì)節(jié),之后會(huì)有專題解析。

那么又衍生出一個(gè)新的問題,棧是如何管理局部變量的。

虛擬機(jī)棧

關(guān)于虛擬機(jī)棧就是我們說的java方法的棧。上文已經(jīng)介紹了在解釋執(zhí)行中,棧幀實(shí)際上指代的是ShadowFrame對(duì)象。這個(gè)對(duì)象對(duì)應(yīng)的內(nèi)存實(shí)際上是申請(qǐng)?jiān)跅V校缦聢D:

art-ShadowFrame家族.png

能看到ShadowFrame本質(zhì)上是通過placement new 方式為一個(gè)對(duì)象申請(qǐng)內(nèi)存。這種特殊的方式會(huì)從棧中申請(qǐng)內(nèi)存。

在虛擬棧中,有這個(gè)4個(gè)核心對(duì)象構(gòu)成:


art-虛擬機(jī)棧.png
  • 1.局部變量表 用于存儲(chǔ)方法中的局部變量
  • 2.操作數(shù)棧 進(jìn)行邏輯操作時(shí)候會(huì)對(duì)臨時(shí)變量進(jìn)行緩存,一旦一個(gè)操作需要完成則會(huì)彈出操作數(shù)棧
  • 3.動(dòng)態(tài)鏈表 是指把常量池中的數(shù)據(jù)鏈接到對(duì)應(yīng)的字段中
  • 4.返回地址 方法返回地址

要徹底理解這4個(gè)對(duì)象,我們需要進(jìn)一步的了解dex指令碼。

dex指令碼

dex指令碼和java字節(jié)碼有著不少的區(qū)別??梢赃@么看這兩者的關(guān)系,java編程生成jar包對(duì)應(yīng)的class文件此時(shí)內(nèi)容是java字節(jié)碼。而在Android編譯apk時(shí)候,則會(huì)取出這些代碼數(shù)據(jù),壓縮成dex字節(jié)碼。其中會(huì)把所有的java字節(jié)碼進(jìn)行轉(zhuǎn)化壓縮成dex字節(jié)碼。dex字節(jié)碼內(nèi)容格式更小更加利于apk的包體積大小。

dex字節(jié)碼更少,但是相對(duì)的記錄的信息多了。

class Test {
    private static int a = 88;
    private static String name = "1111";

    public static void test(String a){
        int b = 127;
        Log.e("aaa",b+"");
    }
}

只看Code字段對(duì)應(yīng)的字節(jié)碼:

  com.yjy.hellotest.Test();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 14: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/yjy/hellotest/Test;

  public static void test(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: bipush        127
         2: istore_1
         3: ldc           #2                  // String aaa
         5: new           #3                  // class java/lang/StringBuilder
         8: dup
         9: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        12: iload_1
        13: invokevirtual #5                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        16: ldc           #6                  // String
        18: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: invokestatic  #9                  // Method android/util/Log.e:(Ljava/lang/String;Ljava/lang/String;)I
        27: pop
        28: return
      LineNumberTable:
        line 19: 0
        line 20: 3
        line 21: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0     a   Ljava/lang/String;
            3      26     1     b   I

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        88
         2: putstatic     #10                 // Field a:I
         5: ldc           #11                 // String 1111
         7: putstatic     #12                 // Field name:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 15: 0
        line 16: 5
}

再來看看apk包壓縮之后,取出其中的dex文件,調(diào)用dexdump命令打印dex中所有的內(nèi)容,比較兩者之間的區(qū)別。

Class #1027            -
  Class descriptor  : 'Lcom/yjy/hellotest/Test;'
  Access flags      : 0x0000 ()
  Superclass        : 'Ljava/lang/Object;'
  Interfaces        -
  Static fields     -
    #0              : (in Lcom/yjy/hellotest/Test;)
      name          : 'a'
      type          : 'I'
      access        : 0x000a (PRIVATE STATIC)
    #1              : (in Lcom/yjy/hellotest/Test;)
      name          : 'name'
      type          : 'Ljava/lang/String;'
      access        : 0x000a (PRIVATE STATIC)
  Instance fields   -
  Direct methods    -
    #0              : (in Lcom/yjy/hellotest/Test;)
      name          : '<clinit>'
      type          : '()V'
      access        : 0x10008 (STATIC CONSTRUCTOR)
      code          -
      registers     : 1
      ins           : 0
      outs          : 0
      insns size    : 9 16-bit code units
11e854:                                        |[11e854] com.yjy.hellotest.Test.<clinit>:()V
11e864: 1300 5800                              |0000: const/16 v0, #int 88 // #58
11e868: 6700 9b2b                              |0002: sput v0, Lcom/yjy/hellotest/Test;.a:I // field@2b9b
11e86c: 1a00 df01                              |0004: const-string v0, "1111" // string@01df
11e870: 6900 9c2b                              |0006: sput-object v0, Lcom/yjy/hellotest/Test;.name:Ljava/lang/String; // field@2b9c
11e874: 0e00                                   |0008: return-void
      catches       : (none)
      positions     : 
        0x0000 line=15
        0x0004 line=16
      locals        : 

    #1              : (in Lcom/yjy/hellotest/Test;)
      name          : '<init>'
      type          : '()V'
      access        : 0x10000 (CONSTRUCTOR)
      code          -
      registers     : 1
      ins           : 1
      outs          : 1
      insns size    : 4 16-bit code units
11e878:                                        |[11e878] com.yjy.hellotest.Test.<init>:()V
11e888: 7010 243c 0000                         |0000: invoke-direct {v0}, Ljava/lang/Object;.<init>:()V // method@3c24
11e88e: 0e00                                   |0003: return-void
      catches       : (none)
      positions     : 
        0x0000 line=14
      locals        : 
        0x0000 - 0x0004 reg=0 this Lcom/yjy/hellotest/Test; 

    #2              : (in Lcom/yjy/hellotest/Test;)
      name          : 'test'
      type          : '(Ljava/lang/String;)V'
      access        : 0x0009 (PUBLIC STATIC)
      code          -
      registers     : 4
      ins           : 1
      outs          : 2
      insns size    : 25 16-bit code units
11e890:                                        |[11e890] com.yjy.hellotest.Test.test:(Ljava/lang/String;)V
11e8a0: 1300 7f00                              |0000: const/16 v0, #int 127 // #7f
11e8a4: 2201 9007                              |0002: new-instance v1, Ljava/lang/StringBuilder; // type@0790
11e8a8: 7010 573c 0100                         |0004: invoke-direct {v1}, Ljava/lang/StringBuilder;.<init>:()V // method@3c57
11e8ae: 6e20 5d3c 0100                         |0007: invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;.append:(I)Ljava/lang/StringBuilder; // method@3c5d
11e8b4: 1a02 0000                              |000a: const-string v2, "" // string@0000
11e8b8: 6e20 613c 2100                         |000c: invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@3c61
11e8be: 6e10 663c 0100                         |000f: invoke-virtual {v1}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@3c66
11e8c4: 0c01                                   |0012: move-result-object v1
11e8c6: 1a02 711e                              |0013: const-string v2, "aaa" // string@1e71
11e8ca: 7120 ac05 1200                         |0015: invoke-static {v2, v1}, Landroid/util/Log;.e:(Ljava/lang/String;Ljava/lang/String;)I // method@05ac
11e8d0: 0e00                                   |0018: return-void
      catches       : (none)
      positions     : 
        0x0000 line=19
        0x0002 line=20
        0x0018 line=21
      locals        : 
        0x0002 - 0x0019 reg=0 b I 
        0x0000 - 0x0019 reg=3 a Ljava/lang/String; 

  Virtual methods   -
  source_file_idx   : 6825 (Test.java)

這部分內(nèi)容最終都會(huì)壓縮在Dex文件的CodeItem結(jié)構(gòu)體中。我們來看看靜態(tài)構(gòu)造函數(shù):

    Code:
      stack=1, locals=0, args_size=0
         0: bipush        88
         2: putstatic     #10                 // Field a:I
         5: ldc           #11                 // String 1111
         7: putstatic     #12                 // Field name:Ljava/lang/String;
        10: return

bipush 把88壓入到操作數(shù)棧中,putstatic 則從操作數(shù)棧中彈出棧頂設(shè)置到a字段中。

art-操作數(shù)棧.png

我們來看看對(duì)應(yīng)的dex字節(jié)碼做了什么?

  Direct methods    -
    #0              : (in Lcom/yjy/hellotest/Test;)
      name          : '<clinit>'
      type          : '()V'
      access        : 0x10008 (STATIC CONSTRUCTOR)
      code          -
      registers     : 1
      ins           : 0
      outs          : 0
      insns size    : 9 16-bit code units
11e854:                                        |[11e854] com.yjy.hellotest.Test.<clinit>:()V
11e864: 1300 5800                              |0000: const/16 v0, #int 88 // #58
11e868: 6700 9b2b                              |0002: sput v0, Lcom/yjy/hellotest/Test;.a:I // field@2b9b
11e86c: 1a00 df01                              |0004: const-string v0, "1111" // string@01df
11e870: 6900 9c2b                              |0006: sput-object v0, Lcom/yjy/hellotest/Test;.name:Ljava/lang/String; // field@2b9c
11e874: 0e00                                   |0008: return-void

此時(shí)就能看到不同了,此時(shí)bipushjava字節(jié)碼轉(zhuǎn)化成const_16 dex字節(jié)碼,并把88壓倒操作棧中,最后通過sputdex字節(jié)碼把88從操作數(shù)棧彈出,設(shè)置到靜態(tài)字段a中。

操作數(shù)棧,局部變量表內(nèi)存劃分

當(dāng)有了上一節(jié)的基礎(chǔ)知識(shí)后,我們進(jìn)一步的來討論操作數(shù)棧和局部變量在其中的所屬的內(nèi)存已經(jīng)作用。

我們回顧圖ShadowFrame家族,這個(gè)圖中詳細(xì)的列舉了ShadowFrame幾個(gè)核心的對(duì)象。

vregs 這個(gè)int數(shù)組就是我們常說的操作數(shù)棧以及局部變量表。很奇妙是吧。

總的來說,java字節(jié)碼可以分為如下4類:

  • 1.*load_* 這種格式的字節(jié)碼代表從本地變量表加載到操作棧
  • 2.*store_* 則是把一個(gè)數(shù)值從操作棧讀取到本地變量表中
  • 3.*ipush,ldc_*,*const_* 是把一個(gè)常量讀取到操作棧中。
  • 4.putstatic等 把數(shù)值彈出操作棧并進(jìn)行后續(xù)的邏輯操作,如賦值給某個(gè)對(duì)象

而dex字節(jié)碼,經(jīng)過獲取到j(luò)ava字節(jié)碼后,就能獲得更加相信的信息,從而轉(zhuǎn)化為更加準(zhǔn)確dex字節(jié)碼。此時(shí)只有一個(gè)引用表 vregs。這個(gè)引用表可以作為局部變量以及操作數(shù)棧。其實(shí)這么設(shè)計(jì)也是可行的,無論是局部變量表還是操作數(shù)棧都是在運(yùn)行字節(jié)碼和解釋執(zhí)行時(shí)候的臨時(shí)結(jié)果的緩存內(nèi)存,只是hotspot虛擬機(jī)對(duì)這兩者的職責(zé)劃分更加詳細(xì),一個(gè)表控制局部變量,另一個(gè)表控制邏輯操作臨時(shí)結(jié)果。

因此操作數(shù)棧和局部變量表都是屬于四驅(qū)內(nèi)存的棧區(qū)域。

程序計(jì)數(shù)器

回顧圖ShadowFrame家族 .實(shí)際上程序計(jì)數(shù)器就是指shadowFrame中的dex_pc。每當(dāng)出現(xiàn)打斷的時(shí)候,就會(huì)終止一個(gè)ShadowFrame解析執(zhí)行其中的CodeItem.此時(shí)就會(huì)記錄當(dāng)前運(yùn)行的行數(shù)的dex_pc,當(dāng)下一次回到當(dāng)前的ShadowFrame繼續(xù)執(zhí)行,就會(huì)跳到dex_pc繼續(xù)執(zhí)行。

一般的,打斷當(dāng)前ShadowFrame的執(zhí)行有如下幾種情況:

  • 1.從解釋執(zhí)行跳到機(jī)器碼執(zhí)行
  • 2.跳轉(zhuǎn)到c/c++本地庫的代碼中執(zhí)行
  • 3.因時(shí)間片輪轉(zhuǎn),而切換線程執(zhí)行方法

由于dex_pc也是屬于ShadowFrame的一個(gè)字段,因此也是屬于棧區(qū)域

本地方法棧

本地方法棧,可以看成就是c/c++四驅(qū)模型中的棧區(qū)域,并沒有特指什么具體的對(duì)象。直接執(zhí)行c/c++中的代碼塊。

方法執(zhí)行原理

理解了棧幀ShadowFrame之后,再來看看在ART虛擬機(jī)中,方法是如何執(zhí)行的。

方法想要正確執(zhí)行,就需要知道當(dāng)前方法的所屬的類。從下圖:

art-虛擬機(jī)的啟動(dòng)后半段.png

可以的得知,ART虛擬機(jī)在加載Class的時(shí)候會(huì)查找所有的屬性和方法。在LoadMethod步驟的時(shí)候就會(huì)加載整個(gè)class中所有的方法和字段的信息。當(dāng)然此時(shí)只是當(dāng)前class的信息。

方法存儲(chǔ)

只有在LinkMethod和LinkField步驟的時(shí)候,才會(huì)把父類和接口的信息全部解析出來并且保存在vtable,embedded_table,methods_,iftable中。
其中可實(shí)例化對(duì)象只存在embedded_table 否則只存在iftable.
methods_保存了所有的方法。
embedded_tableiftable 只保存了接口方法??蓪?shí)例化對(duì)象可以通過embedded_table快速查找當(dāng)前類實(shí)現(xiàn)的接口方法。

方法執(zhí)行

當(dāng)查找到方法后,就能確定這是否是一個(gè)重寫的方法。如果是重寫方法就會(huì)調(diào)用該類中所存儲(chǔ)的對(duì)應(yīng)方法。也就是說重寫是在編譯時(shí)候決定。

如果這是一個(gè)重載方法,只有在運(yùn)行時(shí)候知道是此時(shí)需要調(diào)用的參數(shù)是什么?通過方法描述符從而決定是調(diào)用哪個(gè)方法。

方法是如何運(yùn)行的,可以先來看看這個(gè)后半段的示意圖。


art-方法跳轉(zhuǎn)后半段.png

整個(gè)核心流程可以分為如下幾個(gè)步驟:

  • 1.整個(gè)過程都會(huì)流轉(zhuǎn)到artQuickToInterpreterBridge中進(jìn)行集中處理。一旦虛擬機(jī)調(diào)用到這個(gè)方法后,就會(huì)從棧中生成一個(gè)棧幀對(duì)象,并把當(dāng)前這個(gè)棧幀對(duì)象放到ManagerStack中管理。

此時(shí)就ManagerStack決定了當(dāng)前棧幀為哪個(gè)線程的ShadowFrame.注意看下面一塊的示意圖,在線程的私有內(nèi)存中,保存著ManagerStack對(duì)象,這個(gè)對(duì)象保存的top_shadow_frame_就是我們常說的當(dāng)前棧幀。

  • 2.ExecuteSwitchImpl 這個(gè)方法就會(huì)開始解析ShadowFrame 棧幀中所對(duì)應(yīng)的代碼塊,也就是dex指令流。同時(shí)會(huì)記錄當(dāng)前的指令調(diào)用到了哪一行,也就是程序計(jì)數(shù)器。

  • 3.當(dāng)判斷到不是機(jī)器碼,是解釋執(zhí)行的時(shí)候。就會(huì)調(diào)用ArtInterpreterToInterpreterBridge方法繼續(xù)通過ExecuteSwitchImpl 解析下一個(gè)棧幀的指令流

  • 4.當(dāng)判斷到是機(jī)器碼,則調(diào)用ArtInterpreterToCompiledCodeBirdge,執(zhí)行機(jī)器碼

  • 5.調(diào)用ManagerStack的pop方法,把執(zhí)行完畢的當(dāng)前棧幀彈出。

小結(jié)

本文對(duì)JVM引擎的方法執(zhí)行做了一個(gè)初步的總結(jié)。能看到實(shí)際上我并沒有帶領(lǐng)大家一行行的看看ART虛擬機(jī)整體流程。是因?yàn)樘摂M機(jī)的內(nèi)容實(shí)在太多,沒有一個(gè)初步的概念對(duì)全局有一個(gè)認(rèn)識(shí),會(huì)迷失在其中。加上本文也是對(duì)輝哥課程進(jìn)行的一次小總結(jié),以及自己學(xué)習(xí)過的只是進(jìn)行的擴(kuò)展。

先到這里,我們對(duì)JVM是如何執(zhí)行方法的原理明白后,我們就可以開啟Gradle的總結(jié)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容