這是本期的 JVM面試題目錄,不會的快快查漏補缺~

1. 什么是JVM記憶體結構?

jvm將虛擬機分為5大區域,程式計數器、虛擬機堆疊、本地方法堆疊、java堆、方法區;
-
程式計數器:執行緒私有的,是一塊很小的記憶體空間,作為當前執行緒的行號指示器,用于記錄當前虛擬機正在執行的執行緒指令地址;
-
虛擬機堆疊:執行緒私有的,每個方法執行的時候都會創建一個堆疊幀,用于存盤區域變數表、運算元、動態鏈接和方法回傳等資訊,當執行緒請求的堆疊深度超過了虛擬機允許的最大深度時,就會拋出StackOverFlowError;
-
本地方法堆疊:執行緒私有的,保存的是native方法的資訊,當一個jvm創建的執行緒呼叫native方法后,jvm不會在虛擬機堆疊中為該執行緒創建堆疊幀,而是簡單的動態鏈接并直接呼叫該方法;
-
堆:java堆是所有執行緒共享的一塊記憶體,幾乎所有物件的實體和陣列都要在堆上分配記憶體,因此該區域經常發生垃圾回收的操作;
-
方法區:存放已被加載的類資訊、常量、靜態變數、即時編譯器編譯后的代碼資料,即永久代,在jdk1.8中不存在方法區了,被元資料區替代了,原方法區被分成兩部分;1:加載的類資訊,2:運行時常量池;加載的類資訊被保存在元資料區中,運行時常量池保存在堆中;
2. 什么是JVM記憶體模型?
Java 記憶體模型(下文簡稱 JMM )就是在底層處理器記憶體模型的基礎上,定義自己的多執行緒語意,它明確指定了一組排序規則,來保證執行緒間的可見性,
這一組規則被稱為 Happens-Before , JMM 規定,要想保證 B 操作能夠看到 A 操作的結果(無論它們是否在同一個執行緒),那么 A 和 B 之間必須滿足 Happens-Before 關系 :
-
單執行緒規則:一個執行緒中的每個動作都 happens-before 該執行緒中后續的每個動作
-
監視器鎖定規則:監聽器的 解鎖 動作 happens-before 后續對這個監聽器的 鎖定 動作
-
volatile 變數規則:對 volatile 欄位的寫入動作 happens-before 后續對這個欄位的每個讀取動作
-
執行緒 start 規則:執行緒 start() 方法的執行 happens-before 一個啟動執行緒內的任意動作
-
執行緒 join 規則:一個執行緒內的所有動作 happens-before 任意其他執行緒在該執行緒 join() 成功回傳之前
-
傳遞性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C
怎么理解 happens-before 呢?如果按字面意思,比如第二個規則,執行緒(不管是不是同一個)的解鎖動作發生在鎖定之前?這明顯不對,happens-before 也是為了保證可見性,比如那個解鎖和加鎖的動作,可以這樣理解,執行緒1釋放鎖退出同步塊,執行緒2加鎖進入同步塊,那么執行緒2就能看見執行緒1對共享物件修改的結果,

Java 提供了幾種語言結構,包括 volatile , final 和 synchronized , 它們旨在幫助程式員向 編譯器 描述程式的并發要求,其中:
-
volatile- 保證 可見性 和 有序性
-
synchronized- 保證 可見性 和 有序性 ; 通過**管程(Monitor)* 保證一組動作的* 原子性
-
final- 通過禁止 在建構式初始化 和 給 final 欄位賦值 這兩個動作的重排序,保證 可見性 (如果 this 參考逃逸 就不好說可見性了)
編譯器在遇到這些關鍵字時,會插入相應的記憶體屏障,保證語意的正確性,
有一點需要 注意 的是, synchronized 不保證 同步塊內的代碼禁止重排序,因為它通過鎖保證同一時刻只有 一個執行緒 訪問同步塊(或臨界區),也就是說同步塊的代碼只需滿足 as-if-serial 語意 - 只要單執行緒的執行結果不改變,可以進行重排序,
所以說,Java 記憶體模型描述的是多執行緒對共享記憶體修改后彼此之間的可見性,另外,還確保正確同步的 Java 代碼可以在不同體系結構的處理器上正確運行,
3. heap 和stack 有什么區別?
(1)申請方式
stack:由系統自動分配,例如,宣告在函式中一個區域變數 int b; 系統自動在堆疊中為 b 開辟空間
heap:需要程式員自己申請,并指明大小,在 c 中 malloc 函式,對于Java 需要手動 new Object()的形式開辟
(2)申請后系統的回應
stack:只要堆疊的剩余空間大于所申請空間,系統將為程式提供記憶體,否則將報例外提示堆疊溢位,
heap:首先應該知道作業系統有一個記錄空閑記憶體地址的鏈表,當系統收到程式的申請時,會遍歷該鏈表,尋找第一個空間大于所申請空間的堆結點,然后將該結點從空閑結點鏈表中洗掉,并將該結點的空間分配給程式,另外,由于找到的堆結點的大小不一定正好等于申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中,
(3)申請大小的限制
stack:堆疊是向低地址擴展的資料結構,是一塊連續的記憶體的區域,這句話的意思是堆疊頂的地址和堆疊的最大容量是系統預先規定好的,在 WINDOWS 下,堆疊的大小是 2M(默認值也取決于虛擬記憶體的大小),如果申請的空間超過堆疊的剩余空間時,將提示 overflow,因此,能從堆疊獲得的空間較小,
heap:堆是向高地址擴展的資料結構,是不連續的記憶體區域,這是由于系統是用鏈表來存盤的空閑記憶體地址的, 自然是不連續的,而鏈表的遍歷方向是由低地址向高地址,堆的大小受限于計算機系統中有效的虛擬記憶體,由此可見, 堆獲得的空間比較靈活,也比較大,
(4)申請效率的比較
stack:由系統自動分配,速度較快,但程式員是無法控制的,
heap:由 new 分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便,
(5)heap和stack中的存盤內容
stack:在函式呼叫時,第一個進堆疊的是主函式中后的下一條指令(函式呼叫陳述句的下一條可執行陳述句)的地址, 然后是函式的各個引數,在大多數的 C 編譯器中,引數是由右往左入堆疊的,然后是函式中的區域變數,注意靜態變數是不入堆疊的,
當本次函式呼叫結束后,區域變數先出堆疊,然后是引數,最后堆疊頂指標指向最開始存的地址,也就是主函式中的下一條指令,程式由該點繼續運行,
heap:一般是在堆的頭部用一個位元組存放堆的大小,堆中的具體內容有程式員安排,
4. 什么情況下會發生堆疊記憶體溢位?
1、堆疊是執行緒私有的,堆疊的生命周期和執行緒一樣,每個方法在執行的時候就會創建一個堆疊幀,它包含區域變數表、運算元堆疊、動態鏈接、方法出口等資訊,區域變數表又包括基本資料型別和物件的參考;2、當執行緒請求的堆疊深度超過了虛擬機允許的最大深度時,會拋出StackOverFlowError例外,方法遞回呼叫肯可能會出現該問題;3、調整引數-xss去調整jvm堆疊的大小
5. 談談對 OOM 的認識?如何排查 OOM 的問題?
除了程式計數器,其他記憶體區域都有 OOM 的風險,
-
堆疊一般經常會發生 StackOverflowError,比如 32 位的 windows 系統單行程限制 2G 記憶體,無限創建執行緒就會發生堆疊的 OOM
-
Java 8 常量池移到堆中,溢位會出 java.lang.OutOfMemoryError: Java heap space,設定最大元空間大小引數無效;
-
堆記憶體溢位,報錯同上,這種比較好理解,GC 之后無法在堆中申請記憶體創建物件就會報錯;
-
方法區 OOM,經常會遇到的是動態生成大量的類、jsp 等;
-
直接記憶體 OOM,涉及到 -XX:MaxDirectMemorySize 引數和 Unsafe 物件對記憶體的申請,
排查 OOM 的方法:
-
增加兩個引數 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,當 OOM 發生時自動 dump 堆記憶體資訊到指定目錄;
-
同時 jstat 查看監控 JVM 的記憶體和 GC 情況,先觀察問題大概出在什么區域;
-
使用 MAT 工具載入到 dump 檔案,分析大物件的占用情況,比如 HashMap 做快取未清理,時間長了就會記憶體溢位,可以把改為弱參考 ,
6. 談談 JVM 中的常量池?
JVM常量池主要分為 Class檔案常量池、運行時常量池,全域字串常量池,以及基本型別包裝類物件常量池 ,
-
Class檔案常量池,class檔案是一組以位元組為單位的二進制資料流,在java代碼的編譯期間,我們撰寫的java檔案就被編譯為.class檔案格式的二進制資料存放在磁盤中,其中就包括class檔案常量池,
-
運行時常量池:運行時常量池相對于class常量池一大特征就是具有動態性,java規范并不要求常量只能在運行時才產生,也就是說運行時常量池的內容并不全部來自class常量池,在運行時可以通過代碼生成常量并將其放入運行時常量池中,這種特性被用的最多的就是String.intern(),
-
全域字串常量池:字串常量池是JVM所維護的一個字串實體的參考表,在HotSpot VM中,它是一個叫做StringTable的全域表,在字串常量池中維護的是字串實體的參考,底層C++實作就是一個Hashtable,這些被維護的參考所指的字串實體,被稱作”被駐留的字串”或”interned string”或通常所說的”進入了字串常量池的字串”,
-
基本型別包裝類物件常量池:java中基本型別的包裝類的大部分都實作了常量池技術,這些類是Byte,Short,Integer,Long,Character,Boolean,另外兩種浮點數型別的包裝類則沒有實作,另外上面這5種整型的包裝類也只是在對應值小于等于127時才可使用物件池,也即物件不負責創建和管理大于127的這些類的物件,
7. 如何判斷一個物件是否存活?
判斷一個物件是否存活,分為兩種演算法1:參考計數法;2:可達性分析演算法;
參考計數法:給每一個物件設定一個參考計數器,當有一個地方參考該物件的時候,參考計數器就+1,參考失效時,參考計數器就-1;當參考計數器為0的時候,就說明這個物件沒有被參考,也就是垃圾物件,等待回收;缺點:無法解決回圈參考的問題,當A參考B,B也參考A的時候,此時AB物件的參考都不為0,此時也就無法垃圾回收,所以一般主流虛擬機都不采用這個方法;
可達性分析法從一個被稱為GC Roots的物件向下搜索,如果一個物件到GC Roots沒有任何參考鏈相連接時,說明此物件不可用,在java中可以作為GC Roots的物件有以下幾種:
-
虛擬機堆疊中參考的物件
-
方法區類靜態屬性參考的變數
-
方法區常量池參考的物件
-
本地方法堆疊JNI參考的物件
但一個物件滿足上述條件的時候,不會馬上被回收,還需要進行兩次標記;第一次標記:判斷當前物件是否有finalize()方法并且該方法沒有被執行過,若不存在則標記為垃圾物件,等待回收;若有的話,則進行第二次標記;第二次標記將當前物件放入F-Queue佇列,并生成一個finalize執行緒去執行該方法,虛擬機不保證該方法一定會被執行,這是因為如果執行緒執行緩慢或進入了死鎖,會導致回收系統的崩潰;如果執行了finalize方法之后仍然沒有與GC Roots有直接或者間接的參考,則該物件會被回收;
8. 強參考、軟參考、弱參考、虛參考是什么,有什么區別?
-
強參考,就是普通的物件參考關系,如 String s = new String(“ConstXiong”)
-
軟參考,用于維護一些可有可無的物件,只有在記憶體不足時,系統則會回收軟參考物件,如果回收了軟參考物件之后仍然沒有足夠的記憶體,才會拋出記憶體溢位例外,SoftReference 實作
-
弱參考,相比軟參考來說,要更加無用一些,它擁有更短的生命周期,當 JVM 進行垃圾回收時,無論記憶體是否充足,都會回收被弱參考關聯的物件,WeakReference 實作
-
虛參考是一種形同虛設的參考,在現實場景中用的不是很多,它主要用來跟蹤物件被垃圾回收的活動,PhantomReference 實作
9. 被參考的物件就一定能存活嗎?
不一定,看 Reference 型別,弱參考在 GC 時會被回收,軟參考在記憶體不足的時候,即 OOM 前會被回收,但如果沒有在 Reference Chain 中的物件就一定會被回收,
10. Java中的垃圾回收演算法有哪些?
java中有四種垃圾回收演算法,分別是標記清除法、標記整理法、復制演算法、分代收集演算法; 標記清除法 :第一步:利用可達性去遍歷記憶體,把存活物件和垃圾物件進行標記;第二步:在遍歷一遍,將所有標記的物件回收掉;特點:效率不行,標記和清除的效率都不高;標記和清除后會產生大量的不連續的空間分片,可能會導致之后程式運行的時候需分配大物件而找不到連續分片而不得不觸發一次GC;

標記整理法:第一步:利用可達性去遍歷記憶體,把存活物件和垃圾物件進行標記;第二步:將所有的存活的物件向一段移動,將端邊界以外的物件都回收掉;特點:適用于存活物件多,垃圾少的情況;需要整理的程序,無空間碎片產生;

復制演算法:將記憶體按照容量大小分為大小相等的兩塊,每次只使用一塊,當一塊使用完了,就將還存活的物件移到另一塊上,然后在把使用過的記憶體空間移除;特點:不會產生空間碎片;記憶體使用率極低;
分代收集演算法:根據記憶體物件的存活周期不同,將記憶體劃分成幾塊,java虛擬機一般將記憶體分成新生代和老生代,在新生代中,有大量物件死去和少量物件存活,所以采用復制演算法,只需要付出少量存活物件的復制成本就可以完成收集;老年代中因為物件的存活率極高,沒有額外的空間對他進行分配擔保,所以采用標記清理或者標記整理演算法進行回收;
對比

11. 有哪幾種垃圾回收器,各自的優缺點是什么?
垃圾回收器主要分為以下幾種:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;
-
Serial:單執行緒的收集器,收集垃圾時,必須stop the world,使用復制演算法,它的最大特點是在進行垃圾回收時,需要對所有正在執行的執行緒暫停(stop the world),對于有些應用是難以接受的,但是如果應用的實時性要求不是那么高,只要停頓的時間控制在N毫秒之內,大多數應用還是可以接受的,是client級別的默認GC方式,
-
ParNew:Serial收集器的多執行緒版本,也需要stop the world,復制算
-
Parallel Scavenge:新生代收集器,復制演算法的收集器,并發的多執行緒收集器,目標是達到一個可控的吞吐量,和ParNew的最大區別是GC自動調節策略;虛擬機會根據系統的運行狀態收集性能監控資訊,動態設定這些引數,以提供最優停頓時間和最高的吞吐量;
-
Serial Old:Serial收集器的老年代版本,單執行緒收集器,使用標記整理演算法,
-
Parallel Old:是Parallel Scavenge收集器的老年代版本,使用多執行緒,標記-整理演算法,
-
CMS:是一種以獲得最短回收停頓時間為目標的收集器,標記清除演算法,運作程序:初始標記,并發標記,重新標記,并發清除,收集結束會產生大量空間碎片;
-
G1:標記整理演算法實作,運作流程主要包括以下:初始標記,并發標記,最終標記,篩選回收,不會產生空間碎片,可以精確地控制停頓;G1將整個堆分為大小相等的多個Region(區域),G1跟蹤每個區域的垃圾大小,在后臺維護一個優先級串列,每次根據允許的收集時間,優先回收價值最大的區域,已達到在有限時間內獲取盡可能高的回收效率;
垃圾回收器間的配合使用圖:
各個垃圾回收器對比:
12. 詳細說一下CMS的回收程序?CMS的問題是什么?
CMS(Concurrent Mark Sweep,并發標記清除) 收集器是以獲取最短回收停頓時間為目標的收集器(追求低停頓),它在垃圾收集時使得用戶執行緒和 GC 執行緒并發執行,因此在垃圾收集程序中用戶也不會感到明顯的卡頓,
從名字就可以知道,CMS是基于“標記-清除”演算法實作的,CMS 回收程序分為以下四步:
初始標記 (CMS initial mark):主要是標記 GC Root 開始的下級(注:僅下一級)物件,這個程序會 STW,但是跟 GC Root 直接關聯的下級物件不會很多,因此這個程序其實很快,
并發標記 (CMS concurrent mark):根據上一步的結果,繼續向下標識所有關聯的物件,直到這條鏈上的最盡頭,這個程序是多執行緒的,雖然耗時理論上會比較長,但是其它作業執行緒并不會阻塞,沒有 STW,
重新標記(CMS remark):顧名思義,就是要再標記一次,為啥還要再標記一次?因為第 2 步并沒有阻塞其它作業執行緒,其它執行緒在標識程序中,很有可能會產生新的垃圾,
并發清除(CMS concurrent sweep):清除階段是清理洗掉掉標記階段判斷的已經死亡的物件,由于不需要移動存活物件,所以這個階段也是可以與用戶執行緒同時并發進行的,
CMS 的問題:
1. 并發回收導致CPU資源緊張:
在并發階段,它雖然不會導致用戶執行緒停頓,但卻會因為占用了一部分執行緒而導致應用程式變慢,降低程式總吞吐量,CMS默認啟動的回收執行緒數是:(CPU核數 + 3)/ 4,當CPU核數不足四個時,CMS對用戶程式的影響就可能變得很大,
2. 無法清理浮動垃圾:
在CMS的并發標記和并發清理階段,用戶執行緒還在繼續運行,就還會伴隨有新的垃圾物件不斷產生,但這一部分垃圾物件是出現在標記程序結束以后,CMS無法在當次收集中處理掉它們,只好留到下一次垃圾收集時再清理掉,這一部分垃圾稱為“浮動垃圾”,
3. 并發失敗(Concurrent Mode Failure):
由于在垃圾回收階段用戶執行緒還在并發運行,那就還需要預留足夠的記憶體空間提供給用戶執行緒使用,因此CMS不能像其他回收器那樣等到老年代幾乎完全被填滿了再進行回收,必須預留一部分空間供并發回收時的程式運行使用,默認情況下,當老年代使用了 92% 的空間后就會觸發 CMS 垃圾回收,這個值可以通過 -XX**😗* CMSInitiatingOccupancyFraction 引數來設定,
這里會有一個風險:要是CMS運行期間預留的記憶體無法滿足程式分配新物件的需要,就會出現一次“并發失敗”(Concurrent Mode Failure),這時候虛擬機將不得不啟動后備預案:Stop The World,臨時啟用 Serial Old 來重新進行老年代的垃圾回收,這樣一來停頓時間就很長了,
4.記憶體碎片問題:
CMS是一款基于“標記-清除”演算法實作的回收器,這意味著回收結束時會有記憶體碎片產生,記憶體碎片過多時,將會給大物件分配帶來麻煩,往往會出現老年代還有很多剩余空間,但就是無法找到足夠大的連續空間來分配當前物件,而不得不提前觸發一次 Full GC 的情況,
為了解決這個問題,CMS收集器提供了一個 -XX**: +UseCMSCompactAtFullCollection 開關引數(默認開啟),用于在 Full GC 時開啟記憶體碎片的合并整理程序,由于這個記憶體整理必須移動存活物件,是無法并發的,這樣停頓時間就會變長,還有另外一個引數 -XX 😗*CMSFullGCsBeforeCompaction,這個引數的作用是要求CMS在執行過若干次不整理空間的 Full GC 之后,下一次進入 Full GC 前會先進行碎片整理(默認值為0,表示每次進入 Full GC 時都進行碎片整理),
13. 詳細說一下G1的回收程序?
G1(Garbage First)回收器采用面向區域收集的設計思路和基于Region的記憶體布局形式,是一款主要面向服務端應用的垃圾回收器,G1設計初衷就是替換 CMS,成為一種全功能收集器,G1 在JDK9 之后成為服務端模式下的默認垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默認組合,而 CMS 被宣告為不推薦使用的垃圾回收器,G1從整體來看是基于 標記-整理 演算法實作的回收器,但從區域(兩個Region之間)上看又是基于 標記-復制 演算法實作的,
G1 回收程序,G1 回收器的運作程序大致可分為四個步驟:
初始標記(會STW):僅僅只是標記一下 GC Roots 能直接關聯到的物件,并且修改TAMS指標的值,讓下一階段用戶執行緒并發運行時,能正確地在可用的Region中分配新物件,這個階段需要停頓執行緒,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際并沒有額外的停頓,
并發標記:從 GC Roots 開始對堆中物件進行可達性分析,遞回掃描整個堆里的物件圖,找出要回收的物件,這階段耗時較長,但可與用戶程式并發執行,當物件圖掃描完成以后,還要重新處理在并發時有參考變動的物件,
最終標記(會STW):對用戶執行緒做短暫的暫停,處理并發階段結束后仍有參考變動的物件,
清理階段(會STW):更新Region的統計資料,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然后把決定回收的那一部分Region的存活物件復制到空的Region中,再清理掉整個舊Region的全部空間,這里的操作涉及存活物件的移動,必須暫停用戶執行緒,由多潭訓收器執行緒并行完成的,
14. JVM中一次完整的GC是什么樣子的?
先描述一下Java堆記憶體劃分,
在 Java 中,堆被劃分成兩個不同的區域:新生代 ( Young )、老年代 ( Old ),新生代默認占總空間的 1/3,老年代默認占 2/3,新生代有 3 個磁區:Eden、To Survivor、From Survivor,它們的默認占比是 8:1:1,
新生代的垃圾回收(又稱Minor GC)后只有少量物件存活,所以選用復制演算法,只需要少量的復制成本就可以完成回收,
老年代的垃圾回收(又稱Major GC)通常使用“標記-清理”或“標記-整理”演算法,

