效能優(yōu)化筆記class 文件初識(shí)

前言

我一直覺(jué)得我的學(xué)習(xí)態(tài)度和方法很有問(wèn)題焚刺,不然也不會(huì)覺(jué)得自己走到一個(gè)奇怪的瓶頸。一個(gè)很特殊的怪圈,就是怎么學(xué)都達(dá)不到大廠的水準(zhǔn)和效率温学。從現(xiàn)在開始需要端正自己的態(tài)度,低姿態(tài)學(xué)習(xí)甚疟。學(xué)的多仗岖,不如學(xué)的牢固穩(wěn)妥。

后續(xù)的更新計(jì)劃览妖,只要加班不厲害轧拄,每周都會(huì)跟著輝哥的開課視頻寫一個(gè)效能筆記以及相關(guān)的擴(kuò)展知識(shí)總結(jié)。

關(guān)于socket的源碼解析以及jvm的源碼解析讽膏,甚至計(jì)劃中的RN的源碼解析(內(nèi)含修改RN通信機(jī)制檩电,做到定制化和自定義)和Flutter引擎解析 相關(guān)的分享文章。會(huì)放緩節(jié)奏府树,1-2周更新一次俐末。

輝哥第一部分的分享是Gradle解析和AMS插樁以及JVM源碼加載字節(jié)碼。第一課是jvm相關(guān)的知識(shí)挺尾,剛好我2020年一整年都零零散散的通讀了android art虛擬機(jī)的源碼鹅搪。雖然還有不少的不明白地方,但是大致的流程還是明白的遭铺,聽了輝哥的課程之后丽柿,發(fā)現(xiàn)輝哥的學(xué)習(xí)比我仔細(xì)多了恢准,在這里就和大家分享一二。 關(guān)于更加詳細(xì)的art虛擬機(jī)源碼思想和設(shè)計(jì)甫题,可以期待后續(xù)的jvm源碼解析篇章馁筐。

如果遇到什么問(wèn)題來(lái)到本文:http://www.reibang.com/p/d00db1a7d6b1 互相討論

正文

class 文件格式初識(shí)

既然聊到j(luò)vm,就不得不聊到class字節(jié)碼坠非。要認(rèn)識(shí)虛擬機(jī)的工作原理敏沉,首先要對(duì)class的字節(jié)碼有一個(gè)初步的認(rèn)識(shí)。

java是以class為單位進(jìn)行編譯到dex/odex中炎码。而jvm需要正確運(yùn)行應(yīng)用程序盟迟,經(jīng)過(guò)jvm初始化后,必須經(jīng)過(guò)如下dex文件中的class項(xiàng)到內(nèi)存中潦闲。

在聊class的皆在流程之前攒菠,我們需要對(duì)class文件有一定的了解。

整個(gè)class的文件結(jié)構(gòu)如下:

art-class文件結(jié)構(gòu).png

下面是一個(gè)具體的例子歉闰。讓我一點(diǎn)點(diǎn)分析看看辖众。

public class Test implements ITest {
    protected String name;

    public static void main(String[] args){

    }

    private void testPrivate(){
        name = "aaaa";
    }

    @Override
    public void test() {

    }
}

通過(guò)javap 命令解析上面java代碼對(duì)應(yīng)的class文件如下:

  Last modified 2021-1-14; size 629 bytes
  MD5 checksum 53794a254ed0673600201eac830d13c3
  Compiled from "Test.java"
public class com.pdm.spectrogram.Test implements com.pdm.spectrogram.ITest
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#24         // java/lang/Object."<init>":()V
   #2 = String             #25            // aaaa
   #3 = Fieldref           #4.#26         // com/pdm/spectrogram/Test.name:Ljava/lang/String;
   #4 = Class              #27            // com/pdm/spectrogram/Test
   #5 = Class              #28            // java/lang/Object
   #6 = Class              #29            // com/pdm/spectrogram/ITest
   #7 = Utf8               name
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/pdm/spectrogram/Test;
  #16 = Utf8               main
  #17 = Utf8               ([Ljava/lang/String;)V
  #18 = Utf8               args
  #19 = Utf8               [Ljava/lang/String;
  #20 = Utf8               testPrivate
  #21 = Utf8               test
  #22 = Utf8               SourceFile
  #23 = Utf8               Test.java
  #24 = NameAndType        #9:#10         // "<init>":()V
  #25 = Utf8               aaaa
  #26 = NameAndType        #7:#8          // name:Ljava/lang/String;
  #27 = Utf8               com/pdm/spectrogram/Test
  #28 = Utf8               java/lang/Object
  #29 = Utf8               com/pdm/spectrogram/ITest
{
  protected java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PROTECTED

  public com.pdm.spectrogram.Test();
    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 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/pdm/spectrogram/Test;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 17: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 26: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/pdm/spectrogram/Test;
}
SourceFile: "Test.java"

class文件所對(duì)應(yīng)的二進(jìn)制文件如下:

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: CA FE BA BE 00 00 00 33 00 1E 0A 00 05 00 18 08    J~:>...3........
00000010: 00 19 09 00 04 00 1A 07 00 1B 07 00 1C 07 00 1D    ................
00000020: 01 00 04 6E 61 6D 65 01 00 12 4C 6A 61 76 61 2F    ...name...Ljava/
00000030: 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 01 00 06 3C    lang/String;...<
00000040: 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F    init>...()V...Co
00000050: 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54    de...LineNumberT
00000060: 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69    able...LocalVari
00000070: 61 62 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73    ableTable...this
00000080: 01 00 1A 4C 63 6F 6D 2F 70 64 6D 2F 73 70 65 63    ...Lcom/pdm/spec
00000090: 74 72 6F 67 72 61 6D 2F 54 65 73 74 3B 01 00 04    trogram/Test;...
000000a0: 6D 61 69 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C    main...([Ljava/l
000000b0: 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 04    ang/String;)V...
000000c0: 61 72 67 73 01 00 13 5B 4C 6A 61 76 61 2F 6C 61    args...[Ljava/la
000000d0: 6E 67 2F 53 74 72 69 6E 67 3B 01 00 0B 74 65 73    ng/String;...tes
000000e0: 74 50 72 69 76 61 74 65 01 00 04 74 65 73 74 01    tPrivate...test.
000000f0: 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 09 54    ..SourceFile...T
00000100: 65 73 74 2E 6A 61 76 61 0C 00 09 00 0A 01 00 04    est.java........
00000110: 61 61 61 61 0C 00 07 00 08 01 00 18 63 6F 6D 2F    aaaa........com/
00000120: 70 64 6D 2F 73 70 65 63 74 72 6F 67 72 61 6D 2F    pdm/spectrogram/
00000130: 54 65 73 74 01 00 10 6A 61 76 61 2F 6C 61 6E 67    Test...java/lang
00000140: 2F 4F 62 6A 65 63 74 01 00 19 63 6F 6D 2F 70 64    /Object...com/pd
00000150: 6D 2F 73 70 65 63 74 72 6F 67 72 61 6D 2F 49 54    m/spectrogram/IT
00000160: 65 73 74 00 21 00 04 00 05 00 01 00 06 00 01 00    est.!...........
00000170: 04 00 07 00 08 00 00 00 04 00 01 00 09 00 0A 00    ................
00000180: 01 00 0B 00 00 00 2F 00 01 00 01 00 00 00 05 2A    ....../........*
00000190: B7 00 01 B1 00 00 00 02 00 0C 00 00 00 06 00 01    7..1............
000001a0: 00 00 00 0C 00 0D 00 00 00 0C 00 01 00 00 00 05    ................
000001b0: 00 0E 00 0F 00 00 00 09 00 10 00 11 00 01 00 0B    ................
000001c0: 00 00 00 2B 00 00 00 01 00 00 00 01 B1 00 00 00    ...+........1...
000001d0: 02 00 0C 00 00 00 06 00 01 00 00 00 11 00 0D 00    ................
000001e0: 00 00 0C 00 01 00 00 00 01 00 12 00 13 00 00 00    ................
000001f0: 02 00 14 00 0A 00 01 00 0B 00 00 00 35 00 02 00    ............5...
00000200: 01 00 00 00 07 2A 12 02 B5 00 03 B1 00 00 00 02    .....*..5..1....
00000210: 00 0C 00 00 00 0A 00 02 00 00 00 14 00 06 00 15    ................
00000220: 00 0D 00 00 00 0C 00 01 00 00 00 07 00 0E 00 0F    ................
00000230: 00 00 00 01 00 15 00 0A 00 01 00 0B 00 00 00 2B    ...............+
00000240: 00 00 00 01 00 00 00 01 B1 00 00 00 02 00 0C 00    ........1.......
00000250: 00 00 06 00 01 00 00 00 1A 00 0D 00 00 00 0C 00    ................
00000260: 01 00 00 00 01 00 0E 00 0F 00 00 00 01 00 16 00    ................
00000270: 00 00 02 00 17                                     .....

接下來(lái),我們對(duì)應(yīng)二進(jìn)制文件來(lái)探索和敬,class文件的格式凹炸。

  • 1.二進(jìn)制文件開頭CA FE BA BE 這2個(gè)16進(jìn)制是指class文件格式的標(biāo)示符號(hào)。

  • 2.接下來(lái)的00 00 00 33 是指版本號(hào)昼弟。其中0000 代表次版本號(hào)啤它,00 33代表主版本號(hào)這里是指51。51是指jdk 1.7私杜,00也就是次級(jí)版本號(hào)為0.所以是jdk 1.7.0

  • 3.接下來(lái)就是常量池部分蚕键,首先00 1E 是指常量池中與多少個(gè)常量救欧。1e就是30衰粹,在這里的class文件解析出來(lái)的常量池?cái)?shù)量一共是29.為什么要加1,其實(shí)這是計(jì)算機(jī)習(xí)慣笆怠,也是規(guī)范铝耻。jvm會(huì)為0號(hào)位置的常量池做保留。

常量池解析

接下來(lái)看看常量池內(nèi)容解析蹬刷,要解析二進(jìn)制中所代表的常量池,需要如下表格進(jìn)行輔助:


java常量池解析表.png

我們結(jié)合這個(gè)表格來(lái)解析上面我隨手寫的示例代碼:

Methodref 的解析

第一行是Methodref 也就是指java的方法瓢捉,所對(duì)應(yīng)的標(biāo)示位是0a也就是10.從表中可以得知,這一行所對(duì)應(yīng)的二進(jìn)制代碼也就是0A 00 05 00 18办成。

也就是上述class文件通過(guò)javap解析出來(lái)的#1 = Methodref #5.#24. 后面這個(gè)5和24是指后續(xù)的在常量池中位于第5位置和第24位置泡态。

看看第5和第24個(gè)位置:

#5 = Class              #28            // java/lang/Object

能看到第5行指向了第28行,也就是utf8 的字符串指向了Object 這個(gè)資源:

 #28 = Utf8               java/lang/Object

第24行能看到這是一個(gè)特殊的類型NameAndType 這里指向了第9行(<init>)字符串,以及第10行()V字符串

 #24 = NameAndType        #9:#10         // "<init>":()V

記錄Test的類繼承了Object對(duì)象迂卢,并且擁有一個(gè)無(wú)參構(gòu)造函數(shù)

String 的解析

第二行是

#2 = String             #25            // aaaa

這里是指String類型對(duì)應(yīng)表中就是08某弦,對(duì)應(yīng)就是二進(jìn)制表接下來(lái)的內(nèi)容08 00 19桐汤。最后19從16進(jìn)制轉(zhuǎn)化過(guò)來(lái)就是25.說(shuō)明指向了25行的常量數(shù)據(jù):

#25 = Utf8               aaaa

也就是utf8 的aaa。

說(shuō)明在這個(gè)class中靶壮,存在一個(gè)常量字符串aaaa

Fieldref 解析

常量池第三行是Fieldref 類型也就是class中的成員屬性類型怔毛。對(duì)應(yīng)在二進(jìn)制的內(nèi)容為09 00 04 00 1A09對(duì)應(yīng)說(shuō)明表中為 Fieldref 也就是成員變量的引用腾降。

 #3 = Fieldref           #4.#26         // com/pdm/spectrogram/Test.name:Ljava/lang/String;

能看到這個(gè)屬性類型拣度,指向了第4行+.+26行

#4 = Class              #27            // com/pdm/spectrogram/Test
#27 = Utf8               com/pdm/spectrogram/Test

第4行就是指這個(gè)類的包路徑

#26 = NameAndType        #7:#8          // name:Ljava/lang/String;
   #7 = Utf8               name
   #8 = Utf8               Ljava/lang/String;

第26行則是一個(gè)用NameAndType 記錄這是一個(gè)class中的成員類型

能看到最終指向了2個(gè)utf8的字符串螃壤,并合并成注釋中的一樣com/pdm/spectrogram/Test.name:Ljava/lang/String;

此時(shí)記錄的是抗果,在這個(gè)class類中,存在一個(gè)string類型的成員變量奸晴,其名字為name窖张。

Class 的解析

#4 = Class              #27            // com/pdm/spectrogram/Test

這部分對(duì)應(yīng)的是接下來(lái)二進(jìn)制文件中的07 00 1B07代表了class的內(nèi)容蚁滋。

第27行則是指下面這個(gè)utf8的字符串?dāng)?shù)據(jù)

#27 = Utf8               com/pdm/spectrogram/Test

這里則記錄了宿接,這個(gè)class文件中存在一個(gè)com/pdm/spectrogram/Test的class。其實(shí)就是指當(dāng)前這個(gè)測(cè)試類辕录。

Utf8 解析

#7 = Utf8               name

這一行根據(jù)表中的內(nèi)容可以的得知睦霎,utf8 對(duì)應(yīng)的標(biāo)示為01,而此時(shí)這個(gè)utf8所記錄的才是真正對(duì)應(yīng)的字符串內(nèi)容:01 00 04 6E 61 6D 65 這里面記錄的就是name 這個(gè)字符串

NameAndType 解析

我們來(lái)看看第24行:

#24 = NameAndType        #9:#10         // "<init>":()V

對(duì)應(yīng)的二進(jìn)制為0C 00 09 00 0A.0C會(huì)先作為標(biāo)示位被認(rèn)為是NameAndType類型。也就是帶著類型的名字走诞。而這里記錄的就是一個(gè)無(wú)參數(shù)的構(gòu)造函數(shù)的字符串拼接副女。

總結(jié)

實(shí)際上class 文件中的常量池,是以01 ~ 0C的區(qū)間為標(biāo)示位蚣旱,來(lái)識(shí)別class文件中所有的數(shù)據(jù)碑幅。這些數(shù)據(jù)可能是引用,可能是真實(shí)的字符串塞绿。注意只有01(utf8類型)類型才是真正承載的字符串的內(nèi)容, 其他都是被識(shí)別為引用沟涨,進(jìn)行嵌套解析。

那么問(wèn)題來(lái)了01~0C 區(qū)間會(huì)不會(huì)影響jvm 記錄一些特殊字符串异吻,導(dǎo)致class文件記錄缺失呢裹赴?

實(shí)際上并不會(huì),如果去查ascii表诀浪,就能巧妙的發(fā)現(xiàn)棋返,這個(gè)區(qū)間的acsii對(duì)應(yīng)的數(shù)據(jù),是一些鍵盤操作雷猪,而不會(huì)記錄在文本中睛竣。

而在class文件中,存儲(chǔ)占比最大的部分就是常量池求摇。因?yàn)樗薱lass中所有的字符串字典射沟。這么做也有一個(gè)很大的好處嫉你,把所有的字符串替換成引用保存在池子中,就能極大的減少一個(gè)class文件加載到內(nèi)存后的大小躏惋。這種設(shè)計(jì)十分常見幽污,在Android資源加載的專題中,也能看到實(shí)際上Android系統(tǒng)的AssetManager也是復(fù)用這一套體系簿姨。

有興趣可以閱讀我之前寫的文章:http://www.reibang.com/p/817a787910f2

解析Class的訪問(wèn)標(biāo)示位

由于已經(jīng)知道了整個(gè)字符池的總長(zhǎng)度距误,那么填充完常量池總長(zhǎng)度后。接下來(lái)解析Class的訪問(wèn)標(biāo)示位扁位,訪問(wèn)標(biāo)志位對(duì)應(yīng)的的權(quán)限如下

權(quán)限 字節(jié) 意義
ACC_PUBLIC 0x0001 public 權(quán)限
ACC_PRIVATE 0x0002 private權(quán)限
ACC_PROTECTED 0x0004 protected 權(quán)限
ACC_STATIC 0x0008 static 類型
ACC_FINAL 0x0010 final 權(quán)限
ACC_SYNCHRONIZED 0x0020 經(jīng)過(guò)monitor 鎖的區(qū)域
ACC_SUPER 0x0020 繼承了類或者接口
ACC_VOLATILE 0x0040 VOLATILE 修飾的字段
ACC_NATIVE 0x0100 java的native方法
ACC_INTERFACE 0x0200 接口標(biāo)志位
ACC_ABSTRACT 0x0400 抽象類

00 21 00 04 00 05 仔細(xì)來(lái)看看這一段准潭。

首先00 21 是指ACC_PUBLIC 的public的訪問(wèn)權(quán)限以及super的模式用于記錄當(dāng)調(diào)用了invokspecial 指令時(shí)候?qū)Ω割愡M(jìn)行處理(也就是實(shí)現(xiàn)了繼承)

接下來(lái)的00 04是指訪問(wèn)權(quán)限為ACC_PUBLIC + ACC_SUPER,且指向了常量池中4號(hào)引用也就是Test類域仇。

往后讀4個(gè)為00 05.轉(zhuǎn)化過(guò)來(lái)就是指一個(gè)指向了05的索引刑然。其實(shí)就是指Object類。這就是為什么java中所有的類都是繼承于Object對(duì)象暇务。因?yàn)樵诰幾g的時(shí)候泼掠,會(huì)把繼承的類寫入到class文件中。且可以知道Object對(duì)象實(shí)際上是public final 的權(quán)限垦细。

接口引用解析

在往后讀4個(gè):00 01 00 06 择镇。 首先01是指當(dāng)前只有1個(gè)接口對(duì)象,這個(gè)接口對(duì)象指向了常量池中的6號(hào)引用括改,也就是ITest的接口腻豌。

屬性引用

art_fields.png

對(duì)于class文件中,需要完整描述一個(gè)屬性字段嘱能,需要如上幾個(gè)內(nèi)容才能描述完整吝梅。

分別是:權(quán)限,字段名索引惹骂,字段描述符的索引苏携,屬性表(字段的賦值內(nèi)容)。

對(duì)應(yīng)到j(luò)avap的解析就是如下這一段:

  protected java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PROTECTED

在本文的案例析苫,就是接著接口解析后這一段二進(jìn)制00 01 00 04 00 07 00 08 00 00兜叨。

首先00 01 記錄當(dāng)前的有多少個(gè)字段。此時(shí)只有1個(gè)衩侥。04代表權(quán)限為protected00 07代表引用索引為7指向的utf8的name字符串矛物。08代表該屬性的描述符號(hào)Ljava/lang/String茫死。后面的00 00說(shuō)明所有的屬性數(shù)量和屬性信息都為0.

方法引用

解析完屬性之后,就會(huì)解析方法數(shù)量和方法表履羞,在本文中峦萎,通過(guò)javap解析得到如下結(jié)果:

  public com.pdm.spectrogram.Test();
    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 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/pdm/spectrogram/Test;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 17: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 26: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/pdm/spectrogram/Test;

想要正確的描述一個(gè)方法屡久,需要下面這些數(shù)據(jù)

art_methods.png

