Dalvik虛擬機學習之路

1.Dalvik虛擬機和Java虛擬機的區別

Dalvik虛擬機使用的是dex(Dalvik Executable)格式的類文件,而Java虛擬機使用的是class格式的類文件。一個dex文件可以包含若干個類,而一個class文件只包括一個類。由于一個dex文件可以包含若干個類,因此它就可以將各個類中重復的字符串和其它常數只保存一次,從而節省了空間,這樣就適合在內存和處理器速度有限的手機系統中使用。

Dalvik虛擬機使用的指令是基于寄存器的,而Java虛擬機使用的指令集是基于堆棧的。

寄存器(Register),是中央處理器內的其中組成部分。寄存器是有限存貯容量的高速存貯部件,它們可用來暫存指令、數據地址。在中央處理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序計數器。在中央處理器的算術及邏輯部件中,包含的寄存器有累加器

每一個Android應用在底層都會對應一個獨立的Dalvik虛擬機實例,其代碼在虛擬機的解釋器下得以執行。

有一個特殊的虛擬機進程Zygote,他是虛擬機實例的孵化器。它在系統啟動的時候就會產生,它會完成虛擬機的初始化、庫的加載、預制類庫和初始化的操作。如果系統需要一個新的虛擬機實例,它會迅速復制自身,以最快的速度提供給系統。對于一些只讀的系統庫,所有虛擬機實例都和Zygote共享一塊內存區域。

2.Dalvik的工作流程

Dalvik虛擬機支持已轉換為.dex(即Dalvik Executable)格式的Java應用程序的運行,.dex格式是專為Dalvik設計的一種壓縮格式,適合內存和處理器速度有限的系統。(dx 是一套工具,可以將 Java .class 轉換成 .dex 格式. 一個dex檔通常會有多個.class。由于dex有時必須進行最佳化,會使檔案大小增加1-4倍,以ODEX結尾。)


Dalvik工作模型

2.1 java-class

首先讀取源碼,一個一個字節的讀取進來,找出來我們Java定義的關鍵字,比如if ,else,for,while,finally,等這個步驟就是叫做詞法分析過程

第二步:檢查第一步讀取出來的關鍵字是否符合Java語言規范,比如if后面跟的是不是一個Boolean類型的表達式,這個過程就叫做語法分析

第三步:經過以上2個步驟詞法分析,語法分析,基本上已經按照Java規范了,接下來就是這些拼裝的代碼要表達什么意思,也就是語義分析


class字節碼模型

注:Java源碼中的類名,方法名,變量名,居然都是以字符串形式存儲在常量池中。所以,圖class字節碼模型中的this_class和super_class分別指向兩個字符串,代表本類的名字和基類的名字。

常量池數組的元素類型

常量池結構

常量池常見類型:

CONSTANT_Utf8_info:就是字符串

CONSTANT_Class_info:類信息

CONSTANT_NameAndType_Info:用來描述方法/成員名以及類型信息的

Methodref_Info,InterfaceMethodref_Info,Fieldref_Info

用于描述方法、接口信息和成員變量。

Methodref_Info,InterfaceMethodref_Info,Fieldref_Info數據結構

常量池就不說這么多了,有興趣的就自行解析。解析方法為:javap -verbose xxxx.class

2.2 class-dex

前言:Android平臺中沒有直接使用Class文件格式,是因為早期的Anrdroid手機內存,存儲都比較小,而Class文件顯然有很多可以優化的地方,比如每個Class文件都有一個常量池,里邊存儲了一些字符串。一串內容完全相同的字符串很有可能在不同的Class文件的常量池中存在,這就是一個可以優化的地方。

傳統Class文件是一個Java源碼文件會生成一個.Class文件,而Android是把所有Class文件進行合并,優化,然后生成一個最終的class.dex,如此,多個Class文件里如果有重復的字符串,當把它們都放到一個dex文件的時候,只要一份就可以了嘛。


dex文件模型

2.3 dex-odex

