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

前言

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

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

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

正文

JVM 引擎執(zhí)行

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

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

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

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

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

說起方法執(zhí)行,就必須提及一個概念膳灶,棧幀咱士。

ShadowFrame棧幀

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

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

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

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

單單這么說概念斧抱,可能會不太明白。這里我們就挑出Android 7.0對應(yīng)的虛擬機(jī)中的對應(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 托管著一個當(dāng)前正在運(yùn)行的ShadowFrame。并能通過ManagedStack中的link 對象知道上一次線程切換時候的幀布局弛槐,從而進(jìn)行切換懊亡。

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

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

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

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

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

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

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

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

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

為了徹底弄懂棧幀的的內(nèi)存申請,讓我們把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安裝時通過dex2opt對apk包中dex文件進(jìn)行了優(yōu)化產(chǎn)物枝嘶,之后運(yùn)行App就加載odex文件帘饶,加速App響應(yīng)時間

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

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

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

而這幾個文件,最終都會加載到內(nèi)存中幕垦,對于Android高版本中丢氢,我們來討論oat文件以及art文件傅联。

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

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


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

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

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

其中mem_map 映射了art中的方法衅枫,屬性,類表等核心對象朗伶。

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


art-Heap.png

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

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

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

art-Oat文件.png

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

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

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

  • 3.DexFile OatDexFile 指向的dex偏移量就是指的DexFile對象溪北,這個對象其實指代了dex文件

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

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

  • 6.OatClass 則是ClassOffsets 指向的class對象之拨。其中包含了當(dāng)前的class的狀態(tài)(加載掏膏,校驗,準(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é)果對象腥刹。其中 CompileMethodvmap_table_ 定長數(shù)組是一個映射表马胧,反映了機(jī)器碼用到的物理寄存器到dex中虛擬寄存器的映射關(guān)系.

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

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

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

能看到實際上在上一片文章提到過的ArtMethod在這里可以看到關(guān)聯(lián)威彰。首先art文件加載進(jìn)來的ArtMethod結(jié)構(gòu)體內(nèi)含一個核心的字段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ù)據(jù)豹缀。

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

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

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

對于Java語言來說江滨,堆特指的是JVM虛擬機(jī)生成的Heap對象铛纬。JVM全局有且只有一個。所有的對象都會保存在堆的位圖中唬滑。每一個對象實際上都是屬于四驅(qū)模型中的堆告唆。

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

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

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

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

那么又衍生出一個新的問題途事,棧是如何管理局部變量的验懊。

虛擬機(jī)棧

關(guān)于虛擬機(jī)棧就是我們說的java方法的棧。上文已經(jīng)介紹了在解釋執(zhí)行中尸变,棧幀實際上指代的是ShadowFrame對象义图。這個對象對應(yīng)的內(nèi)存實際上是申請在棧中,如下圖:

art-ShadowFrame家族.png

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

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


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

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

dex指令碼

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

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

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字段對應(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)容最終都會壓縮在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

我們來看看對應(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

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

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

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

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

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

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

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

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

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

程序計數(shù)器

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

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

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

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

本地方法棧

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

方法執(zhí)行原理

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

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

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

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

方法存儲

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

方法執(zhí)行

當(dāng)查找到方法后汤求,就能確定這是否是一個重寫的方法。如果是重寫方法就會調(diào)用該類中所存儲的對應(yīng)方法严拒。也就是說重寫是在編譯時候決定扬绪。

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

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


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

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

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

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

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

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

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

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

小結(jié)

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市奄薇,隨后出現(xiàn)的幾起案子驳阎,更是在濱河造成了極大的恐慌,老刑警劉巖馁蒂,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呵晚,死亡現(xiàn)場離奇詭異,居然都是意外死亡沫屡,警方通過查閱死者的電腦和手機(jī)饵隙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沮脖,“玉大人金矛,你說我怎么就攤上這事∩捉欤” “怎么了驶俊?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長免姿。 經(jīng)常有香客問我饼酿,道長,這世上最難降的妖魔是什么胚膊? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任故俐,我火速辦了婚禮,結(jié)果婚禮上紊婉,老公的妹妹穿的比我還像新娘药版。我一直安慰自己,他們只是感情好肩榕,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布刚陡。 她就那樣靜靜地躺著惩妇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪筐乳。 梳的紋絲不亂的頭發(fā)上歌殃,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天,我揣著相機(jī)與錄音蝙云,去河邊找鬼氓皱。 笑死,一個胖子當(dāng)著我的面吹牛勃刨,可吹牛的內(nèi)容都是我干的波材。 我是一名探鬼主播,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼身隐,長吁一口氣:“原來是場噩夢啊……” “哼廷区!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起贾铝,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤隙轻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后垢揩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玖绿,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年叁巨,在試婚紗的時候發(fā)現(xiàn)自己被綠了斑匪。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡锋勺,死狀恐怖蚀瘸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宙刘,我是刑警寧澤苍姜,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布牢酵,位于F島的核電站悬包,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏馍乙。R本人自食惡果不足惜布近,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丝格。 院中可真熱鬧撑瞧,春花似錦、人聲如沸显蝌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至酬诀,卻和暖如春脏嚷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瞒御。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工父叙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肴裙。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓趾唱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蜻懦。 傳聞我的和親對象是個殘疾皇子甜癞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評論 2 348

推薦閱讀更多精彩內(nèi)容