虛擬機、內存、垃圾回收

計算機執行流程

硬盤:存儲exe、class、dex文件,這些文件存儲的就是指令碼、類信息;

內存:把硬盤中的指令碼複製到內存中,類加載進來,生成對象、調用棧等;

程序計數器:記錄當前執行指令的地址,當前指令執行完成後,計算單元通知程序計數器,程序計數器指向下一條指令的地址;

寄存器:把當前執行棧的指令、參數地址和返回值地址複製到寄存器,根據程序計數器的指示,交給計算單元處理;

計算單元:接收寄存器的值和操作,執行計算,計算的中間值存在寄存器中。計算完成後,把最終值傳給寄存器,寄存器傳給內存,同時,通知程序計數器;

高速緩存/三級緩存:把一些常用的指令、常量存儲在高速緩存中,就不用每次都去內存取,節省了大量時間;

32/64位:上面的每個過程,都涉及到數據從一個地方傳輸到另一個地方,每次傳輸4個字節(32位)的處理器就是32位處理器,每次傳輸8個字節的就是64位處理器。指令集設計、文件存儲時會根據傳輸的最小單位來決定最小單位;

class文件相關

class文件:java虛擬機能識別的可執行文件,包含類信息、常量、jvm字節碼等信息。java、jRuby、Groovy語言編寫的程序,經過對應的編譯器編譯,都能得到class文件;

calss文件的信息協議:class文件由16進制數表示,最小單位是1字節(8位),協議定義了第m-n字節代表x屬性,第m-n字節代表y屬性。。。,如果某個屬性需要很多個不確定的字節表示,開頭就會有2個字節表示這個屬性有x個字節,那麼接下來x個字節就是這個屬性的具體值。其中,有個code屬性,存儲的是class字節碼,就是虛擬機能識別的機器碼,如果這個類是個抽象類、接口,可能就沒有code屬性。class文件文件中記錄的類、方法、變量都是使用唯一標識符,稱爲符號引用,運行時纔會通過虛擬機分配內存,纔會把符號引用轉換爲對應的地址引用;

class字節碼:java虛擬機能讀懂的指令,由於jvm是基於棧的架構,所以字節碼指令只有操作,沒有操作數(參數)。比如,對一個int型數據進行add操作,add是操作,int是操作數,如果沒有操作數,就要用addInt、addFloat等等多個操作來表示。但是字節碼只佔1個字節,最多隻能表示255種不同指令,所以有些類型的操作是不支持的,同時,float、boolean等類型數據都會轉爲對應的int值,使用addInt操作

解釋執行和編譯執行:編譯執行就是編譯器生成的就是本地機器碼,編譯期間就確定了內存分配、哪個指令放在哪個寄存器中執行、參數地址、目標地址等,執行的時候就指令直接從內存複製到寄存器。解釋執行就是編譯器生成的是虛擬機指令碼,程序安裝到硬盤、加載到內存中的指令也是虛擬機指令碼(ART在安裝的時候會把虛擬機指令碼轉爲本地機器碼),執行的時候由虛擬機解釋指令碼,然後調用本地機器碼指令去完成具體的邏輯

基於棧的指令集和基於寄存器的指令集:基於寄存器的指令集,編譯出來的是本地機器碼,一條指令中包含操作、參數、返回值等信息,並且已經確定了哪些指令放在哪些寄存器中執行。基於棧的指令集只有操作指令,沒有參數、返回值、地址等,也不指定在哪些寄存器中執行。

優缺點比較:
1.基於棧的指令,不指定寄存器,可以跨平臺。基於寄存器的不行;
2.基於棧的指令,編譯器不需要考慮空間分配,實現更簡單。基於寄存器的編譯器實現比較複雜;
3.基於棧的指令,實現在內存中,是每條指令都要去內存中讀取,頻繁的內存訪問效率很低。基於寄存器的一次性複製多條指令到寄存器中,不需要頻繁訪問內存;
4.基於棧的指令,佔用空間更少,但指令條數更多,意味着更多的cpu計算次數;基於寄存器的cpu效率更高;
5.基於棧的指令,可以在運行時優化(即JIT技術),根據運行時的一些信息進行更有效率的優化,比如某個同步鎖在執行過程中發現根本用不上,就不用釋放鎖。基於寄存器的,在編譯的時候就要指定哪條指令用哪個寄存器,需要在編譯的時候優化;
6…雖然看上去基於寄存器的指令集效率更高,但是也有一些專業的測試表明基於棧的java虛擬機比dalvik效率更高,所以實際情況還得實際分析;

java虛擬機公有規範和私有實現:java虛擬機規範規定了jvm必須正確地識別class文件、正確地執行class字節碼,但是並沒有限制如何實現。虛擬機開發者可以自由地實現,使效率更高、內存佔用更小,其中,有兩個方向可以挖掘:①將class字節碼在加載或執行時,翻譯成另一種虛擬機指令集,使用另一種更好的虛擬機實現。比如2.2之前的dalvik;②將class字節碼在加載時,翻譯成宿主機CPU指令集(即JIT技術)。比如ART

java類加載

虛擬機類加載流程:加載、驗證、準備、解析、初始化

加載:通過全限定名獲取對應的class文件(二進制流),將class文件中的靜態數據轉換爲運行時的數據結構,存在內存中的方法區,同時生成Class對象(注意,是類的代表,不是類的各個實例,目的是訪問類中的靜態方法、靜態成員用),作爲這個類的靜態數據訪問入口

驗證:因爲虛擬機可以接收任何來源的class文件(不一定是編譯出來的),有些“錯誤”雖然不會通過編譯,但有可能是人爲生成的class文件,驗證的目的是保證class文件符合虛擬機規範,不會對虛擬機造成影響。驗證需要做很多工作,比較重要的4點是:檢查class文件格式、檢查數據規範、檢查指令、檢查符號引用。檢查class文件格式就是按照class文件結構對其進行檢查,這一步通過後,就會把class數據轉換爲運行時數據存在方法區,檢查其中的數據有沒有問題(有沒有父類、final變量有沒有隨意改變),數據檢查完成後檢查指令有沒有問題,最後檢查符號引用是不是唯一的、是否能夠準確找到對應的引用

準備:正式爲類的靜態變量(static類型)分配內存和初始值,這個初始值是變量類型的默認值(int是0,boolean是false),真正跟代碼相關的值,是在類初始化(即初始化階段)的時候分配;

解析:主要是將之前的符號引用轉換爲實際引用

初始化:這個階段是真正開始執行java代碼(即calss文件中的指令碼)了,該賦值的賦值,該執行的執行

類加載時機:虛擬機規範並沒有規定什麼時候開始執行加載,只是規定了“需要使用的時候,類需要初始化完成,如果沒有,就立即開始加載——初始化的過程”,需要使用就是指new對象、讀/寫變量、反射等

熱更新/動態加載:上面的5個步驟,只有“加載”是可以自定義實現的,其它步驟都是虛擬機實現的。類加載的本質就是要讀取一段calss文件二進制流,可以採用各種方法去完成。

java虛擬機中的內存分爲幾個區域:程序計數器、虛擬機棧、本地方法棧、方法區和堆。程序計數器是記錄指令地址和跳轉的,每個線程都由一個獨立的PC計數器。虛擬機棧是保存局部變量(基本類型就是本身值,對象類型就是保存引用)、方法出口等信息,也是每個線程都獨立的,如果請求的棧深度太大會拋異常。本地方法棧和虛擬機棧的作用一樣,對應的是本地方法。

方法區:方法區是線程共享的,保存類相關的信息、類和code的描述、常量池等,如果類太多也會拋異常。這個區域的內存回收依賴於虛擬機的實現,有的虛擬機放在堆內存的永久代中,有的的單獨管理的。

堆:所有的對象都是分配在這個區域,當new一個對象時,這個對象需要的內存大小是編譯時就已知的,會在堆中找出一塊長度足夠的區域分配給該對象,初始化值是0,等到init方法時纔會賦值。對於不連續的內存,會有一個表,類似目錄那樣去記錄。

內存回收:PC計數器、虛擬機棧和本地方法棧的內存在編譯時就確定了的,隨線程創建而生,隨線程而滅,基本不存在內存回收的問題。方法區和堆的內存則不一樣,一個類的不同實現需要的內存不一樣,一個方法的不同分支佔用內存也不一樣,需要等到運行時才能確定,這部分內存的創建和回收都是動態的

哪些內存需要回收(標記垃圾):有一種比較廣泛的說法是“引用計數”來確定是否是垃圾,但是如果相互引用就永遠無法回收了,事實證明,目前主流的商用虛擬機都不是這種方法,而是採用可達性標記。可達性分析的基本思路是,通過一些列GC Roots對象作爲起點,這些Roots引用到達不了的就是垃圾,一般可以作爲Roots的對象有幾種:方法區中常量引用的對象、方法區static類型成員、虛擬機棧和本地方法棧中引用的對象

如何回收(清除垃圾):1.直接清除,就是把標記的內存置爲默認值,爲了避免內存碎片,把剩下的內存移到一起。2.複製算法,把存活的對象拎出來,複製到另一塊空間中,將此區域全部清空,這種算法效率高,但空間利用率不高。現代商用虛擬機普遍採用分代收集算法,把內存分爲新生代和老年代,根據不同代的特點,選擇不同的收集算法。新生代會產生大量對象,GC時80%以上的新生對象都會被回收,剩下的少數對象就移到青年代,經過多次GC都沒有回收的就會移到老年代,而方法區中的類信息、常量,由於很少回收,所以直接在老年代。對於新生代這種,因爲每次需要複製的對象(存活的)很少,所以採用複製算法,對於老年代,對象比較穩定,採用整理-清除比較省空間;

