大家好,我是老三,“面渣逆襲“系列繼續,這節我們來搞定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虛擬機》
[2].《不看后悔》38個JVM精選問答,讓你變成專家!
[3]. 《不看后悔》超贊!來一份常見 JVM 面試題+“答案”!
[4]. JVM性能優化–類加載器,手動實作類的熱加載
[5]. 炸了!一口氣問了我18個JVM問!
[6]. 從實際案例聊聊Java應用的GC優化
[7]. JVM系列(二):JVM 記憶體泄漏與記憶體溢位及問題排查
[8] .《實戰Java虛擬機性能優化》
[9]. 再清楚不過了,JVM逃逸分析,你一定得知道
[10].【JVM進階之路】十:JVM調優總結
對了,老三最近正在參加博客之星的評選,讀者大大如果覺得文章有幫助,不妨順手給老三一個五星好評吧!
- PC主頁:老三的主頁
- 互動區:老三的活動頁

面渣逆襲系列:
-
面渣逆襲:Java集合連環三十問
-
面渣逆襲:執行緒池奪命連環十八問
-
面試位元組,被作業系統問掛了

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/397431.html
標籤:其他
上一篇:遞回函式典型應用
