深入理解JVM
第一章 走近JAVA
第二章 Java內(nèi)存區(qū)域與內(nèi)存溢出異常
第三章 垃圾收集器與內(nèi)存分配策略
第六章 類文件結(jié)構(gòu)
第七章 虛擬機類加載機制
Matrix 相關(guān)
第二章 Java內(nèi)存區(qū)域與內(nèi)存溢出異常
一岸霹、運行時的數(shù)據(jù)區(qū)域
http://androidxref.com/9.0.0_r3/s?defs=BaseDexClassLoader&project=libcore
程序計數(shù)器
Java虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的坐儿,在任何一個具體確定的時刻匠题,一個處理器內(nèi)核只會執(zhí)行一條線程中的指令伸辟,因此,為了不同線程切換之后可以恢復(fù)原先的狀態(tài)励幼,每個線程都需要一個程序計數(shù)器归露,各個線程的程序計數(shù)器之間不會相互影響,獨立存儲掏湾,這類內(nèi)存是線程私有的內(nèi)存。
如果執(zhí)行的是一個Java方法肿嘲,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)指令的地址融击,如果是Native方法, 則值為空(undefined)雳窟,且程序計數(shù)器是唯一一個在java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域尊浪。
Java虛擬機棧
Java虛擬機棧所占有的內(nèi)存空間也就是我們平時所說的“棧內(nèi)存”匣屡,并且也是線程私有的,生命周期與線程相同拇涤。虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時捣作,都會創(chuàng)建一個棧幀,用于存儲局部變量表(基本數(shù)據(jù)類型鹅士,對象的引用和returnAddress類型)券躁、操作數(shù)棧、動態(tài)鏈接掉盅、方法出口等信息也拜。
局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進入一個方法時趾痘,這個方法需要在棧幀中分配多大的局部變量空間是完全確定的慢哈,在方法運行期間不會改變局部變量表的大小。每一個方法被調(diào)用直至執(zhí)行完成的過程永票,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程卵贱。對于Java虛擬機棧,有兩種異常情況:
1)如果線程請求的棧深度大于虛擬機所允許的深度侣集,將拋出StackOverflowError異常键俱;
2)如果虛擬機棧在動態(tài)擴展時,無法申請到足夠的內(nèi)存肚吏,就會拋出OutOfMemoryError方妖;(動態(tài)擴展的方法有Segmented stack雙向鏈表狭魂,可以簡單理解成一個雙向鏈表把多個棧連接起來罚攀,一開始只分配一個棧,這個棧的空間不夠時雌澄,就再分配一個斋泄,用鏈表一個一個連起來。Stack copying就是在棧不夠的時候镐牺,分配一個更大的棧炫掐,然后把原來的棧復(fù)制過去)
本地方法棧和Java堆
本地方法棧和虛擬機棧所發(fā)揮的作用非常相似,它們之間的區(qū)別主要是【虛擬機棧是為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù)的睬涧,而本地方法棧則為虛擬機使用到的Native方法服務(wù)】募胃。與虛擬機棧類似,本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常畦浓。
Java堆是Java虛擬機所管理的內(nèi)存中最大的一塊痹束。Java堆在主內(nèi)存中,是被所有線程共享的一塊內(nèi)存區(qū)域讶请,其隨著JVM的創(chuàng)建而創(chuàng)建祷嘶,【堆內(nèi)存的唯一目的是存放對象實例和數(shù)組。同時Java堆也是GC管理的主要區(qū)域】。
Java堆物理上不需要連續(xù)的內(nèi)存论巍,只要邏輯上連續(xù)即可烛谊。如果堆中沒有內(nèi)存完成實例分配,并且也無法再擴展時嘉汰,將會拋出OutOfMemoryError異常丹禀。《郑现?》
運行時常量池
【圖中沒有運行時常量池了】
還有一項信息是常量表湃崩,用于存放編譯期生成的各種字面常量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放(JDK1.7開始接箫,常量池已經(jīng)被移到了堆內(nèi)存中了)攒读。也就是說,這部分內(nèi)容辛友,在編譯時只是放入到了常量池信息中薄扁,到了加載時,才會放到運行時常量池中去废累。運行時常量池縣歸于Class文件常量池的另外一個重要特征是具備動態(tài)性邓梅,Java語言并不要求常量一定只有編譯期才能產(chǎn)生础钠,也就是并非預(yù)置入Class文件中常量池的內(nèi)容才能進入方法區(qū)的運行時常量池阿弃,運行期間也可能將新的常量放入池中匿值,這種特性被開發(fā)人員利用的比較多的是String類的intern()方法侯养。
方法區(qū)
也是線程公有的一個部分企孩,用來存儲已經(jīng)被加載過的類信息芹枷,常量疏旨,靜態(tài)變量
二上荡、新建對象
Java的語言層面上哎壳,創(chuàng)建對象(克隆毅待,反序列化)都是一個new關(guān)鍵字,在虛擬機中是怎樣工作的呢?
首先虛擬機在解析到new指令的時候归榕,會去檢查參數(shù)是否可以定位到一個類的符號引用尸红,并且檢查這個類是否已經(jīng)被加載、解析刹泄、初始化過外里,如果沒有的話會先進行類加載的操作。
在此之后會為新生的對象進行內(nèi)存的分配工作特石,分配的方式根據(jù)不同JVM的堆中內(nèi)存存儲方式不同也會不一樣(空閑列表/指針碰撞)盅蝗。
指針碰撞:假設(shè)Java堆中的內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存放在一邊县匠,沒有用過的內(nèi)存放在另一邊风科,中間放著一個指針作為分界點的指示器撒轮,那么分配內(nèi)存就是移動這個指針一個對象大小的距離(在給新建對象分配內(nèi)存的時候,會占用多少內(nèi)存是一個已知信息)
空閑列表:如果Java堆中的內(nèi)存是并不規(guī)整的贼穆,虛擬機就需要維護一個列表题山,哪些內(nèi)存塊是可用的,在分配的時候哦找到一塊足夠大的空間劃分給對象故痊,并更新列表上的記錄顶瞳。
JAVA堆是線程公有的,所以在多線程分配內(nèi)存時也會遇到并發(fā)的問題愕秫,那么這個問題的解決方案也有兩種慨菱,一種是CAS配上失敗重試,另一個就是TLAB戴甩,Java堆首先給每個線程分配一塊內(nèi)存符喝,之后每個線程會優(yōu)先在Tlab上進行對象的內(nèi)存分配。(-XX:+UseTLAB)
CAS: Compare and Swap, 比較并交換甜孤,整個Cucurenct包中协饲,CAS理論是它實現(xiàn)的基石。
CAS操作包括3個操作數(shù)------內(nèi)存位置(V)缴川,預(yù)期原值(A)茉稠, 新值(B),如果內(nèi)存位置的值與預(yù)期原值相匹配把夸,那么處理器會自動將該位置更換為新值(原子操作)而线,否則,處理器不做任何操作恋日,不論任何情況膀篮,它都會在CAS指令返回該內(nèi)存位置的值【底層使用總線鎖實現(xiàn),】(CAS只保證操作無錯誤谚鄙,不代表一定會成功各拷,比如ABA問題)
內(nèi)存分配完成后刁绒,虛擬機需要將分配到的內(nèi)存空間都初始化為零值(不包括對象頭)如果使用TLAB,這個操作可以提前到TLAB分配的時候闷营,再之后,對于虛擬機來說知市,一個新建對象已經(jīng)完成了傻盟,對于語言層面來說,class的<init>方法還沒有執(zhí)行到嫂丙,(在這里可以先暫時將<init> 理解為Java的構(gòu)造函數(shù))娘赴,在這之后一個可用的對象才真正的生成。
棧上分配和逃逸分析
上面所談到的內(nèi)存分配都只設(shè)計Java堆的內(nèi)存分配跟啤,書本也因為JDK版本原因沒有涉及到逃逸分析的相關(guān)內(nèi)容诽表,這里自己搜資料總結(jié)了一下唉锌。在JDK8之后的版本默認開啟逃逸分析(棧上分配的前提),8以下(-XX:+DoEscapeAnalysis)可以進行逃逸分析竿奏。
棧上分配:我們都知道Java中的對象都是在堆上分配的袄简,而垃圾回收機制會回收堆中不再使用的對象,但是篩選可回收對象泛啸,回收對象還有整理內(nèi)存都需要消耗時間绿语。如果能夠通過逃逸分析確定某些對象不會逃出方法之外,那就可以讓這個對象在棧上分配內(nèi)存候址,這樣該對象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀吕粹,就減輕了垃圾回收的壓力。
在一般應(yīng)用中岗仑,如果不會逃逸的局部對象所占的比例很大匹耕,如果能使用棧上分配,那大量的對象就會隨著方法的結(jié)束而自動銷毀了荠雕。
對象逃逸有以下幾種情況:
···
public class EscapeAnalysis {
public static Object object;
public void globalVariableEscape(){//全局變量賦值逃逸
object =new Object();
}
public Object methodEscape(){ //方法返回值逃逸
return new Object();
}
public void instancePassEscape(){ //實例引用發(fā)生逃逸
this.speak(this);
}
public void speak(EscapeAnalysis escapeAnalysis){
System.out.println("Escape Hello");
}
}
···
全局變量賦值逃逸
方法返回值逃逸
實例引用發(fā)生逃逸
線程逃逸:賦值給類變量或可以在其他線程中訪問的實例變量
JVM配置[關(guān)閉逃逸分析]:
-server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC
/**
* Created by huangwt on 2019/3/8.
*/
public class StackAlloc {
private static void alloc() {
byte[] tmpAlloc = new byte[20];
}
public static void main(String[] args) {
for (int i = 0; i < 1000000; i ++) {
alloc();
}
}
}
在上面這段Code中泌神,我們生成的長度為20的tmpAlloc數(shù)組并沒有被外界引用,但是查看GC 日志可以發(fā)現(xiàn)
【這里的GC均是Mijor GC】
[GC 2560K->568K(10240K), 0.0057678 secs]
[GC 3128K->576K(10240K), 0.0006989 secs]
[GC 3136K->584K(10240K), 0.0006238 secs]
[GC 3144K->584K(10240K), 0.0019912 secs]
[GC 3144K->584K(10240K), 0.0012139 secs]
[GC 3144K->584K(9216K), 0.0010486 secs]
[GC 2120K->656K(9728K), 0.0007815 secs]
[GC 2192K->648K(9728K), 0.0002554 secs]
[GC 2184K->696K(9728K), 0.0001980 secs]
...
發(fā)生了頻繁的GC,說明內(nèi)存分配在Java堆上而非棧上舞虱,這里的tmpAlloc生命周期應(yīng)該是跟隨alloc任務(wù)出棧而結(jié)束欢际,但是卻沒有結(jié)束,是因為我們關(guān)閉了逃逸分析矾兜,再次打開發(fā)現(xiàn)GC一次都沒有觸發(fā)损趋,說明內(nèi)存分配在棧上跟隨方法一同被出棧了。
這里舉的方法都是這里舉得例子是虛擬機棧的棧上分配椅寺,有關(guān)本地方法椈氩郏可以查看這個回答https://stackoverflow.com/questions/161053/which-is-faster-stack-allocation-or-heap-allocation
code optimization:雖然不清楚DVM/ART的工作機制,但是有關(guān)對象的生命周期我們一般希望在不影響頻繁GC的情況下越短越好返帕,我們的code有很多可以優(yōu)化為local variable桐玻,而不需要被整個對象所持有的對象,Lint可以檢查出來荆萤。
對象的內(nèi)存布局
在HotSpot虛擬機中镊靴,對象在內(nèi)存中存儲的布局分為3個區(qū)域:對象頭,實例數(shù)據(jù)和對齊填充链韭。
對象頭包括兩個部分的數(shù)據(jù)偏竟,第一部分是“mark word”包含 hashcode,GC分代年齡(之后會說到)敞峭,鎖狀態(tài)標志踊谋,線程持有的鎖(對象頭的鎖相關(guān)在書的第十三章會詳細講解,所以我沒有去查資料)旋讹,偏向線程ID殖蚕,偏向時間戳等等轿衔。
第二部分是類型指針,即指向?qū)ο笾赶蝾愒獢?shù)據(jù)的指針睦疫,虛擬機來確定這個對象是哪個類的實例呀枢。 另外,如果對象是一個Array類型笼痛,還需要一個int類型的長度來表示數(shù)組的長度
實例數(shù)據(jù)就是字面意思
對齊填充裙秋,不一定存在,例如HotSpot VM的內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍缨伊,有點類似Android的ZipAlign機制摘刑,總之作用就是用來方便尋址。
Integer = (8 + 4) + 4(int)
Long = (8 + 4) + 8(long) + 4(padding)
優(yōu)化空間:可以被替換為元數(shù)據(jù)類型的不要使用包裝類刻坊。
第三章 垃圾回收器和內(nèi)存分配策略
線程私有的內(nèi)存區(qū)域會隨著線程的生命周期自動的被回收枷恕。
線程公有的Java堆 & 方法區(qū),只有在運行時才知道需要具體創(chuàng)建多少個對象谭胚,這部分內(nèi)存的分配和回收都是動態(tài)的徐块,所以需要GC配合垃圾回收。本章分為以下幾點內(nèi)容分享
1灾而、如何判斷對象需要進行回收
2胡控、垃圾收集的算法
3、JVM的具體實現(xiàn)(HotSpot-JVM)
4旁趟、垃圾收集器
5昼激、內(nèi)存分配&回收策略
一、如何判斷對象是否死去
1锡搜、引用計數(shù)
在Fix內(nèi)存泄漏之前我對此并不了解橙困,現(xiàn)在大家應(yīng)該都知道在Android中是與可達性分析算法相關(guān)。
那么現(xiàn)有的常見的有引用計數(shù)算法耕餐,故名思議凡傅,對象中添加一個引用計數(shù)器,每當(dāng)有地方引用她時肠缔,計數(shù)器+1 夏跷,反之則-1。
Python使用的主流回收方式就是引用計數(shù)桩砰,和所有面向?qū)ο蟮恼Z言一樣拓春,Python的PyObject的結(jié)構(gòu)體如下所示释簿,這個ob_refcnt就是引用計數(shù)【這里選的是CPython】亚隅。
前文也介紹過JVM對象的組成結(jié)構(gòu)了,其中并沒有類似字段庶溶,所以Java采用的并非此算法煮纵,引用計數(shù)的優(yōu)點就是簡單懂鸵,缺點就是浪費內(nèi)存&不好解決循環(huán)引用的問題,前者倒無所謂行疏,要解決后者肯定需要另外的對象來維護這件事或者Object添加新的字段來維護匆光,這就得不償失了。
typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;
2酿联、可達性算法
另一個常見的就是可達性分析算法终息,將某些對象稱為GC-ROOT,從GC-ROOT走過的路徑即是引用鏈贞让,而GC不達的對象即為可回收的對象,像這種周崭,B,C即為可以回收的對象。
那么什么是GC ROOT呢喳张?
在Java中续镇,可以作為GC ROOT的有以下幾種:
虛擬機棧中引用的對象
方法區(qū)類靜態(tài)屬性引用的對象
方法區(qū)常量引用的對象
本地方法棧JNI引用的對象
java中的主流虛擬機HotSpot采用可達性分析算法來確定一個對象的狀態(tài),那么HotSpot在具體實現(xiàn)該算法時采用了哪些結(jié)構(gòu)销部?
使用OopMap記錄并枚舉根節(jié)點
HotSpot首先需要枚舉所有的GC Roots根節(jié)點摸航,虛擬機棧的空間不大,遍歷一次的時間或許可以接受舅桩,但是方法區(qū)的空間很可能就有數(shù)百兆酱虎,遍歷一次需要很久。更加關(guān)鍵的是擂涛,當(dāng)我們遍歷所有GC Roots根節(jié)點時逢净,我們需要暫停所有用戶線程,因為我們需要一個此時此刻的”虛擬機快照”歼指,如果我們不暫停用戶線程爹土,那么虛擬機仍處于運行狀態(tài),我們無法確保能夠正確遍歷所有的根節(jié)點踩身。所以此時的時間開銷過大更是我們不能接受的胀茵。
基于這種情況,HotSpot實現(xiàn)了一種叫做OopMap的數(shù)據(jù)結(jié)構(gòu)挟阻,這種數(shù)據(jù)結(jié)構(gòu)在類加載完成時把對象內(nèi)的偏移量是什么類型計算出琼娘,并且存放下位置,當(dāng)需要遍歷根結(jié)點時訪問所有OopMap即可附鸽。
用安全點Safepoint約束根節(jié)點
如果將每個符合GC Roots條件的對象都存放進入OopMap中脱拼,那么OopMap也會變得很大,而且其中很多對象很可能會發(fā)生一些變化坷备,這些變化使得維護這個映射表很困難熄浓。實際上,HotSpot并沒有為每一個對象都創(chuàng)建OopMap省撑,只在特定的位置上創(chuàng)建了這些信息赌蔑,這些位置稱為安全點(Safepoints)俯在。
從線程角度看,safepoint可以理解成是在代碼執(zhí)行過程中的一些特殊位置娃惯,當(dāng)線程執(zhí)行到這些位置的時候跷乐,說明虛擬機當(dāng)前的狀態(tài)是安全的,如果有需要趾浅,可以在這個位置暫停愕提,比如發(fā)生GC時,需要暫停暫停所以活動線程皿哨,但是線程在這個時刻揪荣,還沒有執(zhí)行到一個安全點,所以該線程應(yīng)該繼續(xù)執(zhí)行往史,到達下一個安全點的時候暫停仗颈,等待GC結(jié)束∽道【挨决?】
SafePoint.cpp 實現(xiàn) http://hg.openjdk.java.net/jdk7u/jdk7u/hotspot/file/e1b1da173e19/src/share/vm/runtime/safepoint.cpp
引用
Java中的引用目前有以下幾種
強引用(SoftReference)
強引用就是正常的使用方式,即便系統(tǒng)拋出OOM也不會對對象進行回收
軟引用
軟引用是指當(dāng)內(nèi)存不夠時才會對軟引用進行回收订歪,只要內(nèi)存還充足脖祈,就不會對其進行回收
弱引用
弱引用則是指不論內(nèi)存是否足夠,只要gc了就會回收
public Reference<Temp> soft;
public Reference<Temp> weak;
soft = new SoftReference<>(new Temp());
weak = new WeakReference<>(new Temp());
Observable.just(new Object())
.observeOn(Schedulers.io())
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object o) throws Exception {
Log.d("--------Object state", "soft" + (soft.get() != null ? "生存" : "毀滅"));
Log.d("--------Object state", "weak" + (weak.get() != null ? "生存" : "毀滅"));
System.gc();
Log.d("--------Object state", "soft" + (soft.get() != null ? "生存" : "毀滅"));
Log.d("--------Object state", "weak" + (weak.get() != null ? "生存" : "毀滅"));
}
});
2019-03-08 14:46:28.161 13532-13558/com.example.hwt.hotfixsample D/--------Object state: soft生存
2019-03-08 14:46:28.161 13532-13558/com.example.hwt.hotfixsample D/--------Object state: weak生存
2019-03-08 14:46:48.162 13532-13558/com.example.hwt.hotfixsample D/--------Object state: soft生存
2019-03-08 14:46:48.163 13532-13558/com.example.hwt.hotfixsample D/--------Object state: weak毀滅
可以看到刷晋,在進行GC之后盖高,軟引用的對象還生存著,而弱引用的對象已經(jīng)掛了
虛引用
虛引用(Phantom Reference):任何時候都可以被 GC 回收眼虱,當(dāng)垃圾回收器準備回收一個對象時喻奥,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前捏悬,把這個虛引用加入到與之關(guān)聯(lián)的引用隊列中撞蚕。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收过牙∩茫可以用來作為 GC 回收 Object 的標志。
public class PhantomReference<T> extends Reference<T> {
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get() {
return null;
}
...
}
Code optimization
有關(guān)軟引用寇钉,由于根據(jù)內(nèi)存動態(tài)回收的特性很多人會使用來作為Cache刀疙,但是Android并不推薦這么做,在SoftReference的備注中我們可以看到這么一段話,推薦使用LruCache來代替SoftReference作為Cache的用法
* <p>Most applications should use an {@code android.util.LruCache} instead of
* soft references. LruCache has an effective eviction policy and lets the user
* tune how much memory is allotted.
被回收了嗎扫倡?
C 有析構(gòu)函數(shù)在被回收時調(diào)用谦秧,那么Java被回收時會有通知嗎?有什么操作可以避免對象被GC回收嗎?
答案是有的
public Object {
protected void finalize() throws Throwable { }
}
Object對象有一個finalize的方法油够,在重寫了finalize的情況下會首先調(diào)用finalize方法并將對象加入一個名為F-QUEUE的隊列中蚁袭, 如果在finalize方法中重新建立鏈接征懈,對象將被拯救石咬,并在下一次GC時移除“即將回收”的集合。
換句話說卖哎,如果對象在回收時又報上了大腿鬼悠,比如將自己賦值給某個static引用,則這個對象會被避免這次GC亏娜。
回收方法區(qū)
前文中有提到過方法區(qū)主要存儲了類信息焕窝,靜態(tài)信息之類的內(nèi)容,這一部分的內(nèi)存在JVM手冊中是允許不做GC處理的维贺,但是不代表這塊區(qū)域不會進行回收它掂,通常在頻繁使用自定義ClassLoader時才會導(dǎo)致有廢棄的類與類信息,如果判斷為不需要的類信息之后可以對類進行卸載溯泣,但是Android中不論DVM虛擬機還是ART架構(gòu)貌似都不支持類的卸載虐秋。
垃圾回收算法
各個平臺虛擬機實現(xiàn)的方式各不相同,這里只介紹一些常見的思想和算法(后面所舉的例子都是基于JDK1.7,每個版本的都不太一樣)
標記-清除算法
標記過程之前講述對象標記判定時也提到過垃沦,首先先標記出需要被回收的對象客给,再統(tǒng)一進行回收,這樣做的優(yōu)點是操作簡單肢簿,但是缺點也非常明顯靶剑。
一是標記和清除過程效率并不高,會對整個java堆的對象進行遍歷池充。
二是標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片桩引。
復(fù)制算法
復(fù)制算法是先將可用內(nèi)存區(qū)一分為二,每次GC的時候?qū)片區(qū)的存活對象依次遷移到B片區(qū)收夸,并一次性將A片區(qū)清空阐污,內(nèi)存的分配是B片區(qū)自然是連續(xù)的,實現(xiàn)簡單咱圆,運行高校笛辟,但是天生就少了一半內(nèi)存狠明顯不太合理,不過算法是可以進行優(yōu)化的序苏,現(xiàn)代的商業(yè)虛擬機都采用復(fù)制算法來回收新生代手幢,因為絕大多數(shù)的對象是朝生夕死的,也就是說忱详,每次Gc之后還能存活的對象實際上很少围来,所以并不需要一半一半的來分區(qū),在JDK1.7版本中,是分為一塊較大的Eden空間和兩塊較小的Survivor空間监透,比例默認是8 1 1桶错,這個可以在VM OPTIONS里自己設(shè)置。
那么如果存活對象所占空間> 10%,B片Survivor空間放不下怎么辦胀蛮?這個時候會依賴老年代進行分配擔(dān)保院刁。
標記-整理算法
復(fù)制算法對空間的較低利用率很明顯不適合 相對穩(wěn)定,需要較少Gc的老年代粪狼,所以老年代一般不會選擇復(fù)制算法退腥,標記整理算法先對于1 標記 清除有什么不一樣呢?他在標記之后會先對所有依然存活的對象往一個地方移動再榄,然后清理掉邊界以外的內(nèi)存狡刘。
我們可以發(fā)現(xiàn),沒有一種完美的算法困鸥,每個算法都有他的缺點和優(yōu)點嗅蔬,所以現(xiàn)行的虛擬機都會進行分代收集,即老年代和新生代疾就,部分對方法區(qū)進行回收的也許還有個持久代澜术,這里不討論,只討論Java堆的垃圾回收虐译。
并行是指多個GC線程同時回收但是此時用戶線程還是處在暫停狀態(tài)
并發(fā)是指GC線程與用戶線程并行瘪板,即在進行垃圾回收的同時用戶可以操作
CMS則是一個追求響應(yīng)時間放棄部分系統(tǒng)吞吐量的垃圾回收算法,具體幾種垃圾收集器的工作流程可以看書或者查一下漆诽。
GC日志的查看
jvm參數(shù)配置
-XX:+PrintGCDetails
/**
* Created by huangwt on 2019/3/8.
*/
public class StackAlloc {
private static int MB_1 = 1024*1024;
public static void main(String[] args) {
byte[] a = new byte[MB_1];
byte[] b = new byte[MB_1];
byte[] c = new byte[MB_1 * 4];
}
}
[GC [PSYoungGen: 2531K->504K(3072K)] 2531K->1600K(10240K), 0.0018824 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 3072K, used 1607K [0x00000000ffc80000, 0x0000000100000000, 0x0000000100000000)
eden space 2560K, 43% used [0x00000000ffc80000,0x00000000ffd93e70,0x00000000fff00000)
from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e010,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 5192K [0x00000000ff580000, 0x00000000ffc80000, 0x00000000ffc80000)
object space 7168K, 72% used [0x00000000ff580000,0x00000000ffa92020,0x00000000ffc80000)
PSPermGen total 21504K, used 3223K [0x00000000fa380000, 0x00000000fb880000, 0x00000000ff580000)
object space 21504K, 14% used [0x00000000fa380000,0x00000000fa6a5c80,0x00000000fb880000)
最終打印的GC日志字段含義是什么呢侮攀? 首先可以看到JAVA堆被分成這幾個區(qū)域,新生代(PSYoungGen)
老年代(OldGen),和永久代(JDK8已經(jīng)廢棄被元空間替代厢拭,可以自行了解)兰英。
首先看新生代,被劃分為Eden, From, To 三個內(nèi)存空間供鸠,2560KB代表了Eden區(qū)的大小畦贸,from to兩塊survivor空間大小一致均為512K ,Eden和Survivor區(qū)的比例可以用參數(shù)配置修改楞捂,同樣新生代與老年代的比例也可以設(shè)置薄坏。所以新生代的可用區(qū)域為eden區(qū) + 一塊Survivor區(qū)的大小
老年代則是一塊7168KB的內(nèi)存區(qū)域。
code首先新建了2個1MB大小的對象寨闹,創(chuàng)建對象會優(yōu)先在新生代創(chuàng)建胶坠,所以在第一次GC前會優(yōu)先在Eden區(qū)和一塊Survivor區(qū)域分配空間》北ぃ【不分配任何對象時Eden區(qū)也會被占用一部分空間沈善,這邊總內(nèi)存為10M時占了1400KB】乡数, 所以這邊生成a對象時會直接在新生代分配,隨后分配b對象時闻牡,新生代空間不足會發(fā)生一次minorGC【不帶FULL GC字樣的即為只發(fā)生在新生代】净赴,
PSYoungGen: 2531K->504K(3072K) 這個代表了新生代從占用2531KB變?yōu)榱?04KB(放在了From區(qū)域),2531K->1600K(10240K) 是指整個新生代加老年代占用內(nèi)存從2531KB變?yōu)榱?600KB罩润。后面的時間是指GC消耗的時間玖翅。這次GC之后對象a會由分配擔(dān)保機制被送入了老年代,而b被分配在了新生代哨啃。
在此之后的C對象烧栋,大對象則會直接進入老年代写妥,虛擬機提供了參數(shù)
-xx:PretenureSizeThreshold = 3145728
來配置大于多大的對象會直接分配到老年代拳球。
Code optimization:千萬要避免 創(chuàng)建朝生夕死的大對象
第四章 & 第五章
講了JAVA工具的使用,還有一些具體的調(diào)優(yōu)案例珍特,可以看書祝峻。
第七章 虛擬機的類加載機制
虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗扎筒、轉(zhuǎn)換解析和初始化莱找,最后形成可以被虛擬機直接使用的Java類型,著就是類加載機制嗜桌。
在Java中奥溺,類型的加載、連接和初始化都是在程序的運行期間完成的骨宠,雖然會讓類加載增加了一些性能開銷浮定,但是靈活性會很高。例如 編寫一個面向接口的代碼层亿,到運行期再指定其具體的實現(xiàn)類桦卒,另外用戶可以通過Java預(yù)定義和自定義類加載器,讓本地程序可以在運行時從網(wǎng)路或者其他地方加載一個二進制流作為程序代碼的一部分動態(tài)組裝匿又。
類加載的時機
類從被加載開始方灾,到卸載出內(nèi)存為止,他的生命周期包括:
加載 驗證 準備 解析 初始化 使用 卸載
【連接 包括 驗證 準備 解析3個步驟】
虛擬機并沒有規(guī)范什么時候一定要執(zhí)行類的加載操作碌更,但是嚴格規(guī)范了什么時候必須立即對類進行初始化操作裕偿,而之前的步驟一定會走在類初始化之前
- 遇到new /getstatic/setstatic/invokestatic 這4條字節(jié)碼指令時,如果類沒有進行過初始化痛单,則需要先觸發(fā)其初始化工作泉蝌。
- 使用java.lang.reflect包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化鳍徽,則需要先觸發(fā)其初始化工作霹期。
- 當(dāng)初始化一個類的時候谆棱,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則先需要對父類進行初始化圆仔。
- 當(dāng)虛擬機啟動時垃瞧,用戶需要指定一個啟動主類(main方法),這個類會被初始化
- invoke的靜態(tài)方法屬于的類如果沒有進行過初始化坪郭,則需要先觸發(fā)其初始化工作个从。
/**
* Created by huangwt on 2019/3/11.
*/
class ClassInitTest {
static {
System.out.println("ClassInitTest initialized!");
}
static final Integer ORZ_VALUE = 1;
static class ClassInitSubTest extends ClassInitTest {
static {
System.out.println("ClassInitTestSub initialized!");
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassInitTest.ClassInitSubTest.ORZ_VALUE);
}
}
這段代碼的輸出會是
可以發(fā)現(xiàn)通過子類去調(diào)用父類的靜態(tài)字段并不會導(dǎo)致子類的初始化,只會帶來父類的初始化
歪沃,當(dāng)然嗦锐,至于是否初始化子類虛擬機的規(guī)范中沒有明確的規(guī)定,所以取決于各個虛擬機制定的規(guī)則有所不同沪曙。
第二段代碼
public class Main {
public static void main(String[] args) throws InterruptedException {
ClassInitTest.ClassInitSubTest[] tmp = new ClassInitTest.ClassInitSubTest[10];
}
}
這段代碼最后的結(jié)果將是沒有一個類被初始化了奕污,為什么會這樣呢,看一下類的反編譯
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
Code:
0: bipush 10
2: anewarray #2 // class ClassInitTest$ClassInitSubTest
5: astore_1
6: return
}
在新建的時候調(diào)用了anewarray字節(jié)碼指令而非規(guī)范中所提及的幾種主動引用液走,所以沒有發(fā)生具體類的初始化碳默。【Q:TraceClassload的時候會發(fā)現(xiàn)loaded缘眶,與書中不太一致】
第三段Code
class ClassInitTest {
public static final String s = "test";
}
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassInitTest.s);
}
}
之外再將原先的ORZ_VALUE的類型從包裝類Integer換成int嘱根,可以發(fā)現(xiàn)并不會調(diào)用到類的加載,這是因為在編譯器虛擬機已經(jīng)將"test"字符串存到了常量池中巷懈,Main.class直接持有常量池的引用该抒,
public class Main {
public Main() {
}
public static void main(String[] args) throws InterruptedException {
System.out.println("test");
}
}
類加載的過程
加載
加載是類加載過程的一個階段,在這個階段顶燕,虛擬機需要完成以下三件事情:
1)通過一個類的全限定名來獲取定義此類的二進制流
2)將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)運行時結(jié)構(gòu)數(shù)據(jù)
3)在內(nèi)存中生成一個代表這個類凑保,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-5.html#jvms-5.3.3
【對于數(shù)組類而言,If C is not an array class, it is created by loading a binary representation of C (§4 (The class
File Format)) using a class loader. Array classes do not have an external binary representation; they are created by the Java Virtual Machine rather than by a class loader.】
虛擬機定義的三件事情并不算具體割岛,所以靈活性極高愉适,在歷程中出現(xiàn)了許多有關(guān)類加載的操作:
1、從ZIP包讀取癣漆,日后成為JAR维咸,AAR之類格式的基礎(chǔ)
2、從網(wǎng)絡(luò)中獲取 (熱修復(fù))
3惠爽、運行時計算生成癌蓖,比如動態(tài)代理
等等
加載階段完成后,虛擬機外部的二進制字節(jié)流就按照虛擬機所需要的格式存儲在方法區(qū)中婚肆,方法區(qū)中的數(shù)據(jù)存儲格式由虛擬機實現(xiàn)自行定義租副,然后在內(nèi)存中實例化一個Class類的對象【沒有明確規(guī)定存儲在Java堆中】。
驗證
驗證是連接階段的第一步较性,主要是為了確保Class文件的字節(jié)流包含的信息是否符合虛擬機要求用僧,丙炔不會危害自身的安全结胀。
驗證階段分為以下四個步驟:
1、文件格式驗證
// first verification pass - validate cross references and fixup class and string constants
for (index = 1; index < length; index++) { // Index 0 is unused
jbyte tag = cp->tag_at(index).value();
switch (tag) {
case JVM_CONSTANT_Class :
ShouldNotReachHere(); // Only JVM_CONSTANT_ClassIndex should be present
break;
case JVM_CONSTANT_Fieldref :
// fall through
case JVM_CONSTANT_Methodref :
// fall through
case JVM_CONSTANT_InterfaceMethodref : {
if (!_need_verify) break;
int klass_ref_index = cp->klass_ref_index_at(index);
int name_and_type_ref_index = cp->name_and_type_ref_index_at(index);
check_property(valid_cp_range(klass_ref_index, length) &&
is_klass_reference(cp, klass_ref_index),
"Invalid constant pool index %u in class file %s",
klass_ref_index,
CHECK_(nullHandle));
check_property(valid_cp_range(name_and_type_ref_index, length) &&
cp->tag_at(name_and_type_ref_index).is_name_and_type(),
"Invalid constant pool index %u in class file %s",
name_and_type_ref_index,
CHECK_(nullHandle));
break;
}
case JVM_CONSTANT_String :
ShouldNotReachHere(); // Only JVM_CONSTANT_StringIndex should be present
break;
case JVM_CONSTANT_Integer :
break;
case JVM_CONSTANT_Float :
......
這邊貼了HotSpot虛擬機的文件檢查的一小部分的代碼责循,可以看到是對class文件的常量池進行檢查糟港,比如是否以CafeBaby作為文件開頭,版本號是否在當(dāng)前虛擬機的處理范圍之內(nèi)院仿,常量池中是否由不被支持的常量類型秸抚,等等等等。
2歹垫、元數(shù)據(jù)驗證
對類的元數(shù)據(jù)信息進行語義校驗剥汤,比如:
這個類是否有父類,父類是否繼承了final class排惨, 類是否實現(xiàn)了父類抽象方法和接口方法吭敢,類中字段和父類是否產(chǎn)生矛盾。
3若贮、字節(jié)碼驗證
主要目的是通過數(shù)據(jù)流和控制流分析省有,確定程序語義是合法的痒留、符合邏輯的谴麦。
例如:
保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,不會出現(xiàn)上一步往棧中壓了一個int類的數(shù)字伸头,而后pop時候當(dāng)作long類型使用匾效。
保證跳轉(zhuǎn)指令不會跳到方法體以外的字節(jié)碼指令上
保證類型轉(zhuǎn)換是安全有效的。
如果一個類方法體的字節(jié)碼通過了字節(jié)碼的驗證恤磷,也不能代表他是安全的面哼。
【停機問題】,沒有一段程序可以判斷程序是否可以在有限時間之內(nèi)結(jié)束運行扫步。
4魔策、符號引用驗證
符號引用驗證發(fā)生在解析階段的最后一個部分,通常檢查以下幾個部分
通過字符串的全限定名能否找到類
符號引用中的類河胎、字段闯袒、方法的訪問性能否被當(dāng)前類直接訪問
準備
準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中分配游岳。例如
public static int value = 123;
在當(dāng)前類被初始化的時候政敢,value的值會先被賦0值,然后將putstatic放在類的static方法中胚迫,也就是類的Cinit方法中喷户,對value重新賦值為123;
public class Main {
public static int a;
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
Code:
0: return
static {};
Code:
0: bipush 123
2: putstatic #2 // Field a:I
5: return
}
不過如果給a設(shè)定了final修飾符访锻,則會在初始化的時候直接賦予123值褪尝。
解析
解析是虛擬機將符號引用替換為直接引用的過程闹获。
/**
* Created by huangwt on 2019/3/11.
*/
public class X {
public void foo() {
bar();
}
public void bar() { }
}
現(xiàn)在有這么一個類X。他編譯出來的Class文件基本如下
Classfile /E:/untitled1/JVMRead/out/production/JVMRead/X.class
Last modified 2019-3-11; size 372 bytes
MD5 checksum 63e83d9ddf2d3c4081f4a742f0e5c2ab
Compiled from "X.java"
public class X
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#16 // java/lang/Object."<init>":()V
#2 = Methodref #3.#17 // X.bar:()V
#3 = Class #18 // X
#4 = Class #19 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 LX;
#12 = Utf8 foo
#13 = Utf8 bar
#14 = Utf8 SourceFile
#15 = Utf8 X.java
#16 = NameAndType #5:#6 // "<init>":()V
#17 = NameAndType #13:#6 // bar:()V
#18 = Utf8 X
#19 = Utf8 java/lang/Object
{
public X();
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 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LX;
public void foo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #2 // Method bar:()V
4: return
LineNumberTable:
line 6: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LX;
public void bar();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this LX;
}
SourceFile: "X.java"
緊接著河哑,我們查看foo()方法
它的第一條字節(jié)碼指令為:
1: invokevirtual #2 // Method bar:()V
這在Class文件中的實際編碼為
[B6] [00 02]
0xB6是invokeVirtual指令的操作嗎昌罩,后面的00 02則是操作數(shù),用于指定要調(diào)用的目標方法灾馒, 也就是常量池中編號為#2的內(nèi)容茎用,我們再找到#2的內(nèi)容
2 = Methodref #3.#17 // X.bar:()V
是一個MethodRef,它在class文件中的實際編碼為
[0A] [00 03] [00 11]
和上面相同睬罗,0A代表了CONSTANT_MethodRef_Info的tag轨功,后面的兩個內(nèi)容則是class_index和name_and_type_index,找一下對應(yīng)的#3 和#17號常量池內(nèi)容
3 = Class #18 // X
17 = NameAndType #13:#6 // bar:()V
和上面一樣,3指向了 #18容达,而#18常量是類X
17 指向了 #13 和 #6,分別是
13 = Utf8 bar
6 = Utf8 ()V
那么我們把剛剛的一系列操作拼湊起來看看結(jié)果
#2 Methodref X.bar:()V
/ \
#3 Class X #17 NameAndType bar:()V
| / \
#18 Utf8 X #13 Utf8 bar #6 Utf8 ()V
由此可以看出古涧,Class文件中的invokevirtual指令的操作數(shù)經(jīng)過幾層間接之后,最后都是由字符串來表示的花盐。這就是Class文件里的“符號引用”的實態(tài):帶有類型(tag) / 結(jié)構(gòu)(符號間引用層次)的字符串羡滑。
然后再看JVM里的“直接引用”的樣子。由于書里面 有關(guān)引用講的都一帶而過算芯,這里舉一個JVM遠古版本的實現(xiàn)
HObject ClassObject
-4 [ hdr ]
--> +0 [ obj ] --> +0 [ ... fields ... ]
+4 [ methods ] \
\ methodtable ClassClass
> +0 [ classdescriptor ] --> +0 [ ... ]
+4 [ vtable[0] ] methodblock
+8 [ vtable[1] ] --> +0 [ ... ]
... [ vtable... ]
在剛加載好一個類的時候柒昏,Class文件里的常量池和每個方法的字節(jié)碼(Code屬性)會被基本原樣的拷貝到內(nèi)存里先放著,也就是說仍然處于使用“符號引用”的狀態(tài)熙揍;直到真的要被使用到的時候才會被解析(resolve)為直接引用职祷。假定我們要第一次執(zhí)行到foo()方法里調(diào)用bar()方法的那條invokevirtual指令了。此時JVM會發(fā)現(xiàn)該指令尚未被解析(resolve)届囚,所以會先去解析一下有梆。通過其操作數(shù)所記錄的常量池下標0x0002,找到常量池項#2意系,發(fā)現(xiàn)該常量池項也尚未被解析(resolve)泥耀,于是進一步去解析一下。通過Methodref所記錄的class_index找到類名蛔添,進一步找到被調(diào)用方法的類的ClassClass結(jié)構(gòu)體痰催;然后通過name_and_type_index找到方法名和方法描述符,到ClassClass結(jié)構(gòu)體上記錄的方法列表里找到匹配的那個methodblock作郭;最終把找到的methodblock的指針寫回到常量池項#2里陨囊。
也就是說,原本常量池項#2在類加載后的運行時常量池里的內(nèi)容跟Class文件里的一致夹攒,是:
[00 03] [00 11]
【剛加載進來的時候數(shù)據(jù)仍然是按高位在前字節(jié)序存儲的 】
而在解析后蜘醋,假設(shè)找到的methodblock*是0x45762300,那么常量池項#2的內(nèi)容會變?yōu)椋?/p>
[00 23 76 45]
(解析后字節(jié)序使用x86原生使用的低位在前字節(jié)序(little-endian)咏尝,為了后續(xù)使用方便)這樣压语,以后再查詢到常量池項#2時啸罢,里面就不再是一個符號引用,而是一個能直接找到Java方法元數(shù)據(jù)的methodblock了胎食。這里的methodblock就是一個“直接引用”扰才。
回顧一下,在解析前那條指令的內(nèi)容是:
[B6] [00 02]
而在解析后厕怜,這塊代碼被改寫為:
[D6] [06] [01]
B6:invokevirtual
D6:invokevirtual_quick,原本存儲操作數(shù)的2字節(jié)空間現(xiàn)在分別存了2個1字節(jié)信息衩匣,第一個是虛方法表的下標(vtable index),第二個是方法的參數(shù)個數(shù)粥航。這兩項信息都由前面解析常量池項#2得到的methodblock*讀取而來琅捏。
也就是:
invokevirtual_quick vtable_index=6, args_size=1
這里例子里,類X對應(yīng)在JVM里的虛方法表會是這個樣子的:
[0]: java.lang.Object.hashCode:()I
[1]: java.lang.Object.equals:(Ljava/lang/Object;)Z
[2]: java.lang.Object.clone:()Ljava/lang/Object;
[3]: java.lang.Object.toString:()Ljava/lang/String;
[4]: java.lang.Object.finalize:()V
[5]: X.foo:()V
[6]: X.bar:()V
所以會直接調(diào)用虛方法表里面的方法所在的內(nèi)存地址
初始化
類初始化是類加載的最后一步递雀,前面類加載的過程中柄延,除了加載階段用戶可以通過自定義ClassLoader參與,其他的都不可以參與缀程,到了初始化階段搜吧,才開始真正的執(zhí)行Java代碼。
類構(gòu)造器代碼是哪一塊杨凑?我們最熟悉的應(yīng)該就是Java中的static代碼塊
/**
* Created by huangwt on 2019/3/11.
*/
public class X {
static {
a = 3;
}
static int a = 1;
}
但是對于虛擬機來說滤奈,類的構(gòu)造方法是<clinit>,這個函數(shù)是由虛擬機將Java文件中所有靜類變量的復(fù)制和static語句合并產(chǎn)生的蠢甲,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量僵刮,定義在它之后的變量
/**
* Created by huangwt on 2019/3/11.
*/
public class X {
static int a = 1;
static {
System.out.println(a);
//合法,但是放到定義之前就不合法
}
}
Clinit和init不一樣鹦牛,他不會顯示的調(diào)用父類的構(gòu)造函數(shù),虛擬機會保證子類初始化之前勇吊,父類都初始化完成曼追,所以第一個被執(zhí)行的就是Object的Clinit方法。
Clinit方法對于類或者接口來說不是必須的汉规,如果沒有靜態(tài)語句塊或者沒有對變量的賦值操作礼殊,就不會生成clinit代碼塊。
虛擬機會保證一個類的init方法在多線程下被同步针史,如果多個線程初始化同一個類晶伦,則會發(fā)生阻塞,所以要避免在clinit代碼塊中執(zhí)行耗時的操作
類加載器
篇幅的問題這里我只拿Android的類加載器來舉例和說明啄枕。
BootClassLoader
和常規(guī)JVM一樣婚陪,加載系統(tǒng)類用的還是BootClassLoader,BootClassLoader實例在Android系統(tǒng)啟動的時候被創(chuàng)建频祝,用于加載一些Android系統(tǒng)框架的類泌参,其中就包括APP用到的一些系統(tǒng)類脆淹。
PathClassLoader
在應(yīng)用啟動的時候創(chuàng)建PathClassLoader實例,只能加載系統(tǒng)中已經(jīng)安裝過的apk沽一;
Provides a simple ClassLoader
implementation that operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and for its application class loader(s).
DexClassLoader
可以加載jar/apk/dex盖溺,可以從SD卡中加載未安裝的apk;
A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.
This class loader requires an application-private, writable directory to cache optimized classes. Use Context.getDir(String, int) to create such a directory:
pathClassLoader和DexClassLoader的父類都是BaseDexClassLoader铣缠,在BaseClassLoader類中有一個很重要的屬性dexpathlist烘嘱,和類的加載關(guān)聯(lián)很大,下面看一下類的具體加載邏輯:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
加載過程:
1)會先查找當(dāng)前ClassLoader是否加載過此類蝗蛙,有就返回拙友;
2)如果沒有,查詢父ClassLoader是否已經(jīng)加載過此類歼郭,如果已經(jīng)加載過,就直接返回Parent加載的類遗契;
3)如果整個類加載器體系上的ClassLoader都沒有加載過,才由當(dāng)前ClassLoader加載病曾,整個過程類似循環(huán)鏈表一樣牍蜂。
klassOop SystemDictionary::find_class(int index, unsigned int hash,
Symbol* class_name,
Handle class_loader) {
assert_locked_or_safepoint(SystemDictionary_lock);
assert (index == dictionary()->index_for(class_name, class_loader),
"incorrect index?");
klassOop k = dictionary()->find_class(index, hash, class_name, class_loader);
return k;
}
Java的類加載機制和此基本類似,都是責(zé)任鏈設(shè)計模式的雙親委派機制泰涂,那么雙親委派的作用有哪些呢鲫竞?
1)共享功能,一些Framework層級的類一旦被頂層的ClassLoader加載過就緩存在內(nèi)存里面逼蒙,以后任何地方用到都不需要重新加載从绘。
2)隔離功能,保證java/Android核心類庫的純凈和安全是牢,防止惡意加載僵井。
簡單的熱修復(fù)
這里提的熱修復(fù)是基于ClassLoader實現(xiàn)的基礎(chǔ)demo,在不進行干涉的情況下驳棱, 上下文所使用的類加載器都是同一個批什,也就是PathClassLoader,那么想要基于類加載器實現(xiàn)簡單的熱修復(fù)社搅,也就是替換類驻债,有兩種方案。
1)破壞雙親委任機制
我們加載類A的順序是先由parent類加載器進行加載形葬,加載失敗才會由子加載器加載合呐,可以選擇自定義類加載器替換上下文加載器重寫加載類的方法和順序
2)加載補丁包插入系統(tǒng)默認的ClassLoader
private final DexPathList pathList;
#Note that all the *.jar and *.apk files from {@code dexPath} might be
#first extracted in-memory before the code is loaded. This can be avoided
#by passing raw dex files (*.dex) in the {@code dexPath}.
這個DexPathList可以理解為一個數(shù)組對象,持有了一個比較核心的動態(tài)加載dex的方法
private static void loadDex(Context context) {
File fileDir = context.getDir(DEX_DIR, Context.MODE_PRIVATE);
pathClassLoader = context.getClassLoader();
String patchDir = fileDir.getAbsolutePath() + File.separator + "qsxtpatch_dex";
File patchFile = new File(patchDir);
if (!patchFile.exists()) {
patchFile.mkdir();
}
List<File> patchFiles = loadPatchFile(context);
for (File dex : patchFiles) {
DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), patchFile.getAbsolutePath(), null, pathClassLoader);
try {
Object dexPathList = getDexPathList(pathClassLoader);
//獲取到classLoader的dexpathlist
Object dexElements = getDexElements(dexPathList);
//獲取刀dexpathlist的elements[]數(shù)組笙以,代表著Dex文件的elements
Object dexPathListDexFile = getDexPathList(dexClassLoader);
//獲取新建DexClassLoader的DexpathList字段
Object dexElementsDexFile = getDexElements(dexPathListDexFile);
//獲取新建DexClassLoader的elements數(shù)組
Object newDexFile = mergeArray(dexElementsDexFile, dexElements);
//將新舊的合并淌实,并且以補丁包的dex文件放置于數(shù)組的前列
putFieldObj("dexElements", newDexFile, dexPathList);
//將pathClassloader的dexElements替換為修復(fù)好的Dex集合
logFix(dex.toString() + "Fix Success");
} catch (Exception e) {
e.printStackTrace();
}
}
}