何時回收:進行垃圾標記的時候,需要保證此時不會有對象的分配變化、引用變化,要不然標記的就不準確了。有兩種方法:搶先式中斷和主動式中斷。搶先式就是GC線程去把其它線程中斷,如果有線程在“不安全”點上,就恢復它,等它跑到安全點再執行。主動式中斷就是當GC線程想要執行GC時設置一個標識,其它線程在某些點會去輪詢GC線程的標識,如果發現GC想要執行,就主動掛起,等GC執行完了恢復。

強引用:顯示賦值的引用,只要引用還在就永遠不會回收;
軟引用:SoftReference,系統將要發生內存溢出時,會把弱引用對象標記爲垃圾進行一次GC,如果內存還是不足就會OOM;
弱引用:WeakReference,GC的時候,無論內存是否充足,都回回收弱引用對象;
虛引用:PhantomReference,GC的時候回回收,虛引用的對象也無法獲得實例,唯一的作用是這個對象被回收時會收到一個系統通知;

jvm、dalvik和art比較

1.文件對比
jvm:識別class文件,具體情況見上文。

dalvik:識別dex文件,dex文件是把多個class文件集中到一起,常量、公共類庫信息只需要保持一份,空間效率更高。apk安裝過程中會把dex文件優化成odex文件,主要是對指令做一些優化。o

art:識別dex文件,在apk安裝過程中,會把20%左右指令集翻譯成本地機器碼,文件格式轉爲oat,但是文件名仍然是odex文件,路徑不變。oat文件中包含原dex文件和dex文件對應的本地機器指令。程序運行時,先解析dex文件找到對應的類/方法,然後通過索引找到對應的本地機器指令。因爲一次性把全部代碼優化爲本地機器碼非常耗時,所以安裝時只優化常用的20%,後面在運行過程中動態優化。

2.字節碼執行對比:
jvm:執行class字節碼,class字節碼是基於棧的指令集,程序運行過程中,jvm從內存中讀取字節碼執行;

dalvik:執行dex字節碼,dex字節碼是基於寄存器的指令集,運行過程中,dalvik從內存中讀取字節碼執行。雖然dalvik是基於寄存器的指令集,只是指令帶有源地址和目標地址,並不會把當前棧整個複製到寄存器中去;

art:執行本地機器碼,基於寄存器的指令集,程序安裝時把dex字節碼中常用的指令翻譯成本地機器碼,程序運行時,從內存中讀取本地機器碼執行。對於翻譯成本地機器碼的指令,運行時會把當前執行棧複製到寄存器中,執行時計算單元直接從寄存器中取指令;

dalvik垃圾回收
1.dalvik虛擬機的內存空間,老羅的分析中只分析了堆區,推測和java虛擬機一樣,分爲程序計數器、虛擬機棧、本地方法棧、方法區和堆區,但是dalvik虛擬機把堆區分爲了java堆、native堆和bitmap堆。對於dalvik虛擬機內存回收的討論都是基於堆區的討論;

2.zygote進程的虛擬機分爲zygote堆和active堆,zygote堆中存放android系統核心類庫、java核心類庫等公共資源,active堆爲空。fork一個進程時,複製一個虛擬機,只會把空active堆複製過去,zygote堆是公共可讀堆,其它進程也可以訪問。當其它進程需要堆zygote堆進行寫操作時,會使用寫時拷貝技術把zygote堆複製過去,這樣就不會影響zygote進程的公共堆了;

3.root對象:java全局變量、當前運行棧的局部變量、JNI全局變量、常量池中的String對象;

4.垃圾標記:標記過程中爲了保證標記的準確性,又要兼顧程序終止的時間,所以分爲兩個階段,第一個階段需要終止其它線程,第二個階段不需要。階段1:標記出成員變量、當前運行棧局部變量和寄存器引用的對象,作爲root對象;階段2:以root對象爲根節點遍歷其它被引用的對象。階段2允許其它線程執行,這樣可能會帶來對象引用關係的變化,如果有變化,在階段2結束後會終止其它線程,堆這些變化的對象進行重新標記,由於階段2的時間很短,此期間變化的對象比較少,所以重新標記也很快,對程序造成的影響非常小;

