前言
我一直覺(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)如下:
下面是一個(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.03.接下來(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)行輔助:
我們結(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 1A
。 09
對(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 1B
。 07
代表了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
的接口腻豌。
屬性引用
對(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)限為protected
, 00 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ù)
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è)表:
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)前的方法
類的加載流程
當(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è)步驟都做了什么?
我們配合上面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ì):
從數(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)存有何不同呢肢执?可以看看如下一圖:
能看到靜態(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)用ClassLinker
的Resolve
方法開始解析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é)圖:
致謝
最后感謝紅橙Darren 的文章以及授課扔字,以及本文文章的相關(guān)出處: