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結尾。)
2.1 java-class
首先讀取源碼,一個一個字節的讀取進來,找出來我們Java定義的關鍵字,比如if ,else,for,while,finally,等這個步驟就是叫做詞法分析過程
第二步:檢查第一步讀取出來的關鍵字是否符合Java語言規范,比如if后面跟的是不是一個Boolean類型的表達式,這個過程就叫做語法分析
第三步:經過以上2個步驟詞法分析,語法分析,基本上已經按照Java規范了,接下來就是這些拼裝的代碼要表達什么意思,也就是語義分析
注: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文件的時候,只要一份就可以了嘛。
2.3 dex-odex
odex文件就是dex文件具體在某個系統(不同手機,不同手機的OS,不同版本的OS等)上的優化。odex文件的優化依賴系統上的幾個核心模塊(由BOOTCLASSPATH環境變量給出,一般是/system/framework/下的jar包,尤其是core.jar),主要還是為了提高Dalvik虛擬機的運行速度,這部分內容了解即可。
3.內存管理
3.1物理內存
物理內存即移動設備上的RAM,當啟動一個Android程序時,會啟動一個Dalvik VM進程,系統會給它分配固定的內存空間(16M,32M不定),這塊內存空間會映射到RAM上某個區域。然后這個Android程序就會運行在這塊空間上。Java里會將這塊空間分成Stack棧內存和Heap堆內存。stack里存放對象的引用,heap里存放實際對象數據。
注:android使用了paging與memory-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。
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兩個調用,其中,前者就是用來創建進程的,而后者就是用來創建線程的。
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部分源碼分析
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函數是在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函數做成了什么事情:
創建了一個Hash表,用來存儲已經加載的類。
創建了代表java.lang.Class和所有基礎數據類型的Class信息。
processClassPath這個函數,它要加載所有的Boot Class,它涉及到system/framework/下的jar包的加載,加載完畢后,虛擬機啟動的流程差不多就完了。
接下來講的是Class的加載和初始化:
先調用dvmDexGetResolvedClass,看看目標類TestAnother是不是已經被解析過了。前面曾經提到說,一個類在初始化的時候可能會解析它所使用到的其他類。
假設被引用的類沒有解析過,則調用dvmResolveClass來加載目標類。
目標類加載成功后,如果該類沒有初始化過,則調用dvmInitClass進行初始化。
dvmResolveClass其主要邏輯就是先得到目標類名(Lcom/test/TestAnother;)然后調用dvmFindClassNoInit來加載目標類。
dvmFindClassNoInit其主要邏輯就是由于referrer的ClassLoader(也就是使用TestAnother類的TestMain類的ClassLoader)不為空,代碼邏輯將走到findClassFromLoaderNoInit。
findClassFromLoaderNoInit其主要邏輯就是調用java/lang/ClassLoader的loadClass函數來加載類。
加載成功后,接下來就是初始化了、dvmInitClass從函數名就能知道它的作用了。
這只是Dalvik虛擬機學習之路的一個簡易整理版本,如果你想深入學習Dalvik虛擬機,這些內容還是不夠的。還有很多東西并沒有講到,還需要大家繼續努力。
參考地址: