大家好,我是老三,“面渣逆襲“系列繼續,這節我們來搞定JVM。說真的,JVM調優什麼的一個程式設計師可能整個職業生涯都碰不到兩次,但是,一旦用到的時候,那就是救命了,而且最重要的是——面試必問,所以,還能怎麼辦?整!
引言
1.什麼是JVM?
JVM——Java虛擬機器,它是Java實現平臺無關性的基石。
Java程式執行的時候,編譯器將Java檔案編譯成平臺無關的Java位元組碼檔案(.class),接下來對應平臺JVM對位元組碼檔案進行解釋,翻譯成對應平臺匹配的機器指令並執行。
同時JVM也是一個跨語言的平臺,和語言無關,只和class的檔案格式關聯,任何語言,只要能翻譯成符合規範的位元組碼檔案,都能被JVM執行。
記憶體管理
2.能說一下JVM的記憶體區域嗎?
JVM記憶體區域最粗略的劃分可以分為堆
和棧
,當然,按照虛擬機器規範,可以劃分為以下幾個區域:
JVM記憶體分為執行緒私有區和執行緒共享區,其中方法區
和堆
是執行緒共享區,虛擬機器棧
、本地方法棧
和程式計數器
是執行緒隔離的資料區。
1、程式計數器
程式計數器(Program Counter Register)也被稱為PC暫存器,是一塊較小的記憶體空間。
它可以看作是當前執行緒所執行的位元組碼的行號指示器。
2、Java虛擬機器棧
Java虛擬機器棧(Java Virtual Machine Stack)也是執行緒私有的,它的生命週期與執行緒相同。
Java虛擬機器棧描述的是Java方法執行的執行緒記憶體模型:方法執行時,JVM會同步建立一個棧幀,用來儲存區域性變數表、運算元棧、動態連線等。
3、本地方法棧
本地方法棧(Native Method Stacks)與虛擬機器棧所發揮的作用是非常相似的,其區別只是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的本地(Native)方法服務。
Java 虛擬機器規範允許本地方法棧被實現成固定大小的或者是根據計算動態擴充套件和收縮的。
4、Java堆
對於Java應用程式來說,Java堆(Java Heap)是虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,Java裡“幾乎”所有的物件例項都在這裡分配記憶體。
Java堆是垃圾收集器管理的記憶體區域,因此一些資料中它也被稱作“GC堆”(Garbage Collected Heap,)。從回收記憶體的角度看,由於現代垃圾收集器大部分都是基於分代收集理論設計的,所以Java堆中經常會出現新生代
、老年代
、Eden空間
、From Survivor空間
、To Survivor空間
等名詞,需要注意的是這種劃分只是根據垃圾回收機制來進行的劃分,不是Java虛擬機器規範本身制定的。
5.方法區
方法區是比較特別的一塊區域,和堆類似,它也是各個執行緒共享的記憶體區域,用於儲存已被虛擬機器載入的型別資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取等資料。
它特別在Java虛擬機器規範對它的約束非常寬鬆,所以方法區的具體實現歷經了許多變遷,例如jdk1.7之前使用永久代作為方法區的實現。
3.說一下JDK1.6、1.7、1.8記憶體區域的變化?
JDK1.6、1.7/1.8記憶體區域發生了變化,主要體現在方法區的實現:
- JDK1.6使用永久代實現方法區:
- JDK1.7時發生了一些變化,將字串常量池、靜態變數,存放在堆上
-
在JDK1.8時徹底幹掉了永久代,而在直接記憶體中劃出一塊區域作為元空間,執行時常量池、類常量池都移動到元空間。
4.為什麼使用元空間替代永久代作為方法區的實現?
Java虛擬機器規範規定的方法區只是換種方式實現。有客觀和主觀兩個原因。
-
客觀上使用永久代來實現方法區的決定的設計導致了Java應用更容易遇到記憶體溢位的問題(永久代有-XX:MaxPermSize的上限,即使不設定也有預設大小,而J9和JRockit只要沒有觸碰到程式可用記憶體的上限,例如32位系統中的4GB限制,就不會出問題),而且有極少數方法 (例如String::intern())會因永久代的原因而導致不同虛擬機器下有不同的表現。
-
主觀上當Oracle收購BEA獲得了JRockit的所有權後,準備把JRockit中的優秀功能,譬如Java Mission Control管理工具,移植到HotSpot 虛擬機器時,但因為兩者對方法區實現的差異而面臨諸多困難。考慮到HotSpot未來的發展,在JDK 6的 時候HotSpot開發團隊就有放棄永久代,逐步改為採用本地記憶體(Native Memory)來實現方法區的計劃了,到了JDK 7的HotSpot,已經把原本放在永久代的字串常量池、靜態變數等移出,而到了 JDK 8,終於完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地記憶體中實現的元空間(Meta-space)來代替,把JDK 7中永久代還剩餘的內容(主要是型別資訊)全部移到元空間中。
5.物件建立的過程瞭解嗎?
在JVM中物件的建立,我們從一個new指令開始:
-
首先檢查這個指令的引數是否能在常量池中定位到一個類的符號引用
-
檢查這個符號引用代表的類是否已被載入、解析和初始化過。如果沒有,就先執行相應的類載入過程
-
類載入檢查通過後,接下來虛擬機器將為新生物件分配記憶體。
-
記憶體分配完成之後,虛擬機器將分配到的記憶體空間(但不包括物件頭)都初始化為零值。
-
接下來設定物件頭,請求頭裡包含了物件是哪個類的例項、如何才能找到類的後設資料資訊、物件的雜湊碼、物件的GC分代年齡等資訊。
這個過程大概圖示如下:
6.什麼是指標碰撞?什麼是空閒列表?
記憶體分配有兩種方式,指標碰撞(Bump The Pointer)、空閒列表(Free List)。
- 指標碰撞:假設Java堆中記憶體是絕對規整的,所有被使用過的記憶體都被放在一邊,空閒的記憶體被放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閒空間方向挪動一段與物件大小相等的距離,這種分配方式稱為“指標碰撞”。
- 空閒列表:如果Java堆中的記憶體並不是規整的,已被使用的記憶體和空閒的記憶體相互交錯在一起,那就沒有辦法簡單地進行指標碰撞了,虛擬機器就必須維護一個列表,記錄上哪些記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄,這種分配方式稱為“空閒列表”。
- 兩種方式的選擇由Java堆是否規整決定,Java堆是否規整是由選擇的垃圾收集器是否具有壓縮整理能力決定的。
7.JVM 裡 new 物件時,堆會發生搶佔嗎?JVM是怎麼設計來保證執行緒安全的?
會,假設JVM虛擬機器上,每一次new 物件時,指標就會向右移動一個物件size的距離,一個執行緒正在給A物件分配記憶體,指標還沒有來的及修改,另一個為B物件分配記憶體的執行緒,又引用了這個指標來分配記憶體,這就發生了搶佔。
有兩種可選方案來解決這個問題:
-
採用CAS分配重試的方式來保證更新操作的原子性
-
每個執行緒在Java堆中預先分配一小塊記憶體,也就是本地執行緒分配緩衝(Thread Local Allocation
Buffer,TLAB),要分配記憶體的執行緒,先在本地緩衝區中分配,只有本地緩衝區用完了,分配新的快取區時才需要同步鎖定。
8.能說一下物件的記憶體佈局嗎?
在HotSpot虛擬機器裡,物件在堆記憶體中的儲存佈局可以劃分為三個部分:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)。
物件頭主要由兩部分組成:
- 第一部分儲存物件自身的執行時資料:雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,官方稱它為Mark Word,它是個動態的結構,隨著物件狀態變化。
- 第二部分是型別指標,指向物件的類後設資料型別(即物件代表哪個類)。
- 此外,如果物件是一個Java陣列,那還應該有一塊用於記錄陣列長度的資料
例項資料用來儲存物件真正的有效資訊,也就是我們在程式程式碼裡所定義的各種型別的欄位內容,無論是從父類繼承的,還是自己定義的。
對齊填充不是必須的,沒有特別含義,僅僅起著佔位符的作用。
9.物件怎麼訪問定位?
Java程式會通過棧上的reference資料來操作堆上的具體物件。由於reference型別在《Java虛擬機器規範》裡面只規定了它是一個指向物件的引用,並沒有定義這個引用應該通過什麼方式去定位、訪問到堆中物件的具體位置,所以物件訪問方式也是由虛擬機器實現而定的,主流的訪問方式主要有使用控制程式碼和直接指標兩種:
- 如果使用控制程式碼訪問的話,Java堆中將可能會劃分出一塊記憶體來作為控制程式碼池,reference中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與型別資料各自具體的地址資訊,其結構如圖所示:
- 如果使用直接指標訪問的話,Java堆中物件的記憶體佈局就必須考慮如何放置訪問型別資料的相關資訊,reference中儲存的直接就是物件地址,如果只是訪問物件本身的話,就不需要多一次間接訪問的開銷,如圖所示:
這兩種物件訪問方式各有優勢,使用控制程式碼來訪問的最大好處就是reference中儲存的是穩定控制程式碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制程式碼中的例項資料指標,而reference本身不需要被修改。
使用直接指標來訪問最大的好處就是速度更快,它節省了一次指標定位的時間開銷,由於物件訪問在Java中非常頻繁,因此這類開銷積少成多也是一項極為可觀的執行成本。
HotSpot虛擬機器主要使用直接指標來進行物件訪問。
10.記憶體溢位和記憶體洩漏是什麼意思?
記憶體洩露就是申請的記憶體空間沒有被正確釋放,導致記憶體被白白佔用。
記憶體溢位就是申請的記憶體超過了可用記憶體,記憶體不夠了。
兩者關係:記憶體洩露可能會導致記憶體溢位。
用一個有味道的比喻,記憶體溢位就是排隊去蹲坑,發現沒坑位了,記憶體洩漏,就是有人佔著茅坑不拉屎,佔著茅坑不拉屎的多了可能會導致坑位不夠用。
11.能手寫記憶體溢位的例子嗎?
在JVM的幾個記憶體區域中,除了程式計數器外,其他幾個執行時區域都有發生記憶體溢位(OOM)異常的可能,重點關注堆和棧。
- Java堆溢位
Java堆用於儲存物件例項,只要不斷建立不可被回收的物件,比如靜態物件,那麼隨著物件數量的增加,總容量觸及最大堆的容量限制後就會產生記憶體溢位異常(OutOfMemoryError)。
這就相當於一個房子裡,不斷堆積不能被收走的雜物,那麼房子很快就會被堆滿了。
/**
* VM引數: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
- 虛擬機器棧.OutOfMemoryError
JDK使用的HotSpot虛擬機器的棧記憶體大小是固定的,我們可以把棧的記憶體設大一點,然後不斷地去建立執行緒,因為作業系統給每個程式分配的記憶體是有限的,所以到最後,也會發生OutOfMemoryError異常。
/**
* vm引數:-Xss2M
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
12.記憶體洩漏可能由哪些原因導致呢?
記憶體洩漏可能的原因有很多種:
靜態集合類引起記憶體洩漏
靜態集合的生命週期和 JVM 一致,所以靜態集合引用的物件不能被釋放。
public class OOM {
static List list = new ArrayList();
public void oomTests(){
Object obj = new Object();
list.add(obj);
}
}
單例模式
和上面的例子原理類似,單例物件在初始化後會以靜態變數的方式在 JVM 的整個生命週期中存在。如果單例物件持有外部的引用,那麼這個外部物件將不能被 GC 回收,導致記憶體洩漏。
資料連線、IO、Socket等連線
建立的連線不再使用時,需要呼叫 close 方法關閉連線,只有連線被關閉後,GC 才會回收對應的物件(Connection,Statement,ResultSet,Session)。忘記關閉這些資源會導致持續佔有記憶體,無法被 GC 回收。
try {
Connection conn = null;
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("url", "", "");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("....");
} catch (Exception e) {
}finally {
//不關閉連線
}
}
變數不合理的作用域
一個變數的定義作用域大於其使用範圍,很可能存在記憶體洩漏;或不再使用物件沒有及時將物件設定為 null,很可能導致記憶體洩漏的發生。
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他程式碼
//由於作用域原因,method1執行完成之後,object 物件所分配的記憶體不會馬上釋放
object = null;
}
}
hash值發生變化
物件Hash值改變,使用HashMap、HashSet等容器中時候,由於物件修改之後的Hah值和儲存進容器時的Hash值不同,所以無法找到存入的物件,自然也無法單獨刪除了,這也會造成記憶體洩漏。說句題外話,這也是為什麼String型別被設定成了不可變型別。
ThreadLocal使用不當
ThreadLocal的弱引用導致記憶體洩漏也是個老生常談的話題了,使用完ThreadLocal一定要記得使用remove方法來進行清除。
13.如何判斷物件仍然存活?
有兩種方式,引用計數演算法(reference counting)和可達性分析演算法。
- 引用計數演算法
引用計數器的演算法是這樣的:在物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器為零的物件就是不可能再被使用的。
- 可達性分析演算法
目前 Java 虛擬機器的主流垃圾回收器採取的是可達性分析演算法。這個演算法的實質在於將一系列 GC Roots 作為初始的存活物件合集(Gc Root Set),然後從該合集出發,探索所有能夠被該集合引用到的物件,並將其加入到該集合中,這個過程我們也稱之為標記(mark)。最終,未被探索到的物件便是死亡的,是可以回收的。
14.Java中可作為GC Roots的物件有哪幾種?
可以作為GC Roots的主要有四種物件:
- 虛擬機器棧(棧幀中的本地變數表)中引用的物件
- 方法區中類靜態屬性引用的物件
- 方法區中常量引用的物件
- 本地方法棧中JNI引用的物件
15.說一下物件有哪幾種引用?
Java中的引用有四種,分為強引用(Strongly Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。
- 強引用是最傳統的
引用
的定義,是指在程式程式碼之中普遍存在的引用賦值,無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的物件。
Object obj =new Object();
- 軟引用是用來描述一些還有用,但非必須的物件。只被軟引用關聯著的物件,在系統將要發生記憶體溢位異常前,會把這些物件列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的記憶體, 才會丟擲記憶體溢位異常。在JDK 1.2版之後提供了SoftReference類來實現軟引用。
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
SoftReference reference = new SoftReference(obj, queue);
//強引用物件滯空,保留軟引用
obj = null;
- 弱引用也是用來描述那些非必須物件,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生為止。當垃圾收集器開始工作,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。在JDK 1.2版之後提供了WeakReference類來實現弱引用。
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
WeakReference reference = new WeakReference(obj, queue);
//強引用物件滯空,保留軟引用
obj = null;
- 虛引用也稱為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的只是為了能在這個物件被收集器回收時收到一個系統通知。在JDK 1.2版之後提供了PhantomReference類來實現虛引用。
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
PhantomReference reference = new PhantomReference(obj, queue);
//強引用物件滯空,保留軟引用
obj = null;
16.finalize()方法瞭解嗎?有什麼作用?
用一個不太貼切的比喻,垃圾回收就是古代的秋後問斬,finalize()就是刀下留人,在人犯被處決之前,還要做最後一次審計,青天大老爺看看有沒有什麼冤情,需不需要刀下留人。
如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記,隨後進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。如果物件在在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個物件建立關聯即可,譬如把自己 (this關鍵字)賦值給某個類變數或者物件的成員變數,那在第二次標記時它就”逃過一劫“;但是如果沒有抓住這個機會,那麼物件就真的要被回收了。
17.Java堆的記憶體分割槽瞭解嗎?
按照垃圾收集,將Java堆劃分為新生代 (Young Generation)和老年代(Old Generation)兩個區域,新生代存放存活時間短的物件,而每次回收後存活的少量物件,將會逐步晉升到老年代中存放。
而新生代又可以分為三個區域,eden、from、to,比例是8:1:1,而新生代的記憶體分割槽同樣是從垃圾收集的角度來分配的。
18.垃圾收集演算法瞭解嗎?
垃圾收集演算法主要有三種:
- 標記-清除演算法
見名知義,標記-清除
(Mark-Sweep)演算法分為兩個階段:
- 標記 : 標記出所有需要回收的物件
- 清除:回收所有被標記的物件
標記-清除演算法比較基礎,但是主要存在兩個缺點:
- 執行效率不穩定,如果Java堆中包含大量物件,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過程的執行效率都隨物件數量增長而降低。
- 記憶體空間的碎片化問題,標記、清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致當以後在程式執行過程中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
- 標記-複製演算法
標記-複製演算法解決了標記-清除演算法面對大量可回收物件時執行效率低的問題。
過程也比較簡單:將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。
這種演算法存在一個明顯的缺點:一部分空間沒有使用,存在空間的浪費。
新生代垃圾收集主要採用這種演算法,因為新生代的存活物件比較少,每次複製的只是少量的存活物件。當然,實際新生代的收集不是按照這個比例。
- 標記-整理演算法
為了降低記憶體的消耗,引入一種針對性的演算法:標記-整理
(Mark-Compact)演算法。
其中的標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向記憶體空間一端移動,然後直接清理掉邊界以外的記憶體。
標記-整理演算法主要用於老年代,移動存活物件是個極為負重的操作,而且這種操作需要Stop The World才能進行,只是從整體的吞吐量來考量,老年代使用標記-整理演算法更加合適。
19.說一下新生代的區域劃分?
新生代的垃圾收集主要採用標記-複製演算法,因為新生代的存活物件比較少,每次複製少量的存活物件效率比較高。
基於這種演算法,虛擬機器將記憶體分為一塊較大的Eden空間和兩塊較小的 Survivor空間,每次分配記憶體只使用Eden和其中一塊Survivor。發生垃圾收集時,將Eden和Survivor中仍然存活的物件一次性複製到另外一塊Survivor空間上,然後直接清理掉Eden和已用過的那塊Survivor空間。預設Eden和Survivor的大小比例是8∶1。
20.Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC都是什麼意思?
部分收集(Partial GC):指目標不是完整收集整個Java堆的垃圾收集,其中又分為:
- 新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。目前只有CMS收集器會有單獨收集老年代的行為。
- 混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收集器會有這種行為。
整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。
21.Minor GC/Young GC什麼時候觸發?
新建立的物件優先在新生代Eden區進行分配,如果Eden區沒有足夠的空間時,就會觸發Young GC來清理新生代。
22.什麼時候會觸發Full GC?
這個觸發條件稍微有點多,往下看:
- Young GC之前檢查老年代:在要進行 Young GC 的時候,發現
老年代可用的連續記憶體空間
<新生代歷次Young GC後升入老年代的物件總和的平均大小
,說明本次Young GC後可能升入老年代的物件大小,可能超過了老年代當前可用記憶體空間,那就會觸發 Full GC。 - Young GC之後老年代空間不足:執行Young GC之後有一批物件需要放入老年代,此時老年代就是沒有足夠的記憶體空間存放這些物件了,此時必須立即觸發一次Full GC
- 老年代空間不足,老年代記憶體使用率過高,達到一定比例,也會觸發Full GC。
- 空間分配擔保失敗( Promotion Failure),新生代的 To 區放不下從 Eden 和 From 拷貝過來物件,或者新生代物件 GC 年齡到達閾值需要晉升這兩種情況,老年代如果放不下的話都會觸發 Full GC。
- 方法區記憶體空間不足:如果方法區由永久代實現,永久代空間不足 Full GC。
- System.gc()等命令觸發:System.gc()、jmap -dump 等命令會觸發 full gc。
23.物件什麼時候會進入老年代?
長期存活的物件將進入老年代
在物件的物件頭資訊中儲存著物件的迭代年齡,迭代年齡會在每次YoungGC之後物件的移區操作中增加,每一次移區年齡加一.當這個年齡達到15(預設)之後,這個物件將會被移入老年代。
可以通過這個引數設定這個年齡值。
- XX:MaxTenuringThreshold
大物件直接進入老年代
有一些佔用大量連續記憶體空間的物件在被載入就會直接進入老年代.這樣的大物件一般是一些陣列,長字串之類的對。
HotSpot虛擬機器提供了這個引數來設定。
-XX:PretenureSizeThreshold
動態物件年齡判定
為了能更好地適應不同程式的記憶體狀況,HotSpot虛擬機器並不是永遠要求物件的年齡必須達到- XX:MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代。
空間分配擔保
假如在Young GC之後,新生代仍然有大量物件存活,就需要老年代進行分配擔保,把Survivor無法容納的物件直接送入老年代。
24.知道有哪些垃圾收集器嗎?
主要垃圾收集器如下,圖中標出了它們的工作區域、垃圾收集演算法,以及配合關係。
這些收集器裡,面試的重點是兩個——CMS和G1。
- Serial收集器
Serial收集器是最基礎、歷史最悠久的收集器。
如同它的名字(序列),它是一個單執行緒工作的收集器,使用一個處理器或一條收集執行緒去完成垃圾收集工作。並且進行垃圾收集時,必須暫停其他所有工作執行緒,直到垃圾收集結束——這就是所謂的“Stop The World”。
Serial/Serial Old收集器的執行過程如圖:
- ParNew
ParNew收集器實質上是Serial收集器的多執行緒並行版本,使用多條執行緒進行垃圾收集。
ParNew/Serial Old收集器執行示意圖如下:
- Parallel Scavenge
Parallel Scavenge收集器是一款新生代收集器,基於標記-複製演算法實現,也能夠並行收集。和ParNew有些類似,但Parallel Scavenge主要關注的是垃圾收集的吞吐量——所謂吞吐量,就是CPU用於執行使用者程式碼的時間和總消耗時間的比值,比值越大,說明垃圾收集的佔比越小。
- Serial Old
Serial Old是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用標記-整理演算法。
- Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,支援多執行緒併發收集,基於標記-整理演算法實現。
- CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,同樣是老年代的收集器,採用標記-清除演算法。
- Garbage First收集器
Garbage First(簡稱G1)收集器是垃圾收集器的一個顛覆性的產物,它開創了區域性收集的設計思路和基於Region的記憶體佈局形式。
25.什麼是Stop The World ? 什麼是 OopMap ?什麼是安全點?
進行垃圾回收的過程中,會涉及物件的移動。為了保證物件引用更新的正確性,必須暫停所有的使用者執行緒,像這樣的停頓,虛擬機器設計者形象描述為Stop The World
。也簡稱為STW。
在HotSpot中,有個資料結構(對映表)稱為OopMap
。一旦類載入動作完成的時候,HotSpot就會把物件內什麼偏移量上是什麼型別的資料計算出來,記錄到OopMap。在即時編譯過程中,也會在特定的位置
生成 OopMap,記錄下棧上和暫存器裡哪些位置是引用。
這些特定的位置主要在:
-
1.迴圈的末尾(非 counted 迴圈)
-
2.方法臨返回前 / 呼叫方法的call指令後
-
3.可能拋異常的位置
這些位置就叫作安全點(safepoint)。 使用者程式執行時並非在程式碼指令流的任意位置都能夠在停頓下來開始垃圾收集,而是必須是執行到安全點才能夠暫停。
用通俗的比喻,假如老王去拉車,車上東西很重,老王累的汗流浹背,但是老王不能在上坡或者下坡休息,只能在平地上停下來擦擦汗,喝口水。
26.能詳細說一下CMS收集器的垃圾收集過程嗎?
CMS收集齊的垃圾收集分為四步:
- 初始標記(CMS initial mark):單執行緒執行,需要Stop The World,標記GC Roots能直達的物件。
- 併發標記((CMS concurrent mark):無停頓,和使用者執行緒同時執行,從GC Roots直達物件開始遍歷整個物件圖。
- 重新標記(CMS remark):多執行緒執行,需要Stop The World,標記併發標記階段產生物件。
- 併發清除(CMS concurrent sweep):無停頓,和使用者執行緒同時執行,清理掉標記階段標記的死亡的物件。
Concurrent Mark Sweep收集器執行示意圖如下:
27.G1垃圾收集器瞭解嗎?
Garbage First(簡稱G1)收集器是垃圾收集器的一個顛覆性的產物,它開創了區域性收集的設計思路和基於Region的記憶體佈局形式。
雖然G1也仍是遵循分代收集理論設計的,但其堆記憶體的佈局與其他收集器有非常明顯的差異。以前的收集器分代是劃分新生代、老年代、持久代等。
G1把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region採用不同的策略去處理。
這樣就避免了收集整個堆,而是按照若干個Region集進行收集,同時維護一個優先順序列表,跟蹤各個Region回收的“價值,優先收集價值高的Region。
G1收集器的執行過程大致可劃分為以下四個步驟:
- 初始標記(initial mark),標記了從GC Root開始直接關聯可達的物件。STW(Stop the World)執行。
- 併發標記(concurrent marking),和使用者執行緒併發執行,從GC Root開始對堆中物件進行可達性分析,遞迴掃描整個堆裡的物件圖,找出要回收的物件、
- 最終標記(Remark),STW,標記再併發標記過程中產生的垃圾。
- 篩選回收(Live Data Counting And Evacuation),制定回收計劃,選擇多個Region 構成回收集,把回收集中Region的存活物件複製到空的Region中,再清理掉整個舊 Region的全部空間。需要STW。
28.有了CMS,為什麼還要引入G1?
優點:CMS最主要的優點在名字上已經體現出來——併發收集、低停頓。
缺點:CMS同樣有三個明顯的缺點。
- Mark Sweep演算法會導致記憶體碎片比較多
- CMS的併發能力比較依賴於CPU資源,併發回收時垃圾收集執行緒可能會搶佔使用者執行緒的資源,導致使用者程式效能下降。
- 併發清除階段,使用者執行緒依然在執行,會產生所謂的理“浮動垃圾”(Floating Garbage),本次垃圾收集無法處理浮動垃圾,必須到下一次垃圾收集才能處理。如果浮動垃圾太多,會觸發新的垃圾回收,導致效能降低。
G1主要解決了記憶體碎片過多的問題。
29.你們線上用的什麼垃圾收集器?為什麼要用它?
怎麼說呢,雖然調優說的震天響,但是我們一般都是用預設。管你Java怎麼升,我用8,那麼JDK1.8預設用的是什麼呢?
可以使用命令:
java -XX:+PrintCommandLineFlags -version
可以看到有這麼一行:
-XX:+UseParallelGC
UseParallelGC
= Parallel Scavenge + Parallel Old
,表示的是新生代用的Parallel Scavenge
收集器,老年代用的是Parallel Old
收集器。
那為什麼要用這個呢?預設的唄。
當然面試肯定不能這麼答。
Parallel Scavenge的特點是什麼?
高吞吐,我們可以回答:因為我們系統是業務相對複雜,但併發並不是非常高,所以希望儘可能的利用處理器資源,出於提高吞吐量的考慮採用Parallel Scavenge + Parallel Old
的組合。
當然,這個預設雖然也有說法,但不太討喜。
還可以說:
採用Parallel New
+CMS
的組合,我們比較關注服務的響應速度,所以採用了CMS來降低停頓時間。
或者一步到位:
我們線上採用了設計比較優秀的G1垃圾收集器,因為它不僅滿足我們低停頓的要求,而且解決了CMS的浮動垃圾問題、記憶體碎片問題。
30.垃圾收集器應該如何選擇?
垃圾收集器的選擇需要權衡的點還是比較多的——例如執行應用的基礎設施如何?使用JDK的發行商是什麼?等等……
這裡簡單地列一下上面提到的一些收集器的適用場景:
- Serial :如果應用程式有一個很小的記憶體空間(大約100 MB)亦或它在沒有停頓時間要求的單執行緒處理器上執行。
- Parallel:如果優先考慮應用程式的峰值效能,並且沒有時間要求要求,或者可以接受1秒或更長的停頓時間。
- CMS/G1:如果響應時間比吞吐量優先順序高,或者垃圾收集暫停必須保持在大約1秒以內。
- ZGC:如果響應時間是高優先順序的,或者堆空間比較大。
31.物件一定分配在堆中嗎?有沒有了解逃逸分析技術?
物件一定分配在堆中嗎? 不一定的。
隨著JIT編譯期的發展與逃逸分析技術逐漸成熟,所有的物件都分配到堆上也漸漸變得不那麼“絕對”了。其實,在編譯期間,JIT會對程式碼做很多優化。其中有一部分優化的目的就是減少記憶體堆分配壓力,其中一種重要的技術叫做逃逸分析。
什麼是逃逸分析?
逃逸分析是指分析指標動態範圍的方法,它同編譯器優化原理的指標分析和外形分析相關聯。當變數(或者物件)在方法中分配後,其指標有可能被返回或者被全域性引用,這樣就會被其他方法或者執行緒所引用,這種現象稱作指標(或者引用)的逃逸(Escape)。
通俗點講,當一個物件被new出來之後,它可能被外部所呼叫,如果是作為引數傳遞到外部了,就稱之為方法逃逸。
除此之外,如果物件還有可能被外部執行緒訪問到,例如賦值給可以在其它執行緒中訪問的例項變數,這種就被稱為執行緒逃逸。
逃逸分析的好處
- 棧上分配
如果確定一個物件不會逃逸到執行緒之外,那麼久可以考慮將這個物件在棧上分配,物件佔用的記憶體隨著棧幀出棧而銷燬,這樣一來,垃圾收集的壓力就降低很多。
- 同步消除
執行緒同步本身是一個相對耗時的過程,如果逃逸分析能夠確定一個變數不會逃逸出執行緒,無法被其他執行緒訪問,那麼這個變數的讀寫肯定就不會有競爭, 對這個變數實施的同步措施也就可以安全地消除掉。
- 標量替換
如果一個資料是基本資料型別,不可拆分,它就被稱之為標量。把一個Java物件拆散,將其用到的成員變數恢復為原始型別來訪問,這個過程就稱為標量替換。假如逃逸分析能夠證明一個物件不會被方法外部訪問,並且這個物件可以被拆散,那麼可以不建立物件,直接用建立若干個成員變數代替,可以讓物件的成員變數在棧上分配和讀寫。
JVM調優
32.有哪些常用的命令列效能監控和故障處理工具?
-
作業系統工具
- top:顯示系統整體資源使用情況
- vmstat:監控記憶體和CPU
- iostat:監控IO使用
- netstat:監控網路使用
-
JDK效能監控工具
- jps:虛擬機器程式檢視
- jstat:虛擬機器執行時資訊檢視
- jinfo:虛擬機器配置檢視
- jmap:記憶體映像(匯出)
- jhat:堆轉儲快照分析
- jstack:Java堆疊跟蹤
- jcmd:實現上面除了jstat外所有命令的功能
33.瞭解哪些視覺化的效能監控和故障處理工具?
以下是一些JDK自帶的視覺化效能監控和故障處理工具:
- JConsole
- VisualVM
- Java Mission Control
除此之外,還有一些第三方的工具:
- MAT
Java 堆記憶體分析工具。
- GChisto
GC 日誌分析工具。
- GCViewer
GC
日誌分析工具。
- JProfiler
商用的效能分析利器。
- arthas
阿里開源診斷工具。
- async-profiler
Java 應用效能分析工具,開源、火焰圖、跨平臺。
34.JVM的常見引數配置知道哪些?
一些常見的引數配置:
堆配置:
- -Xms:初始堆大小
- -Xms:最大堆大小
- -XX:NewSize=n:設定年輕代大小
- -XX:NewRatio=n:設定年輕代和年老代的比值。如:為3表示年輕代和年老代比值為1:3,年輕代佔整個年輕代年老代和的1/4
- -XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如3表示Eden: 3 Survivor:2,一個Survivor區佔整個年輕代的1/5
- -XX:MaxPermSize=n:設定持久代大小
收集器設定:
- -XX:+UseSerialGC:設定序列收集器
- -XX:+UseParallelGC:設定並行收集器
- -XX:+UseParalledlOldGC:設定並行年老代收集器
- -XX:+UseConcMarkSweepGC:設定併發收集器
並行收集器設定
- -XX:ParallelGCThreads=n:設定並行收集器收集時使用的CPU數。並行收集執行緒數
- -XX:MaxGCPauseMillis=n:設定並行收集最大的暫停時間(如果到這個時間了,垃圾回收器依然沒有回收完,也會停止回收)
- -XX:GCTimeRatio=n:設定垃圾回收時間佔程式執行時間的百分比。公式為:1/(1+n)
- -XX:+CMSIncrementalMode:設定為增量模式。適用於單CPU情況
- -XX:ParallelGCThreads=n:設定併發收集器年輕代手機方式為並行收集時,使用的CPU數。並行收集執行緒數
列印GC回收的過程日誌資訊
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
35.有做過JVM調優嗎?
JVM調優是一件很嚴肅的事情,不是拍腦門就開始調優的,需要有嚴密的分析和監控機制,大概的一個JVM調優流程圖:
實際上,JVM調優是不得已而為之,有那功夫,好好把爛程式碼重構一下不比瞎調JVM強。
但是,面試官非要問怎麼辦?可以從處理問題的角度來回答(對應圖中事後),這是一箇中規中矩的案例:電商公司的運營後臺系統,偶發性的引發OOM異常,堆記憶體溢位。
1、因為是偶發性的,所以第一次簡單的認為就是堆記憶體不足導致,單方面的加大了堆記憶體從4G調整到8G -Xms8g。
2、但是問題依然沒有解決,只能從堆記憶體資訊下手,通過開啟了-XX:+HeapDumpOnOutOfMemoryError引數 獲得堆記憶體的dump檔案。
3、用JProfiler 對 堆dump檔案進行分析,通過JProfiler檢視到佔用記憶體最大的物件是String物件,本來想跟蹤著String物件找到其引用的地方,但dump檔案太大,跟蹤進去的時候總是卡死,而String物件佔用比較多也比較正常,最開始也沒有認定就是這裡的問題,於是就從執行緒資訊裡面找突破點。
4、通過執行緒進行分析,先找到了幾個正在執行的業務執行緒,然後逐一跟進業務執行緒看了下程式碼,有個方法引起了我的注意,匯出訂單資訊
。
5、因為訂單資訊匯出這個方法可能會有幾萬的資料量,首先要從資料庫裡面查詢出來訂單資訊,然後把訂單資訊生成excel,這個過程會產生大量的String物件。
6、為了驗證自己的猜想,於是準備登入後臺去測試下,結果在測試的過程中發現匯出訂單的按鈕前端居然沒有做點選後按鈕置灰互動事件,後端也沒有做防止重複提交,因為匯出訂單資料本來就非常慢,使用的人員可能發現點選後很久後頁面都沒反應,然後就一直點,結果就大量的請求進入到後臺,堆記憶體產生了大量的訂單物件和EXCEL物件,而且方法執行非常慢,導致這一段時間內這些物件都無法被回收,所以最終導致記憶體溢位。
7、知道了問題就容易解決了,最終沒有調整任何JVM引數,只是做了兩個處理:
- 在前端的匯出訂單按鈕上加上了置灰狀態,等後端響應之後按鈕才可以進行點選
- 後端程式碼加分散式鎖,做防重處理
這樣雙管齊下,保證匯出的請求不會一直打到服務端,問題解決!
36.線上服務CPU佔用過高怎麼排查?
問題分析:CPU高一定是某個程式長期佔用了CPU資源。
1、所以先需要找出那個程式佔用CPU高。
- top 列出系統各個程式的資源佔用情況。
2、然後根據找到對應進行裡哪個執行緒佔用CPU高。
- top -Hp 程式ID 列出對應程式裡面的執行緒佔用資源情況
3、找到對應執行緒ID後,再列印出對應執行緒的堆疊資訊
- printf "%x\n" PID 把執行緒ID轉換為16進位制。
- jstack PID 列印出程式的所有執行緒資訊,從列印出來的執行緒資訊中找到上一步轉換為16進位制的執行緒ID對應的執行緒資訊。
4、最後根據執行緒的堆疊資訊定位到具體業務方法,從程式碼邏輯中找到問題所在。
檢視是否有執行緒長時間的watting 或blocked,如果執行緒長期處於watting狀態下, 關注watting on xxxxxx,說明執行緒在等待這把鎖,然後根據鎖的地址找到持有鎖的執行緒。
37.記憶體飆高問題怎麼排查?
分析: 記憶體飈高如果是發生在java程式上,一般是因為建立了大量物件所導致,持續飈高說明垃圾回收跟不上物件建立的速度,或者記憶體洩露導致物件無法回收。
1、先觀察垃圾回收的情況
- jstat -gc PID 1000 檢視GC次數,時間等資訊,每隔一秒列印一次。
- jmap -histo PID | head -20 檢視堆記憶體佔用空間最大的前20個物件型別,可初步檢視是哪個物件佔用了記憶體。
如果每次GC次數頻繁,而且每次回收的記憶體空間也正常,那說明是因為物件建立速度快導致記憶體一直佔用很高;如果每次回收的記憶體非常少,那麼很可能是因為記憶體洩露導致記憶體一直無法被回收。
2、匯出堆記憶體檔案快照
- jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump堆記憶體資訊到檔案。
3、使用visualVM對dump檔案進行離線分析,找到佔用記憶體高的物件,再找到建立該物件的業務程式碼位置,從程式碼和業務場景中定位具體問題。
38.頻繁 minor gc 怎麼辦?
優化Minor GC頻繁問題:通常情況下,由於新生代空間較小,Eden區很快被填滿,就會導致頻繁Minor GC,因此可以通過增大新生代空間-Xmn
來降低Minor GC的頻率。
39.頻繁Full GC怎麼辦?
Full GC的排查思路大概如下:
- 清楚從程式角度,有哪些原因導致FGC?
- 大物件:系統一次性載入了過多資料到記憶體中(比如SQL查詢未做分頁),導致大物件進入了老年代。
- 記憶體洩漏:頻繁建立了大量物件,但是無法被回收(比如IO物件使用完後未呼叫close方法釋放資源),先引發FGC,最後導致OOM.
- 程式頻繁生成一些長生命週期的物件,當這些物件的存活年齡超過分代年齡時便會進入老年代,最後引發FGC. (即本文中的案例)
- 程式BUG
- 程式碼中顯式呼叫了gc方法,包括自己的程式碼甚至框架中的程式碼。
- JVM引數設定問題:包括總記憶體大小、新生代和老年代的大小、Eden區和S區的大小、元空間大小、垃圾回收演算法等等。
- 清楚排查問題時能使用哪些工具
- 公司的監控系統:大部分公司都會有,可全方位監控JVM的各項指標。
- JDK的自帶工具,包括jmap、jstat等常用命令:
# 檢視堆記憶體各區域的使用率以及GC情況
jstat -gcutil -h20 pid 1000
# 檢視堆記憶體中的存活物件,並按空間排序
jmap -histo pid | head -n20
# dump堆記憶體檔案
jmap -dump:format=b,file=heap pid
- 視覺化的堆記憶體分析工具:JVisualVM、MAT等
- 排查指南
- 檢視監控,以瞭解出現問題的時間點以及當前FGC的頻率(可對比正常情況看頻率是否正常)
- 瞭解該時間點之前有沒有程式上線、基礎元件升級等情況。
- 瞭解JVM的引數設定,包括:堆空間各個區域的大小設定,新生代和老年代分別採用了哪些垃圾收集器,然後分析JVM引數設定是否合理。
- 再對步驟1中列出的可能原因做排除法,其中元空間被打滿、記憶體洩漏、程式碼顯式呼叫gc方法比較容易排查。
- 針對大物件或者長生命週期物件導致的FGC,可通過 jmap -histo 命令並結合dump堆記憶體檔案作進一步分析,需要先定位到可疑物件。
- 通過可疑物件定位到具體程式碼再次分析,這時候要結合GC原理和JVM引數設定,弄清楚可疑物件是否滿足了進入到老年代的條件才能下結論。
40.有沒有處理過記憶體洩漏問題?是如何定位的?
記憶體洩漏是內在病源,外在病症表現可能有:
- 應用程式長時間連續執行時效能嚴重下降
- CPU 使用率飆升,甚至到 100%
- 頻繁 Full GC,各種報警,例如介面超時報警等
- 應用程式丟擲
OutOfMemoryError
錯誤 - 應用程式偶爾會耗盡連線物件
嚴重記憶體洩漏往往伴隨頻繁的 Full GC,所以分析排查記憶體洩漏問題首先還得從檢視 Full GC 入手。主要有以下操作步驟:
-
使用
jps
檢視執行的 Java 程式 ID -
使用
top -p [pid]
檢視程式使用 CPU 和 MEM 的情況 -
使用
top -Hp [pid]
檢視程式下的所有執行緒佔 CPU 和 MEM 的情況 -
將執行緒 ID 轉換為 16 進位制:
printf "%x\n" [pid]
,輸出的值就是執行緒棧資訊中的 nid。例如:
printf "%x\n" 29471
,換行輸出 731f。 -
抓取執行緒棧:
jstack 29452 > 29452.txt
,可以多抓幾次做個對比。線上程棧資訊中找到對應執行緒號的 16 進位制值,如下是 731f 執行緒的資訊。執行緒棧分析可使用 Visualvm 外掛 TDA。
"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fbe2c164000 nid=0x731f runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE
-
使用
jstat -gcutil [pid] 5000 10
每隔 5 秒輸出 GC 資訊,輸出 10 次,檢視 YGC 和 Full GC 次數。通常會出現 YGC 不增加或增加緩慢,而 Full GC 增加很快。或使用
jstat -gccause [pid] 5000
,同樣是輸出 GC 摘要資訊。或使用
jmap -heap [pid]
檢視堆的摘要資訊,關注老年代記憶體使用是否達到閥值,若達到閥值就會執行 Full GC。 -
如果發現
Full GC
次數太多,就很大概率存在記憶體洩漏了 -
使用
jmap -histo:live [pid]
輸出每個類的物件數量,記憶體大小(位元組單位)及全限定類名。 -
生成
dump
檔案,藉助工具分析哪 個物件非常多,基本就能定位到問題在那了使用 jmap 生成 dump 檔案:
# jmap -dump:live,format=b,file=29471.dump 29471 Dumping heap to /root/dump ... Heap dump file created
- dump 檔案分析
可以使用 jhat 命令分析:
jhat -port 8000 29471.dump
,瀏覽器訪問 jhat 服務,埠是 8000。通常使用圖形化工具分析,如 JDK 自帶的 jvisualvm,從選單 > 檔案 > 裝入 dump 檔案。
或使用第三方式具分析的,如 JProfiler 也是個圖形化工具,GCViewer 工具。Eclipse 或以使用 MAT 工具檢視。或使用線上分析平臺 GCEasy。
注意:如果 dump 檔案較大的話,分析會佔比較大的記憶體。
- 在 dump 文析結果中查詢存在大量的物件,再查對其的引用。
基本上就可以定位到程式碼層的邏輯了。
41.有沒有處理過記憶體溢位問題?
記憶體洩漏和記憶體溢位二者關係非常密切,記憶體溢位可能會有很多原因導致,記憶體洩漏最可能的罪魁禍首之一。
排查過程和排查記憶體洩漏過程類似。
虛擬機器執行
42.能說一下類的生命週期嗎?
一個類從被載入到虛擬機器記憶體中開始,到從記憶體中解除安裝,整個生命週期需要經過七個階段:載入 (Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和解除安裝(Unloading),其中驗證、準備、解析三個部分統稱為連線(Linking)。
43.類載入的過程知道嗎?
載入是JVM載入的起點,具體什麼時候開始載入,《Java虛擬機器規範》中並沒有進行強制約束,可以交給虛擬機器的具體實現來自由把握。
在載入過程,JVM要做三件事情:
-
1)通過一個類的全限定名來獲取定義此類的二進位制位元組流。
-
2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
-
3)在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。
載入階段結束後,Java虛擬機器外部的二進位制位元組流就按照虛擬機器所設定的格式儲存在方法區之中了,方法區中的資料儲存格式完全由虛擬機器實現自行定義,《Java虛擬機器規範》未規定此區域的具體資料結構。
型別資料妥善安置在方法區之後,會在Java堆記憶體中例項化一個java.lang.Class類的物件, 這個物件將作為程式訪問方法區中的型別資料的外部介面。
44.類載入器有哪些?
主要有四種類載入器:
-
啟動類載入器(Bootstrap ClassLoader)用來載入java核心類庫,無法被java程式直接引用。
-
擴充套件類載入器(extensions class loader):它用來載入 Java 的擴充套件庫。Java 虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java 類。
-
系統類載入器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來載入Java 類。一般來說,Java 應用的類都是由它來完成載入的。可以通過ClassLoader.getSystemClassLoader()來獲取它。
-
使用者自定義類載入器 (user class loader),使用者通過繼承 java.lang.ClassLoader類的方式自行實現的類載入器。
45.什麼是雙親委派機制?
雙親委派模型的工作過程:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到最頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求時,子載入器才會嘗試自己去完成載入。
46.為什麼要用雙親委派機制?
答案是為了保證應用程式的穩定有序。
例如類java.lang.Object,它存放在rt.jar之中,通過雙親委派機制,保證最終都是委派給處於模型最頂端的啟動類載入器進行載入,保證Object的一致。反之,都由各個類載入器自行去載入的話,如果使用者自己也編寫了一個名為java.lang.Object的類,並放在程式的 ClassPath中,那系統中就會出現多個不同的Object類。
47.如何破壞雙親委派機制?
如果不想打破雙親委派模型,就重寫ClassLoader類中的fifindClass()方法即可,無法被父類載入器載入的類最終會通過這個方法被載入。而如果想打破雙親委派模型則需要重寫loadClass()方法。
48.歷史上有哪幾次雙親委派機制的破壞?
雙親委派機制在歷史上主要有三次破壞:
第一次破壞
雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前——即JDK 1.2面世以前的“遠古”時代。
由於雙親委派模型在JDK 1.2之後才被引入,但是類載入器的概念和抽象類 java.lang.ClassLoader則在Java的第一個版本中就已經存在,為了向下相容舊程式碼,所以無法以技術手段避免loadClass()被子類覆蓋的可能性,只能在JDK 1.2之後的java.lang.ClassLoader中新增一個新的 protected方法findClass(),並引導使用者編寫的類載入邏輯時儘可能去重寫這個方法,而不是在 loadClass()中編寫程式碼。
第二次破壞
雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷導致的,如果有基礎型別又要呼叫回使用者的程式碼,那該怎麼辦呢?
例如我們比較熟悉的JDBC:
各個廠商各有不同的JDBC的實現,Java在核心包\lib
裡定義了對應的SPI,那麼這個就毫無疑問由啟動類載入器
載入器載入。
但是各個廠商的實現,是沒辦法放在核心包裡的,只能放在classpath
裡,只能被應用類載入器
載入。那麼,問題來了,啟動類載入器它就載入不到廠商提供的SPI服務程式碼。
為了解決這個問題,引入了一個不太優雅的設計:執行緒上下文類載入器 (Thread Context ClassLoader)。這個類載入器可以通過java.lang.Thread類的setContext-ClassLoader()方法進行設定,如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過的話,那這個類載入器預設就是應用程式類載入器。
JNDI服務使用這個執行緒上下文類載入器去載入所需的SPI服務程式碼,這是一種父類載入器去請求子類載入器完成類載入的行為。
第三次破壞
雙親委派模型的第三次“被破壞”是由於使用者對程式動態性的追求而導致的,例如程式碼熱替換(Hot Swap)、模組熱部署(Hot Deployment)等。
OSGi實現模組化熱部署的關鍵是它自定義的類載入器機制的實現,每一個程式模組(OSGi中稱為 Bundle)都有一個自己的類載入器,當需要更換一個Bundle時,就把Bundle連同類載入器一起換掉以實現程式碼的熱替換。在OSGi環境下,類載入器不再雙親委派模型推薦的樹狀結構,而是進一步發展為更加複雜的網狀結構。
49.你覺得應該怎麼實現一個熱部署功能?
我們已經知道了Java類的載入過程。一個Java類檔案到虛擬機器裡的物件,要經過如下過程:首先通過Java編譯器,將Java檔案編譯成class位元組碼,類載入器讀取class位元組碼,再將類轉化為例項,對例項newInstance就可以生成物件。
類載入器ClassLoader功能,也就是將class位元組碼轉換到類的例項。在Java應用中,所有的例項都是由類載入器,載入而來。
一般在系統中,類的載入都是由系統自帶的類載入器完成,而且對於同一個全限定名的java類(如com.csiar.soc.HelloWorld),只能被載入一次,而且無法被解除安裝。
這個時候問題就來了,如果我們希望將java類解除安裝,並且替換更新版本的java類,該怎麼做呢?
既然在類載入器中,Java類只能被載入一次,並且無法解除安裝。那麼我們是不是可以直接把Java類載入器幹掉呢?答案是可以的,我們可以自定義類載入器,並重寫ClassLoader的findClass方法。
想要實現熱部署可以分以下三個步驟:
- 銷燬原來的自定義ClassLoader
- 更新class類檔案
- 建立新的ClassLoader去載入更新後的class類檔案。
到此,一個熱部署的功能就這樣實現了。
50.Tomcat的類載入機制瞭解嗎?
Tomcat是主流的Java Web伺服器之一,為了實現一些特殊的功能需求,自定義了一些類載入器。
Tomcat類載入器如下:
Tomcat實際上也是破壞了雙親委派模型的。
Tomact是web容器,可能需要部署多個應用程式。不同的應用程式可能會依賴同一個第三方類庫的不同版本,但是不同版本的類庫中某一個類的全路徑名可能是一樣的。如多個應用都要依賴hollis.jar,但是A應用需要依賴1.0.0版本,但是B應用需要依賴1.0.1版本。這兩個版本中都有一個類是com.hollis.Test.class。如果採用預設的雙親委派類載入機制,那麼無法載入多個相同的類。
所以,Tomcat破壞了雙親委派原則,提供隔離的機制,為每個web容器單獨提供一個WebAppClassLoader載入器。每一個WebAppClassLoader負責載入本身的目錄下的class檔案,載入不到時再交CommonClassLoader載入,這和雙親委派剛好相反。
好了,本期的50道JVM面試題就分享到這了,下期繼續分享Java併發相關面試題,點贊、關注 不迷路,我們們下期見!
參考:
[1].《深入理解Java虛擬機器》
[3]. 《不看後悔》超讚!來一份常見 JVM 面試題+“答案”!
[5]. 炸了!一口氣問了我18個JVM問!
[6]. 從實際案例聊聊Java應用的GC優化
[7]. JVM系列(二):JVM 記憶體洩漏與記憶體溢位及問題排查
[8] .《實戰Java虛擬機器效能優化》
[10].【JVM進階之路】十:JVM調優總結
![](https://img2020.cnblogs.com/blog/1659274/202112/1659274-20211229094625509-403453558.png)