5.GC有四種類型:內存不足觸發的GC、拋OOM前的GC、內存達到閾值的GC、顯示調用的GC。當一個對象分配失敗時,會啓動GC,根據參數決定是內存不足GC還是OOM的GC;當一個對象分配成功後,會檢查當前內存數值,如果超過閾值則會喚醒GC線程,GC線程5000毫秒輪詢一次,是否需要執行GC;如果虛擬機允許顯示調用GC,調用System.gc和VMRuntime.gc就會觸發GC;

6.上面四種類型的GC會有幾個參數來區分,是否回收zygote堆、是否回收軟引用對象、是否是並行GC;

art垃圾回收
1.art虛擬機內存空間分爲image空間、zygote堆、active堆和largeObject空間,在dalvik的基礎上新增了image空間和largeObject空間。art虛擬機在安裝過程中,dex翻譯成oat文件,除了翻譯出本地機器碼,還會創建需要預加載的系統類對象,zygote進程虛擬機啓動時,會將這些對象映射到內存中。需要創建新的進程時,active堆會複製過去,zygote根據需要會通過寫時拷貝技術複製,而image空間不會複製。image空間中保存的是一些預先創建的對象,這些對象之間可能會互相引用,所以地址是固定的,這個空間在運行時是不能改變的(不會回收,也不會分配新對象);

2.art的垃圾收集器分爲3種:收集active堆的收集器、收集active和zygote堆的收集器、收集(自上次收集以來)新分配的對象。收集器需要實現5個接口,也分別對應着垃圾收集的5個步驟:初始化、標記(並行或非並行)、處理標記過程種變化的對象(如果是並行的纔有這步)、回收、結束。

3.上面所說的這些內存分配和回收,都是通過C庫提供的內存管理接口來實現的,而largeObject空間是虛擬機自己維護一個FreeList,創建大對象時,爲這個對象找一個快合適的空閒內存來分配,釋放後將釋放的內存添加到FreeList中去;

4.art在dalvik基礎上的優化:1.區分了更多的區,可以只回收固定的區,將影響降到最低;2.功能更加細分的收集器,比如只收集新生對象,影響降到最低;3.二次標記過程中使用的記錄表緩存起來重複使用;4.專門的LargeObject區和分配回收策略,避免分配大內存時的頻繁GC

5.內存碎片會導致分配一個稍微大一點的對象時分配失敗,引起OOM,android從5.0開始支持Compacting GC,即在垃圾標記、回收時,增加整理/壓縮步驟。壓縮Compacting GC需要進行整理和壓縮,效率會比標記清除GC效率低,但是可以通過結合使用,提升體驗,比如應用在前臺的時候使用標記清除,在後臺的時候使用壓縮;

6.採用Compacting GC,在分配對象失敗導致OOM前,會對堆空間進行壓縮整理,然後再嘗試分配;

android虛擬機相關知識點
1.android系統啓動的時候,會創建zygote進程,創建一個dalvik虛擬機實例,這個虛擬機會將java核心庫加載進來。需要創建其它進程時,會複製zygote進程中的虛擬機,並共享zygote進程中的java核心庫。這種模式,新的應用進程創建快,並且能共享java核心庫,節省內存空間;

2.java虛擬機定義了3個接口:獲取虛擬機參數、創建虛擬機、獲取虛擬機,dalvik、art和jvm都實現了這三個接口,可以無縫切換。有一個系統屬性persist.sys.dalvik.vm.lib表示當前系統使用哪個虛擬機;

3.Zygote進程在啓動過程中,獲取系統屬性persist.sys.dalvik.vm.lib的值來決定到底加載libdvm.so還是libart.so,然後創建不同的虛擬機。程序安裝時,如果系統屬性persist.sys.dalvik.vm.lib的值爲libdvm.so,就將dex指令碼優化後存在odex文件中,如果未libart.so,就將dex指令碼翻譯成本地機器碼,還是存放在odex文件中;

4.java創建的對象放在java堆中,手機廠商可以配置java堆區的大小,代碼中可以獲取java堆區的最大值;

5.低版本的dalvik虛擬機,bitmap堆是單獨的,但是會把這部分大小和java堆一起計算,不能超出配置的java堆區大小。高版本的虛擬機中,bitmap堆已經合入java堆了;

6.native代碼分配的內存放在native堆區,沒有文檔表明這部分大小如何限制,但是4.4的系統,2G內存,當一個應用分配到480M左右的時候就崩潰了;

7.Manifest中可以配置該進程爲largeHeap,在4.4的系統,2G內存機器中,分配到120M就崩潰了;

8.高版本的dalvik虛擬機,垃圾回收線程並行,一次只回收一部分,每次造成的中止時間小於5ms。每次回收都會打印日誌,回收了多少、當前已使用/總共、造成的中止時間;

9.我們調用的大部分java運行時庫,都是通過調用目標機器的操作系統(即linux)接口實現的,比如線程調度;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章