再描述它們之間轉化流程:
-
物件優先在Eden分配,當 eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC,
-
Eden 區再次 GC,這時會采用復制演算法,將 Eden 和 from 區一起清理,存活的物件會被復制到 to 區;
-
移動一次,物件年齡加 1,物件年齡大于一定閥值會直接移動到老年代,GC年齡的閥值可以通過引數 -XX:MaxTenuringThreshold 設定,默認為 15;
-
動態物件年齡判定:Survivor 區相同年齡所有物件大小的總和 > (Survivor 區記憶體大小 * 這個目標使用率)時,大于或等于該年齡的物件直接進入老年代,其中這個使用率通過 -XX:TargetSurvivorRatio 指定,默認為 50%;
-
Survivor 區記憶體不足會發生擔保分配,超過指定大小的物件可以直接進入老年代,
-
在 Eden 區執行了第一次 GC 之后,存活的物件會被移動到其中一個 Survivor 磁區;
-
大物件直接進入老年代,大物件就是需要大量連續記憶體空間的物件(比如:字串、陣列),為了避免為大物件分配記憶體時由于分配擔保機制帶來的復制而降低效率,
-
老年代滿了而 無法容納更多的物件 ,Minor GC 之后通常就會進行Full GC,Full GC 清理整個記憶體堆 – 包括年輕代和老年代 ,
15. Minor GC 和 Full GC 有什么不同呢?
Minor GC:只收集新生代的GC,
Full GC: 收集整個堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,換為metaspace 元空間)等所有部分的模式,
**Minor GC觸發條件:**當Eden區滿時,觸發Minor GC,
Full GC觸發條件:《求職面試筆試寶典》
-
通過Minor GC后進入老年代的平均大小大于老年代的可用記憶體,如果發現統計資料說之前Minor GC的平均晉升大小比目前old gen剩余的空間大,則不會觸發Minor GC而是轉為觸發full GC,
-
老年代空間不夠分配新的記憶體(或永久代空間不足,但只是JDK1.7有的,這也是用元空間來取代永久代的原因,可以減少Full GC的頻率,減少GC負擔,提升其效率),
-
由Eden區、From Space區向To Space區復制時,物件大小大于To Space可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小于該物件大小,
-
呼叫System.gc時,系統建議執行Full GC,但是不必然執行,
16. 介紹下空間分配擔保原則?
如果YougGC時新生代有大量物件存活下來,而 survivor 區放不下了,這時必須轉移到老年代中,但這時發現老年代也放不下這些物件了,那怎么處理呢?其實JVM有一個老年代空間分配擔保機制來保證物件能夠進入老年代,
在執行每次 YoungGC 之前,JVM會先檢查老年代最大可用連續空間是否大于新生代所有物件的總大小,因為在極端情況下,可能新生代 YoungGC 后,所有物件都存活下來了,而 survivor 區又放不下,那可能所有物件都要進入老年代了,這個時候如果老年代的可用連續空間是大于新生代所有物件的總大小的,那就可以放心進行 YoungGC,但如果老年代的記憶體大小是小于新生代物件總大小的,那就有可能老年代空間不夠放入新生代所有存活物件,這個時候JVM就會先檢查 -XX:HandlePromotionFailure 引數是否允許擔保失敗,如果允許,就會判斷老年代最大可用連續空間是否大于歷次晉升到老年代物件的平均大小,如果大于,將嘗試進行一次YoungGC,盡快這次YoungGC是有風險的,如果小于,或者 -XX:HandlePromotionFailure 引數不允許擔保失敗,這時就會進行一次 Full GC,
在允許擔保失敗并嘗試進行YoungGC后,可能會出現三種情況:
-
① YoungGC后,存活物件小于survivor大小,此時存活物件進入survivor區中
-
② YoungGC后,存活物件大于survivor大小,但是小于老年大可用空間大小,此時直接進入老年代,
-
③ YoungGC后,存活物件大于survivor大小,也大于老年大可用空間大小,老年代也放不下這些物件了,此時就會發生“Handle Promotion Failure”,就觸發了 Full GC,如果 Full GC后,老年代還是沒有足夠的空間,此時就會發生OOM記憶體溢位了,
通過下圖來了解空間分配擔保原則:

17. 什么是類加載?類加載的程序?
虛擬機把描述類的資料加載到記憶體里面,并對資料進行校驗、決議和初始化,最終變成可以被虛擬機直接使用的class物件;
類的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、決議(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段,其中準備、驗證、決議3個部分統稱為連接(Linking),如圖所示:

加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載程序必須按照這種順序按部就班地開始,而決議階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時系結(也稱為動態系結或晚期系結)
類加載程序如下:
-
加載,加載分為三步:1、通過類的全限定性類名獲取該類的二進制流;2、將該二進制流的靜態存盤結構轉為方法區的運行時資料結構;3、在堆中為該類生成一個class物件;
-
驗證:驗證該class檔案中的位元組流資訊復合虛擬機的要求,不會威脅到jvm的安全;
-
準備:為class物件的靜態變數分配記憶體,初始化其初始值;
-
決議:該階段主要完成符號參考轉化成直接參考;
-
初始化:到了初始化階段,才開始執行類中定義的java代碼;初始化階段是呼叫類構造器的程序;
18. 什么是類加載器,常見的類加載器有哪些?
類加載器是指:通過一個類的全限定性類名獲取該類的二進制位元組流叫做類加載器;類加載器分為以下四種:《求職面試筆試寶典》
-
啟動類加載器(BootStrapClassLoader):用來加載java核心類別庫,無法被java程式直接參考;
-
擴展類加載器(Extension ClassLoader):用來加載java的擴展庫,java的虛擬機實作會提供一個擴展庫目錄,該類加載器在擴展庫目錄里面查找并加載java類;
-
系統類加載器(AppClassLoader):它根據java的類路徑來加載類,一般來說,java應用的類都是通過它來加載的;
-
自定義類加載器:由java語言實作,繼承自ClassLoader;

19. 什么是雙親委派模型?為什么需要雙親委派模型?
當一個類加載器收到一個類加載的請求,他首先不會嘗試自己去加載,而是將這個請求委派給父類加載器去加載,只有父類加載器在自己的搜索范圍類查找不到給類時,子加載器才會嘗試自己去加載該類;
為了防止記憶體中出現多個相同的位元組碼;因為如果沒有雙親委派的話,用戶就可以自己定義一個java.lang.String類,那么就無法保證類的唯一性,
補充:那怎么打破雙親委派模型?
自定義類加載器,繼承ClassLoader類,重寫loadClass方法和findClass方法,
20. 列舉一些你知道的打破雙親委派機制的例子,為什么要打破?
-
JNDI 通過引入執行緒背景關系類加載器,可以在 Thread.setContextClassLoader 方法設定,默認是應用程式類加載器,來加載 SPI 的代碼,有了執行緒背景關系類加載器,就可以完成父類加載器請求子類加載器完成類加載的行為,打破的原因,是為了 JNDI 服務的類加載器是啟動器類加載,為了完成高級類加載器請求子類加載器(即上文中的執行緒背景關系加載器)加載類,
-
Tomcat,應用的類加載器優先自行加載應用目錄下的 class,并不是先委派給父加載器,加載不了才委派給父加載器,
tomcat之所以造了一堆自己的classloader,大致是出于下面三類目的:
tomcat類加載器如下圖:

webapp
class
lib
jvm
classloader
tomcat
-
熱部署,《求職面試筆試寶典》
-
OSGi,實作模塊化熱部署,為每個模塊都自定義了類加載器,需要更換模塊時,模塊與類加載器一起更換,其類加載的程序中,有平級的類加載器加載行為,打破的原因是為了實作模塊熱替換,
-
JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,當平臺及應用程式類加載器收到類加載請求,在委派給父加載器加載前,要先判斷該類是否能夠歸屬到某一個系統模塊中,如果可以找到這樣的歸屬關系,就要優先委派給負責那個模塊的加載器完成加載,打破的原因,是為了添加模塊化的特性,
21.說一下 JVM 調優的命令?
-
jps:JVM Process Status Tool,顯示指定系統內所有的HotSpot虛擬機行程,
-
jstat:jstat(JVM statistics Monitoring)是用于監視虛擬機運行時狀態資訊的命令,它可以顯示出虛擬機行程中的類裝載、記憶體、垃圾收集、JIT編譯等運行資料,
-
jmap:jmap(JVM Memory Map)命令用于生成heap dump檔案,如果不使用這個命令,還闊以使用-XX:+HeapDumpOnOutOfMemoryError引數來讓虛擬機出現OOM的時候·自動生成dump檔案,jmap不僅能生成dump檔案,還闊以查詢finalize執行佇列、Java堆和永久代的詳細資訊,如當前使用率、當前使用的是哪種收集器等,
-
jhat:jhat(JVM Heap Analysis Tool)命令是與jmap搭配使用,用來分析jmap生成的dump,jhat內置了一個微型的HTTP/HTML服務器,生成dump的分析結果后,可以在瀏覽器中查看,在此要注意,一般不會直接在服務器上進行分析,因為jhat是一個耗時并且耗費硬體資源的程序,一般把服務器生成的dump檔案復制到本地或其他機器上進行分析,
-
jstack:jstack用于生成java虛擬機當前時刻的執行緒快照,jstack來查看各個執行緒的呼叫堆疊,就可以知道沒有回應的執行緒到底在后臺做什么事情,或者等待什么資源,如果java程式崩潰生成core檔案,jstack工具可以用來獲得core檔案的java stack和native stack的資訊,從而可以輕松地知道java程式是如何崩潰和在程式何處發生問題,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/296570.html
標籤:java
上一篇:Java工程師的進階之路
下一篇:開發第一個Java專案—五子棋