3個(gè)方法加上一個(gè)默認(rèn)的構(gòu)造函數(shù)一共4個(gè)方法侣背。 我們用默認(rèn)的構(gòu)造函數(shù)為例子對(duì)應(yīng)的二進(jìn)制如下:
00 04 00 01 00 09 00 0A 00 01 00 0B 00 00 00 2F

我們拆解出來(lái):00 04 是指一共有4個(gè)方法在這個(gè)class中咒劲。

00 01 代表java/lang/Object."<init>":()V 這是代表了父類的默認(rèn)構(gòu)造函數(shù)

00 09 代表<init>黍析;00 0A 代表()V 到這里就完成了對(duì)當(dāng)前class的默認(rèn)構(gòu)造函數(shù)的描述菲饼。

00 01 代表當(dāng)前的方法有1個(gè)屬性表

00 0B 代表常量池引用指向Code

接下來(lái)就是代碼的內(nèi)容了更鲁。

Code的解析

想要正確的解析Code露懒,就需要理解這個(gè)表:


code屬性表.png

00 00 00 2F 代表這個(gè)方法占用內(nèi)存大小為2F,也就是47字節(jié)贮缅。前面的00說(shuō)明默認(rèn)的構(gòu)造函數(shù)名指向占位的地方战秋。

接下來(lái)就是這個(gè)方法的Code內(nèi)容:

00 01 00 01 00 00 首先這里可以看成三個(gè)部分:stack=1, locals=1, args_size=0

分別代表 方法棧為1唇聘,局部屬性為1版姑,方法參數(shù)為0

接下來(lái)就是00 05 2A B7 00 01 B1

00 05 是指這個(gè)方法中包含了多少java指令。因此就能找到實(shí)際上整個(gè)方法的代碼就是指2A B7 00 01 B1這一段二進(jìn)制內(nèi)容迟郎。

依次了解一下這些指令代表什么:

  • 2A aload_0 是將第一個(gè)引用變量推出
  • B7 invokespecial 代表調(diào)用父類構(gòu)造函數(shù)
  • 00 不做任何事情
  • 01 將null推到棧頂
  • B1 調(diào)用return方法剥险,結(jié)束當(dāng)前的方法

類的加載流程

art-類的加載過(guò)程.png

當(dāng)然我們一般都是都只是籠統(tǒng)的把上圖中的藍(lán)色區(qū)域步驟歸納出來(lái):

  • 1.setup 和 Load 一般都是把這兩個(gè)一起說(shuō)成加載 裝載ClassLoader
  • 2.link 則是鏈接,內(nèi)含校驗(yàn)宪肖,準(zhǔn)備表制,解析class方法
  • 3.初始化 初始化靜態(tài)成員,靜態(tài)代碼控乾,以及靜態(tài)構(gòu)造函數(shù)(clint)

也就是常說(shuō)的:加載夫凸,校驗(yàn),準(zhǔn)備阱持,解析夭拌,初始化。

而下面這張圖衷咽,則完整的表示了jvm在運(yùn)行期間鸽扁,這5個(gè)步驟都做了什么?

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

我們配合上面2張圖來(lái)仔細(xì)聊聊jvm在這幾個(gè)步驟中都做了什么镶骗?

當(dāng)jvm 初始化好啟動(dòng)好jvm后桶现,并加載第一個(gè)線程完。就會(huì)執(zhí)行ClassLinker的DefineClass 方法開始加載class鼎姊。

加載class 文件
  • 1.首先會(huì)加載靜態(tài)成員變量
  • 2.加載非靜態(tài)成員變量
  • 3.加載direct方法
  • 4.加載代碼段

在這里需要提及一個(gè)概念骡和,在art虛擬機(jī)中會(huì)把方法 區(qū)分為三種:

  • 1.direct 方法,也稱為直接方法相寇。這種方法是指private訪問(wèn)權(quán)限慰于,static修飾方法,以及構(gòu)造函數(shù)唤衫。

  • 2.virtual 方法婆赠,也稱為虛方法。這種方法是指除了private佳励,static以及構(gòu)造函數(shù)之外的方法休里。不包含父類繼承的方法蛆挫。

  • 3.miranda 方法,也稱為米蘭達(dá)方法妙黍。這種方法是指那些繼承了抽象類或者接口而沒(méi)有實(shí)現(xiàn)的方法悴侵。最早的java虛擬機(jī)因?yàn)榫帉憜?wèn)題,導(dǎo)致無(wú)法找到這類型方法拭嫁。為了修復(fù)這種特殊類型的方法可免,會(huì)在Link鏈接階段,把這種方法保存到虛函數(shù)表中噩凹。

額外需要補(bǔ)充一點(diǎn)巴元,java虛擬機(jī)常用的5種調(diào)用方法指令:

  • invokestatic:用于調(diào)用靜態(tài)方法。
  • invokespecial:用于調(diào)用私有實(shí)例方法驮宴、構(gòu)造器逮刨,以及使用 super 關(guān)鍵字調(diào)用父類的實(shí)例方法或構(gòu)造器,和所實(shí)現(xiàn)接口的默認(rèn)方法堵泽。
  • invokevirtual:用于調(diào)用非私有實(shí)例方法修己。
  • invokeinterface:用于調(diào)用接口方法。
  • invokedynamic:用于調(diào)用動(dòng)態(tài)方法迎罗。