odex文件就是dex文件具體在某個系統(不同手機,不同手機的OS,不同版本的OS等)上的優化。odex文件的優化依賴系統上的幾個核心模塊(由BOOTCLASSPATH環境變量給出,一般是/system/framework/下的jar包,尤其是core.jar),主要還是為了提高Dalvik虛擬機的運行速度,這部分內容了解即可。

odex文件模型

3.內存管理

3.1物理內存

物理內存即移動設備上的RAM,當啟動一個Android程序時,會啟動一個Dalvik VM進程,系統會給它分配固定的內存空間(16M,32M不定),這塊內存空間會映射到RAM上某個區域。然后這個Android程序就會運行在這塊空間上。Java里會將這塊空間分成Stack棧內存和Heap堆內存。stack里存放對象的引用,heap里存放實際對象數據。

注:android使用了pagingmemory-mapping(mmapping)的機制來管理內存。這意味著任何你修改的內存(無論是通過分配新的對象還是去訪問mmaped pages中的內容)都會貯存在RAM中,而且不能被paged out。因此唯一完整釋放內存的方法是釋放那些你可能hold住的對象的引用,當這個對象沒有被任何其他對象所引用的時候,它就能夠被GC回收了。只有一種例外是:如果系統想要在其他地方重用這個對象。

3.1.1Java Object Heap

Java Object Heap是用來分配Java對象的,也就是我們在代碼new出來的對象都是位于Java Object Heap上的。Dalvik虛擬機在啟動的時候,可以通過-Xms和-Xmx選項來指定Java Object Heap的最小值和最大值。可以通過ActivityManager類的成員函數getMemoryClass來獲得Dalvik虛擬機的Java Object Heap的最大值。Android應用程序進程能夠使用的最大內存指的是能夠用來分配Java Object的堆。

3.1.2Bitmap Memory?

它是用來處理圖像的。在3.0以及更高的版本中,Bitmap Memory就直接是在Java Object Heap中分配了,這樣就可以直接接受GC的管理。

3.1.3Native Heap

Native Heap就是在Native Code中使用malloc等分配出來的內存,這部分內存是不受Java Object Heap的大小限制的,也就是它可以自由使用,當然它是會受到系統的限制。但是有一點需要注意的是,不要因為Native Heap可以自由使用就濫用,因為濫用Native Heap會導致系統可用內存急劇減少,從而引發系統采取激進的措施來Kill掉某些進程,用來補充可用內存,這樣會影響系統體驗。

4.垃圾收集

Dalvik虛擬機可以自動回收那些不再使用了的Java Object,也就是那些不再被引用了的Java Object。垃圾自動收集機制將開發者從內存問題中解放出來,極大地提高了開發效率,以及提高了程序的可維護性。

Dalvik虛擬機使用Mark-Sweep算法來進行垃圾收集。顧名思義,Mark-Sweep算法就是為Mark和Sweep兩個階段進行垃圾回收。其中,Mark階段從根集(Root Set)開始,遞歸地標記出當前所有被引用的對象,而Sweep階段負責回收那些沒有被引用的對象。

當Dalvik虛擬機成功地在堆上分配一個對象之后,會檢查一下當前分配的內存是否超出一個閥值。

GC_FOR_MALLOC:表示是在堆上分配對象時內存不足觸發的GC。

GC_CONCURRENT:表示是在已分配內存達到一定量之后觸發的GC。

GC_EXPLICIT:表示是應用程序調用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號時觸發的GC。

GC_BEFORE_OOM:表示是在準備拋OOM異常之前進行的最后努力而觸發的GC。

GC工作模型

Dalvik虛擬機支持非并行和并行兩種GC。在圖中,左邊是非并行GC的執行過程,而右邊是并行GC的執行過程。它們的總體流程是相似的,主要差別在于前者在執行的過程中一直是掛起非GC線程的,而后者是有條件地掛起非GC線程。

第1步到第3步用于并行和非并行GC:

1.? 調用函數dvmSuspendAllThreads掛起所有的線程,以免它們干擾GC。

2.? 調用函數dvmHeapBeginMarkStep初始化Mark Stack,并且設定好GC范圍。