通過(guò)這些了解后睬愤,就能明白實(shí)際上加載,也并非把一口氣的方法都加載到內(nèi)存中纹安,而是分批進(jìn)行加載尤辱。而這個(gè)階段的完成,會(huì)為這個(gè)class打上一個(gè)kStatusLoaded標(biāo)志位厢岂,避免重復(fù)加載同一個(gè)class文件光督。

而加載代碼段和加載方法看起來(lái)有沖突。實(shí)際上不是如此塔粒,從我javap中可以得知java方法是指一個(gè)方法引用结借,而代碼段是指代碼引用(內(nèi)含相關(guān)的虛擬機(jī)指令)。

對(duì)應(yīng)在class編譯過(guò)程中是兩個(gè)不同的結(jié)構(gòu)體進(jìn)行存儲(chǔ)卒茬,一個(gè)是method_item船老,一個(gè)code_item

這個(gè)過(guò)程圃酵,會(huì)把java方法存放到方法引用表中,而每一個(gè)方法的又指向了每一個(gè)方法的代碼段結(jié)構(gòu)體柳畔,這個(gè)UML圖就是如下設(shè)計(jì):

art-ArtMethod.png

從數(shù)據(jù)結(jié)構(gòu)上來(lái)看,加載到內(nèi)存的class結(jié)構(gòu)體辜昵,會(huì)有一個(gè)methods的數(shù)組指針荸镊,指向一塊內(nèi)存。這一塊內(nèi)存按照順序堪置,依次中保存了direct躬存,virtual,miranda.

而這個(gè)數(shù)組并非直接指向了ArtMethod結(jié)構(gòu)體,而是先指向了PtrSizeField結(jié)構(gòu)體后舀锨,再通過(guò)該結(jié)構(gòu)體的entry_point_from_quick_compiled_code_指向真正的ArtMethod結(jié)構(gòu)體岭洲。這么做的好處什么呢?

這么做其實(shí)就是為了區(qū)分坎匿,是aot(機(jī)器碼執(zhí)行)還是jit(解釋執(zhí)行)的區(qū)別盾剩。如果是jit 則是走jit的指令翻譯流程,如果是機(jī)器碼則走機(jī)器碼的指令執(zhí)行流程替蔬。

關(guān)于更多的內(nèi)容告私,可以關(guān)注我未來(lái)寫的java虛擬機(jī) 方法是如何執(zhí)行的源碼分析篇章。

校驗(yàn) class文件
  • 文件格式的校驗(yàn):校驗(yàn)class文件的格式和對(duì)應(yīng)的java版本是否符合規(guī)范
  • 元數(shù)據(jù)校驗(yàn):對(duì)類的元數(shù)據(jù)信息進(jìn)行校驗(yàn)承桥,保證不會(huì)出現(xiàn)不符合java規(guī)范的元數(shù)據(jù)
  • 字節(jié)碼校驗(yàn):對(duì)類的方法體進(jìn)行校驗(yàn)驻粟,保證不會(huì)出現(xiàn)危害java虛擬機(jī)的行為出現(xiàn)
  • 符號(hào)引用校驗(yàn):這個(gè)階段發(fā)生在鏈接的第三個(gè)階段解析 后打上的.主要是保證解析過(guò)程可以正確的執(zhí)行。比如說(shuō)凶异,能否通過(guò)類導(dǎo)入的import 全類名路徑找到對(duì)應(yīng)類蜀撑,訪問(wèn)其他類的方法和字段是否存在,且是否有對(duì)應(yīng)的訪問(wèn)權(quán)限剩彬。

那么對(duì)應(yīng)到第二副圖中酷麦,也就是指VerifyClass方法。這個(gè)方法會(huì)調(diào)用MethodVerifier.VerifyMethods校驗(yàn)每一個(gè)方法.

當(dāng)解析和初始化完畢之后喉恋,就會(huì)給class打上kStatusVerify標(biāo)志位沃饶。確定已經(jīng)校驗(yàn)完畢的避免再讓class重新走一遍校驗(yàn)的流程。

注意class的校驗(yàn)分為兩個(gè)步驟:

  • 1.一個(gè)是dex2oat安裝時(shí)候預(yù)編譯校驗(yàn)上述的軟錯(cuò)誤轻黑。而這個(gè)步驟已經(jīng)校驗(yàn)了90%的class中的校驗(yàn)問(wèn)題糊肤。如果成功也會(huì)給這個(gè)class打上一個(gè)kStatusVerified

  • 2.另一個(gè)是加載class 發(fā)現(xiàn)是一個(gè)需要泛型才能處理的class文件。此時(shí)才會(huì)等到app運(yùn)行后苔悦,第一次加載class獲取到上下文后轩褐,在進(jìn)行一次校驗(yàn)。

而上圖中的VerifyClass 放在初始化后面玖详,這是java虛擬機(jī)做的最后一道保險(xiǎn)措施把介。在初始化后,會(huì)看看有沒(méi)有這個(gè)kStatusVerified標(biāo)志位蟋座,沒(méi)有再一次校驗(yàn)拗踢。

class的準(zhǔn)備
  • 會(huì)為靜態(tài)屬性字段申請(qǐng)內(nèi)存,不包含非靜態(tài)字段向臀。非靜態(tài)字段只會(huì)在是在實(shí)例化對(duì)象后才進(jìn)行分配

  • 初始化class的靜態(tài)變量(也稱為類變量)時(shí)候巢墅,沒(méi)有任何賦值,則為其設(shè)置默認(rèn)的值。

  • 對(duì)于常量君纫,會(huì)在編譯階段保存在字段表的ConstantValue中驯遇。當(dāng)準(zhǔn)備階段結(jié)束之后就把讓對(duì)應(yīng)的常量指定為對(duì)應(yīng)常量池中的數(shù)據(jù)。

對(duì)應(yīng)在流程圖的過(guò)程蓄髓,就是對(duì)應(yīng)LinkSuperclass叉庐,LinkMethods,LinkStaticFields会喝,LinkInstanceFields 計(jì)算需要多少空間陡叠。

既然聊到了class在這個(gè)階段中為靜態(tài)變量分配內(nèi)存,class的準(zhǔn)備階段和實(shí)例化階段申請(qǐng)的內(nèi)存有何不同呢肢执?可以看看如下一圖:

art-Class內(nèi)存分布.png

能看到靜態(tài)變量是跟著加載到內(nèi)存class文件對(duì)應(yīng)的對(duì)象枉阵。而實(shí)例化對(duì)象中的非靜態(tài)變量則是跟著通過(guò)class實(shí)例化對(duì)象走的。

因此兩者不是同一個(gè)東西预茄,要區(qū)分兴溜。一個(gè)對(duì)象在jvm/art虛擬機(jī)中,實(shí)際上會(huì)存在一個(gè)加載到內(nèi)存的class對(duì)象反璃,會(huì)存在多個(gè)通過(guò)class對(duì)象實(shí)例化出來(lái)的對(duì)象昵慌。

當(dāng)計(jì)算兩者內(nèi)存大小時(shí)候,靜態(tài)屬性淮蜈,靜態(tài)方法都要算入class對(duì)象中斋攀。而實(shí)例化對(duì)象需要算上父類對(duì)應(yīng)的實(shí)例化的大小

class的解析
  • class的解析并沒(méi)有嚴(yán)格規(guī)定時(shí)間。只規(guī)定了在執(zhí)行newarray,new,putstatic,getfield,getstatic等16個(gè)指令之前梧田,需要對(duì)他們的所引用的符號(hào)進(jìn)行解析淳蔼。所以可以在類被虛擬機(jī)加載后解析,也能在調(diào)用這幾個(gè)指令之前被解析

  • 對(duì)于同一個(gè)符號(hào)可以進(jìn)行多次解析裁眯。而且多次解析鹉梨。除了invokedynamic以外,虛擬機(jī)可以對(duì)解析的結(jié)果進(jìn)行緩存穿稳。

  • 解析行為主要是面對(duì)類或者接口存皂,字段,類方法逢艘,接口方法旦袋,方法類型,方法句柄和調(diào)用的點(diǎn)限定符它改,7種類型疤孕。

對(duì)應(yīng)在流程圖的過(guò)程,就是對(duì)應(yīng)就是在校驗(yàn)完class和方法之后央拖。如果沒(méi)有打上解析的標(biāo)志位kStatusResolved祭阀,就會(huì)調(diào)用ClassLinkerResolve方法開始解析class中所有的方法鹉戚,字段。

class 的初始化

  • 初始化靜態(tài)構(gòu)造函數(shù)(類構(gòu)造函數(shù))<clinit>专控。這個(gè)過(guò)程會(huì)按照java文件中 編寫的順訊一次執(zhí)行靜態(tài)代碼塊抹凳,初始化靜態(tài)變量。

  • 在子類<clinit>靜態(tài)構(gòu)造函數(shù)執(zhí)行之前踩官,會(huì)默認(rèn)的執(zhí)行父類的靜態(tài)構(gòu)造函數(shù)

  • 因?yàn)楦割惖撵o態(tài)構(gòu)造函數(shù)優(yōu)先執(zhí)行却桶,因此父類比起子類會(huì)優(yōu)先執(zhí)行靜態(tài)代碼段

  • 如果一個(gè)類境输,不存在靜態(tài)變量蔗牡,不存在靜態(tài)方法。那么就不會(huì)存在靜態(tài)構(gòu)造函數(shù)嗅剖。

  • 接口不能存在靜態(tài)代碼塊辩越,但是會(huì)存在靜態(tài)變量。但是接口的靜態(tài)構(gòu)造函數(shù)的調(diào)用不會(huì)調(diào)用父類的靜態(tài)構(gòu)造函數(shù)信粮,除非使用了父類的靜態(tài)變量黔攒。同時(shí)接口的實(shí)現(xiàn)類也不會(huì)調(diào)用接口的靜態(tài)構(gòu)造函數(shù)

  • class的初始化只會(huì)執(zhí)行一次,因?yàn)闀?huì)在內(nèi)存中為這個(gè)class文件打上一個(gè)kStatusInitialized標(biāo)志位强缘。并且只會(huì)保證一個(gè)線程執(zhí)行一次該類的靜態(tài)構(gòu)造函數(shù)督惰。