Mark Stack具體來說,當我們標記完成根集對象之后,就按照它們的地址從小到大的順序標記它們所引用的其它對象。假設有A、B、C和D四個對象,它的地址大小關系為A < B < C < D,其中,B和D是根集對象,A被D引用,C沒有被B和D引用。那么我們將依次遍歷B和D。當遍歷到B的時候,沒有發現它引用其它對象,然后就繼續向前遍歷D對象。發現它引用了A對象。按照遞歸的算法,這時候除了標記A對象是正在使用之外,還應該去檢查A對象有沒有引用其它對象,然后又再檢查它引用的對象有沒有又引用其它的對象,一直這樣遍歷下去。這樣就跟函數遞歸一樣。更好的做法是將對象A記錄在一個Mark Stack中,然后繼續檢查地址值比對象D大的其它對象。對于地址值比對象D大的其它對象,如果它們引用了一個地址值比它們小的其它對象,那么這些其它對象同樣要記錄在Mark Stack中。等到該輪檢查結束之后,再回過頭來檢查記錄在Mark Stack里面的對象。然后又重復上述過程,直到Mark Stack等于空為止。

3.? 調用函數dvmHeapMarkRootSet標記根集對象。

第4到第6步用于并行GC:

4.? 調用函數dvmClearCardTable清理Card Table。Card Table由Card組成,一個Card實際上就是一個字節,它的值要么是CLEAN,要么是DIRTY。因為接下來我們將會喚醒第1步掛起的線程。并且使用這個Card Table來記錄那些在GC過程中被修改的對象。

5.? 調用函數dvmUnlock解鎖堆。這個是針對調用函數dvmCollectGarbageInternal執行GC前的堆鎖定操作。

6.? 調用函數dvmResumeAllThreads喚醒第1步掛起的線程。

第7步用于并行和非并行GC:

7.? 調用函數dvmHeapScanMarkedObjects從第3步獲得的根集對象開始,歸遞標記所有被根集對象引用的對象。

第8步到第11步用于并行GC:

8.? 調用函數dvmLockHeap重新鎖定堆。這個是針對前面第5步的操作。

9.? 調用函數dvmSuspendAllThreads重新掛起所有的線程。這個是針對前面第6步的操作。

10. 調用函數dvmHeapReMarkRootSet更新根集對象。因為有可能在第4步到第6步的執行過程中,有線程創建了新的根集對象。

11. 調用函數dvmHeapReScanMarkedObjects歸遞標記那些在第4步到第6步的執行過程中被修改的對象。這些對象記錄在Card Table中。

第12步到第14步用于并行和非并行GC:

12. 調用函數dvmHeapProcessReferences處理那些被軟引用(Soft Reference)、弱引用(Weak Reference)和影子引用(Phantom Reference)引用的對象,以及重寫了finalize方法的對象。這些對象都是需要特殊處理的。

13. 調用函數dvmHeapSweepSystemWeaks回收系統內部使用的那些被弱引用引用的對象。

14. 調用函數dvmHeapSourceSwapBitmaps交換Live Bitmap和Mark Bitmap。執行了前面的13步之后,所有還被引用的對象在Mark Bitmap中的bit都被設置為1。而Live Bitmap記錄的是當前GC前還被引用著的對象。通過交換這兩個Bitmap,就可以使得當前GC完成之后,使得Live Bitmap記錄的是下次GC前還被引用著的對象。

第15步和第16步用于并行GC:

15. 調用函數dvmUnlock解鎖堆。這個是針對前面第8步的操作。

16. 調用函數dvmResumeAllThreads喚醒第9步掛起的線程。

第17步和第18步用于并行和非并行GC:

17. 調用函數dvmHeapSweepUnmarkedObjects回收那些沒有被引用的對象。沒有被引用的對象就是那些在執行第14步之前,在Live Bitmap中的bit設置為1,但是在Mark Bitmap中的bit設置為0的對象。

18. 調用函數dvmHeapFinishMarkStep重置Mark Bitmap以及Mark Stack。這個是針對前面第2步的操作。

第19步用于并行GC:

19. 調用函數dvmLockHeap重新鎖定堆。這個是針對前面第15步的操作。

第20步用于并行和非并行GC:

20. 調用函數dvmHeapSourceGrowForUtilization根據設置的堆目標利用率調整堆的大小。

第21步用于并行GC:

21. 調用函數dvmBroadcastCond喚醒那些等待GC執行完成再在堆上分配對象的線程。

第22步用于非并行GC:

22. 調用函數dvmResumeAllThreads喚醒第1步掛起的線程。

第23步用到并行和非并行GC:

23. 調用函數dvmEnqueueClearedReferences將那些目標對象已經被回收了的引用對象增加到相應的Java隊列中去,以便應用程序可以知道哪些引用引用的對象已經被回收了。


5.進程與線程管理

Dalvik虛擬機運行在Linux操作系統之上。我們知道,Linux操作系統并沒有純粹的線程概念,只要兩個進程共享同一個地址空間,那么就可以認為它們同一個進程的兩個線程。Linux操作系統提供了兩個fork和clone兩個調用,其中,前者就是用來創建進程的,而后者就是用來創建線程的。

Dalvik虛擬機線程的創建過程

5.1Thread.start

Thread類的成員函數start首先檢查成員變量hasBeenStarted的值是否等于true。如果等于true的話,那么就說明當前正在處理的Thread對象所描述的Java線程已經啟動起來了。一個Java線程是不能重復啟動的,否則的話,Thread類的成員函數start就會拋出一個類型為IllegalThreadStateException的異常。通過了上面的檢查之后,Thread類的成員函數start接下來就繼續調用VMThread類的靜態成員函數create來創建一個線程。

5.2VMThread.create

VMThread類的靜態成員函數create是一個JNI方法,它將Java層傳遞過來的參數獲取出來之后,就調用另外一個函數dvmCreateInterpThread來執行創建線程的工作。

5.3dvmCreateInterpThread

將用來描述新創建的Dalvik虛擬機線程的Native層的Thread對象保存在gDvm.threadList所描述的一個線程列表中,這是因為當前所有Dalvik虛擬機線程都保存在這個列表中。

將新創建的Dalvik虛擬機線程的狀態設置為THREAD_VMWAIT,使得新創建的Dalvik虛擬機線程繼續往前執行,這是因為新創建的Dalvik虛擬機線程將自己的狀態設置為THREAD_STARTING喚醒創建它的線程之后,又會等待創建它的線程通知它繼續往前執行。

5.4interpThreadStart

1. 調用函數prepareThread來初始化新創建的Dalvik虛擬機線程。

2. 將新創建的Dalvik虛擬機線程的狀態設置為THREAD_STARTING,以便其父線程,也就是創建它的線程可以繼續往前執行。

3. 通過一個while循環來等待父線程通知自己繼續往前執行,也就是等待父線程將自己的狀態設置為THREAD_VMWAIT。

4. 調用函數dvmCreateJNIEnv來為新創建的Dalvik虛擬機線程創建一個JNI環境。

5. 調用函數dvmChangeStatus將新創建的Dalvik虛擬機線程的狀態設置為THREAD_RUNNING,表示它正式進入運行狀態。

6. 如果此時gDvm.debuggerConnected的值等于true,那么就說明有調試器連接到當前Dalvik虛擬機來了,這時候就調用函數dvmDbgPostThreadStart來通知調試器新創建了一個線程。

7. 調用函數dvmChangeThreadPriority來設置新創建的Dalvik虛擬機線程的優先級,這個優先級值保存在用來描述新創建的Dalvik虛擬機線程的一個Java層Thread對象的成員變量priority中。

8. 找到Java層的java.lang.Thread類的成員函數run,并且通過函數dvmCallMethod來交給Dalvik虛擬機解釋器執行,這個java.lang.Thread類的成員函數run即為Dalvik虛擬機線程的Java代碼入口點函數。

9. 從函數dvmCallMethod返回來之后,新創建的Dalvik虛擬機線程就完成自己的使命了,這時候就可以調用函數dvmDetachCurrentThread來執行清理工作。

6.Dalvik部分源碼分析


start指令對應的代碼

jni_invocation.Init:初始化JNI相關的幾個重要函數。通過dlopen加載libdvm.so。看來每個Java進程都會有這個東西。這可是dalvik vm的核心庫。這個庫有很多API,我個人覺得如果了解libdvm.so的話,應該能干很多事情。這里就不講那么仔細了。

startVm:注意,它傳入了一個JNIEnv* env對象進去,當這個函數返回時,我們在JNI中天天見的JNIEnv對象就是這個東西。startVm是Dalvik VM的核心,該函數返回后,VM就基本就緒了。其實startVm方法做的事情就是初始化VM核心數據結構。講一下跟目前有相關的知識點,實際上,根據Java VM規范,類的唯一性由全路徑類名+定義它的ClassLoader兩者唯一確定。

startReg:注冊Android平臺中一些特有的JNI函數。有興趣的可以深入研究。

?
dvmStartup指令對應的代碼

dvmStartup函數是在startVm函數內的虛擬機創建的核心。

dvmStartup首先是解析參數,這些參數信息可能會傳給gDvm相關的成員變量。解析參數是由setCommandLineDefaults和processOptions來完成的。代碼就不看了,就講幾個關鍵參數。

gDvm.executionMode = kExecutionModeJit:如果定義的WITH_JIT宏,則執行模式是JIT模式。

gDvm.bootClassPathStr:由BOOTCLASSPATH環境變量提供。講一下BOOTCLASSPATH值指向是什么,system/framework下幾乎所有的jar包都被放在了BOOT CLASSPATH里。

gDvm.mainThreadStackSize = kDefaultStackSize。kDefaultStackSize值為16K,代表主線程的堆棧大小

gDvm.dexOptMode = OPTIMIZE_MODE_VERIFIED,用于控制odex操作,該參數表示只對verified的類進行odex。

接下來一堆startup函數中的重點,dvmClassStartup函數

dvmClassStartup函數相應的代碼

先講一下dvmClassStartup函數做成了什么事情:

創建了一個Hash表,用來存儲已經加載的類。

創建了代表java.lang.Class和所有基礎數據類型的Class信息。

processClassPath這個函數,它要加載所有的Boot Class,它涉及到system/framework/下的jar包的加載,加載完畢后,虛擬機啟動的流程差不多就完了。

接下來講的是Class的加載和初始化:

new-instance指令對應的代碼

先調用dvmDexGetResolvedClass,看看目標類TestAnother是不是已經被解析過了。前面曾經提到說,一個類在初始化的時候可能會解析它所使用到的其他類。

假設被引用的類沒有解析過,則調用dvmResolveClass來加載目標類。

目標類加載成功后,如果該類沒有初始化過,則調用dvmInitClass進行初始化。

dvmResolveClass其主要邏輯就是先得到目標類名(Lcom/test/TestAnother;)然后調用dvmFindClassNoInit來加載目標類。

dvmFindClassNoInit其主要邏輯就是由于referrer的ClassLoader(也就是使用TestAnother類的TestMain類的ClassLoader)不為空,代碼邏輯將走到findClassFromLoaderNoInit。

findClassFromLoaderNoInit其主要邏輯就是調用java/lang/ClassLoader的loadClass函數來加載類。

加載成功后,接下來就是初始化了、dvmInitClass從函數名就能知道它的作用了。

dvmInitClass函數對應的代碼

這只是Dalvik虛擬機學習之路的一個簡易整理版本,如果你想深入學習Dalvik虛擬機,這些內容還是不夠的。還有很多東西并沒有講到,還需要大家繼續努力。

參考地址:

http://www.infoq.com/cn/articles/android-in-depth-dalvik?utm_source=infoq&utm_campaign=user_page&utm_medium=link

http://blog.csdn.net/luoshengyang/article/details/8852432

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

推薦閱讀更多精彩內容