class 的加載時(shí)機(jī)

實(shí)際上class的加載觸發(fā),實(shí)際上都是因?yàn)檎{(diào)用的虛擬機(jī)下一個(gè)ClassLinker的類旅掂,并調(diào)用的DefineClass方法赏胚。

常見場(chǎng)景有:

  • 1.調(diào)用new指令
  • 2.調(diào)用getstatic,putstatic,invokestatic 調(diào)用靜態(tài)方法或者操作靜態(tài)屬性
  • 3.反射調(diào)用類,會(huì)通過(guò)ClassLinker查找后商虐,找到并沒(méi)有緩存則裝載
  • 4.實(shí)例化一個(gè)子類觉阅,發(fā)現(xiàn)父類并沒(méi)有加載
  • 5.當(dāng)使用 JDK 1.7 的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè) java.lang.invoke.MethodHandle 實(shí)例最后的解析結(jié)果 REF_getStatic秘车、REF_putStatic典勇、REF_invodeStatic 的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒(méi)有進(jìn)行過(guò)初始化叮趴,則需要先觸發(fā)其初始化割笙。

jvm的雙親委派模型

    protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
        synchronized(this.getClassLoadingLock(var1)) {
            Class var4 = this.findLoadedClass(var1);
            if (var4 == null) {
                long var5 = System.nanoTime();

                try {
                    if (this.parent != null) {
                        var4 = this.parent.loadClass(var1, false);
                    } else {
                        var4 = this.findBootstrapClassOrNull(var1);
                    }
                } catch (ClassNotFoundException var10) {
                }

                if (var4 == null) {
                    long var7 = System.nanoTime();
                    var4 = this.findClass(var1);
                    PerfCounter.getParentDelegationTime().addTime(var7 - var5);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
                    PerfCounter.getFindClasses().increment();
                }
            }

            if (var2) {
                this.resolveClass(var4);
            }

            return var4;
        }
    }

何為雙親委派機(jī)制。聽起來(lái)的很玄乎眯亦,從上述代碼看一看就知道伤溉,實(shí)際上是當(dāng)前的classLoader在加載class的時(shí)候,并不會(huì)先從當(dāng)前的ClassLoader中查找搔驼,而是先從更加上層的classLoader中查找谈火。

關(guān)于這一點(diǎn),我在橫向淺析Small,RePlugin兩個(gè)插件化框架一文中和大家簡(jiǎn)單的聊過(guò)舌涨。

也在Android 重學(xué)系列 ActivityThread的初始化 一文中簡(jiǎn)單的聊過(guò)在Application初始化時(shí)候會(huì)調(diào)用LoadedApk.makeApplication 裝載應(yīng)用對(duì)應(yīng)PathClassLoader糯耍。

在這里有一個(gè)總結(jié)圖:

art-ClassLoader.png

致謝

最后感謝紅橙Darren 的文章以及授課扔字,以及本文文章的相關(guān)出處:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市温技,隨后出現(xiàn)的幾起案子革为,更是在濱河造成了極大的恐慌,老刑警劉巖舵鳞,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件震檩,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蜓堕,警方通過(guò)查閱死者的電腦和手機(jī)抛虏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)套才,“玉大人迂猴,你說(shuō)我怎么就攤上這事”嘲椋” “怎么了沸毁?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)傻寂。 經(jīng)常有香客問(wèn)我息尺,道長(zhǎng),這世上最難降的妖魔是什么疾掰? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任搂誉,我火速辦了婚禮,結(jié)果婚禮上个绍,老公的妹妹穿的比我還像新娘勒葱。我一直安慰自己,他們只是感情好巴柿,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布凛虽。 她就那樣靜靜地躺著,像睡著了一般广恢。 火紅的嫁衣襯著肌膚如雪凯旋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天钉迷,我揣著相機(jī)與錄音至非,去河邊找鬼。 笑死糠聪,一個(gè)胖子當(dāng)著我的面吹牛荒椭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播舰蟆,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼趣惠,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼狸棍!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起味悄,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤草戈,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后侍瑟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體唐片,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年涨颜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了费韭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡咐低,死狀恐怖揽思,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情见擦,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布羹令,位于F島的核電站鲤屡,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏福侈。R本人自食惡果不足惜酒来,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肪凛。 院中可真熱鬧堰汉,春花似錦、人聲如沸伟墙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)戳葵。三九已至就乓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拱烁,已是汗流浹背生蚁。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留戏自,地道東北人邦投。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像擅笔,于是被迫代替她去往敵國(guó)和親志衣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子见芹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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