【死磕JVM】這可能是最全的JVM面試題了
1. 描述一下jvm內存模型

2.堆內存劃分的空間
垃圾回收算法: 標記清除、復制(多為新生代垃圾回收使用)、標記整理
3.如何解決線上gc頻繁的問題?
- 查看監控,以了解出現問題的時間點以及當前FGC的頻率(可對比正常情況看頻率是否正常)
- 了解該時間點之前有沒有程序上線、基礎組件升級等情況。
- 了解JVM的參數設置,包括:堆空間各個區域的大小設置,新生代和老年代分別采用了哪些垃 圾收集器,然后分析JVM參數設置是否合理。
- 再對步驟1中列出的可能原因做排除法,其中元空間被打滿、內存泄漏、代碼顯式調用gc方法 比較容易排查。
- 針對大對象或者長生命周期對象導致的FGC,可通過 jmap -histo 命令并結合dump堆內存文件作進一步分析,需要先定位到可疑對象。
- 通過可疑對象定位到具體代碼再次分析,這時候要結合GC原理和JVM參數設置,弄清楚可疑 對象是否滿足了進入到老年代的條件才能下結論。
4.描述一下class初始化過程?
一個類初始化就是執行clinit()方法,過程如下:
- 父類初始化
- static變量初始化/static塊(按照文本順序執行)
Java Language Specification中,類初始化詳細過程如下(最重要的是類初始化是線程安全的):
- 每個類都有一個初始化鎖LC,進程獲取LC(如果沒有獲取到,就一直等待)
- 如果C正在被其他線程初始化,釋放LC并等待C初始化完成
- 如果C正在被本線程初始化,即遞歸初始化,釋放LC
- 如果C已經被初始化了,釋放LC
- 如果C處于erroneous狀態,釋放LC并拋出異常NoClassDefFoundError
- 否則,將C標記為正在被本線程初始化,釋放LC;然后, 初始化那些final且為基礎類型的類成員變量
- 初始化C的父類SC和各個接口SI_n(按照implements子句中的順序來) ;如果SC或SIn初始化過程中拋出異常,則獲取LC,將C標記為erroneous,并通知所有線程,然后釋放LC,然后 再拋出同樣的異常。
- 從classloader處獲取assertion是否被打開
- 接下來, 按照文本順序執行類變量初始化和靜態代碼塊,或接口的字段初始化,把它們當作是一個個單獨的代碼塊。
- 如果執行正常,獲取LC,標記C為已初始化,并通知所有線程,然后釋放LC
- 否則,如果拋出了異常E。若E不是Error,則以E為參數創建新的異常ExceptionInInitializerError作為E。如果因為OutOfMemoryError導致無法創建ExceptionInInitializerError,則將OutOfMemoryError作為E。
- 獲取LC,將C標記為erroneous,通知所有等待的線程,釋放LC,并拋出異常E
5.簡述一下內存溢出的原因,如何排查線上問題?
內存溢出的原因
- java.lang.OutOfMemoryError: ......java heap space. 堆棧溢出,代碼問題的可能性極大
- java.lang.OutOfMemoryError: GC over head limit exceeded 系統處于高頻的GC狀態,而且回收的效果依然不佳的情況,就會開始報這個錯誤,這種情況一般是產生了很多不可以被釋放 的對象,有可能是引用使用不當導致,或申請大對象導致,但是java heap space的內存溢出有可能提前不會報這個錯誤,也就是可能內存就直接不夠導致,而不是高頻GC.
- java.lang.OutOfMemoryError: PermGen space jdk1.7之前才會出現的問題 ,原因是系統的代碼非常多或引用的第三方包非常多、或代碼中使用了大量的常量、或通過intern注入常量、 或者通過動態代碼加載等方法,導致常量池的膨脹
- java.lang.OutOfMemoryError: Direct buffer memory 直接內存不足,因為jvm垃圾回收不會回收掉直接內存這部分的內存,所以可能原因是直接或間接使用了ByteBuffer中的allocateDirect方法的時候,而沒有做clear
- java.lang.StackOverflowError - Xss設置的太小了
- java.lang.OutOfMemoryError: unable to create new native thread 堆外內存不足,無法為線程分配內存區域
- java.lang.OutOfMemoryError: request {} byte for {}out of swap 地址空間不夠
6.jvm有哪些垃圾回收器,實際中如何選擇?
圖中展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,則說明它們可以搭配使用。虛 擬機所處的區域則表示它是屬于新生代還是老年代收集器。
新生代收集器(全部的都是復制算法):Serial、ParNew、Parallel Scavenge
老年代收集器:CMS(標記-清理)、Serial Old(標記-整理)、Parallel Old(標記整理) 整堆收集器:G1(一個Region中是標記-清除算法,2個Region之間是復制算法)
同時,先解釋幾個名詞:
- 并行(Parallel):多個垃圾收集線程并行工作,此時用戶線程處于等待狀態
- 并發(Concurrent):用戶線程和垃圾收集線程同時執行
- 吞吐量:運行用戶代碼時間/(運行用戶代碼時間+垃圾回收時間)
1.Serial收集器是最基本的、發展歷史最悠久的收集器。
特點: 單線程、簡單高效(與其他收集器的單線程相比),對于限定單個CPU的環境來說,Serial收集器 由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程手機效率。收集器進行垃圾回收 時,必須暫停其他所有的工作線程,直到它結束(Stop The World)。
應用場景: 適用于Client模式下的虛擬機。
Serial / Serial Old收集器運行示意圖:
2.ParNew收集器其實就是Serial收集器的多線程版本。
除了使用多線程外其余行為均和Serial收集器一模一樣(參數控制、收集算法、Stop The World、對象分配規則、回收策略等)。
特點: 多線程、ParNew收集器默認開啟的收集線程數與CPU的數量相同,在CPU非常多的環境中,可以 使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
和Serial收集器一樣存在Stop The World問題
應用場景: ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器,因為它是除了 Serial收集器外,唯一一個能與CMS收集器配合工作的。
ParNew/Serial Old組合收集器運行示意圖如下:
3.Parallel Scavenge 收集器與吞吐量關系密切,故也稱為吞吐量優先收集器。
特點: 屬于新生代收集器也是采用復制算法的收集器,又是并行的多線程收集器(與ParNew收集器類 似)。該收集器的目標是達到一個可控制的吞吐量。還有一個值得關注的點是:GC自適應調節策略(與 ParNew收集器最重要的一個區別)
GC自適應調節策略: Parallel Scavenge收集器可設置-XX:+UseAdptiveSizePolicy參數。當開關打開時不需要手動指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代 的對象年齡(-XX:PretenureSizeThreshold)等,虛擬機會根據系統的運行狀況收集性能監控信息,動 態設置這些參數以提供最優的停頓時間和最高的吞吐量,這種調節方式稱為GC的自適應調節策略。
Parallel Scavenge收集器使用兩個參數控制吞吐量:XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時間 XX:GCRatio 直接設置吞吐量的大小。
4.Serial Old是Serial收集器的老年代版本。
特點: 同樣是單線程收集器,采用標記-整理算法。
應用場景: 主要也是使用在Client模式下的虛擬機中。也可在Server模式下使用。Server模式下主要的兩大用途(在后續中詳細講解···):
在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用。
作為CMS收集器的后備方案,在并發收集Concurent Mode Failure時使用。
Serial / Serial Old收集器工作過程圖(Serial收集器圖示相同):
5.Parallel Old是Parallel Scavenge收集器的老年代版本。
特點: 多線程,采用標記-整理算法。
應用場景: 注重高吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old 收集器。
6.CMS收集器是一種以獲取最短回收停頓時間為目標的收集器。
特點: 基于標記-清除算法實現。并發收集、低停頓。
應用場景: 適用于注重服務的響應速度,希望系統停頓時間最短,給用戶帶來更好的體驗等場景下。如web程序、b/s服務。
CMS收集器的運行過程分為下列4步:
初始標記: 標記GC Roots能直接到的對象。速度很快但是仍存在Stop The World問題。
并發標記: 進行GC Roots Tracing 的過程,找出存活對象且用戶線程可并發執行。
重新標記: 為了修正并發標記期間因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記 錄。仍然存在Stop The World問題。
并發清除: 對標記的對象進行清除回收。CMS收集器的內存回收過程是與用戶線程一起并發執行的。
CMS收集器的工作過程圖:
CMS收集器的缺點:
- 對CPU資源非常敏感。
- 無法處理浮動垃圾,可能出現Concurrent Model Failure失敗而導致另一次Full GC的產生。
- 因為采用標記-清除算法所以會存在空間碎片的問題,導致大對象無法分配空間,不得不提前觸發 一次Full GC。
7.G1收集器一款面向服務端應用的垃圾收集器。
特點如下:
并行與并發:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短Stop-The-World停頓時間。部分收集器原本需要停頓Java線程來執行GC動作,G1收集器仍然可以通過并發的方式讓Java程序繼續運行。
分代收集:G1能夠獨自管理整個Java堆,并且采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。
空間整合:G1運作期間不會產生空間碎片,收集后能提供規整的可用內存。
可預測的停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型。能讓使用者明確指定在一個長度為M毫秒的時間段內,消耗在垃圾收集上的時間不得超過N毫秒。
G1收集器運行示意圖:
關于gc的選擇除非應用程序有非常嚴格的暫停時間要求,否則請先運行應用程序并允許VM選擇收集器(如果沒有特別要求。使用VM提供給的默認GC就好)。
如有必要,調整堆大小以提高性能。如果性能仍然不能滿足目標,請使用以下準則作為選擇收集器的起點:
- 如果應用程序的數據集較小(最大約100 MB),則選擇帶有選項-XX:+ UseSerialGC的串行收集器。
- 如果應用程序將在單個處理器上運行,并且沒有暫停時間要求,則選擇帶有選項-XX:+UseSerialGC的串行收集器
- 如果(a)峰值應用程序性能是第一要務,并且(b)沒有暫停時間要求或可接受一秒或更長時間的暫停,則讓VM選擇收集器或使用-XX:+ UseParallelGC選擇并行收集器 。
- 如果響應時間比整體吞吐量更重要,并且垃圾收集暫停時間必須保持在大約一秒鐘以內,則選擇具有-XX:+ UseG1GC。(值得注意的是JDK9中CMS已經被Deprecated,不可使用!移除該選項)
- 如果使用的是jdk8,并且堆內存達到了16G,那么推薦使用G1收集器,來控制每次垃圾收集的時間。
- 如果響應時間是高優先級,或使用的堆非常大,請使用-XX:UseZGC選擇完全并發的收集器。(值得注意的是JDK11開始可以啟動ZGC,但是此時ZGC具有實驗性質,在JDK15中
- [202009發布]才取消實驗性質的標簽,可以直接顯示啟用,但是JDK15默認GC仍然是G1)
這些準則僅提供選擇收集器的起點,因為性能取決于堆的大小,應用程序維護的實時數據量以及可用處理器的數量和速度。如果推薦的收集器沒有達到所需的性能,則首先嘗試調整堆和新生代大小以達到所需的目標。如果性能仍然不足,嘗試使用其他收集器總體原則:減少STOP THE WORD時間,使用并發收集器(比如CMS+ParNew,G1)來減少暫停時間,加快響應時間,并使用并行收集器來增加多處理器硬件上的總體吞吐量。
7. 簡述一下Java類加載模型?
雙親委派模型在某個類加載器加載class文件時,它首先委托父加載器去加載這個類,依次傳遞到頂層類加載器(Bootstrap)。如果頂層加載不了(它的搜索范圍中找不到此類),子加載器才會嘗試加載這個類。雙親委派的好處
- 每一個類都只會被加載一次,避免了重復加載
- 每一個類都會被盡可能的加載(從引導類加載器往下,每個加載器都可能會根據優先次序嘗試加載它)
- 有效避免了某些惡意類的加載(比如自定義了Java.lang.Object類,一般而言在雙親委派模型下會加載系統的Object類而不是自定義的Object類)
8. JVM8為什么要增加元空間,帶來什么好處?
原因:
- 字符串存在永久代中,容易出現性能問題和內存溢出。
- 類及方法的信息等比較難確定其大小,因此對于永久代的大小指定比較困難,太小容易出現永久代溢 出,太大則容易導致老年代溢出。
- 永久代會為 GC 帶來不必要的復雜度,并且回收效率偏低。
元空間的特點:
- 每個加載器有專門的存儲空間。
- 不會單獨回收某個類。
- 元空間里的對象的位置是固定的。
- 如果發現某個加載器不再存貨了,會把相關的空間整個回收。
9. 堆G1垃圾收集器有了解么,有什么特點
G1的特點:
- G1的設計原則是"首先收集盡可能多的垃圾(Garbage First)"。因此,G1并不會等內存耗盡(串行、并行)或者快耗盡(CMS)的時候開始垃圾收集,而是在內部采用了啟發式算法,在老年代找出具有高收集收益的分區進行收集。同時G1可以根據用戶設置的暫停時間目標自動調整年輕代和總堆大小,暫停目標越短年輕代空間越小、總空間就越大;
- G1采用內存分區(Region)的思路,將內存劃分為一個個相等大小的內存分區,回收時則以分區為單位進行回收,存活的對象復制到另一個空閑分區中。由于都是以相等大小的分區為單位進行操作,因此G1天然就是一種壓縮方案(局部壓縮);
- G1雖然也是分代收集器,但整個內存分區不存在物理上的年輕代與老年代的區別,也不需要完全獨立的survivor(to space)堆做復制準備。G1只有邏輯上的分代概念,或者說每個分區都可能隨G1的運行在不同代之間前后切換;
- G1的收集都是STW的,但年輕代和老年代的收集界限比較模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年輕代分區(年輕代收集),也可能在收集年輕代的同時,包含部分老年代分區(混合收集),這樣即使堆內存很大時,也可以限制收集范圍,從而降低停頓。
- 因為G1建立可預測的停頓時間模型,所以每一次的垃圾回收時間都可控,那么對于大堆(16G左右)的垃圾收集會有明顯優勢
10. 介紹一下垃圾回收算法?
標記-清除
缺點: 產生內存碎片,如上圖,如果清理了兩個1kb的對象,再添加一個2kb的對象,無法放入這兩個位置
標記-整理(老年代)
缺點:移動對象開銷較大
復制(新生代)
11. Happens-Before規則?
先行發生原則(Happens-Before)是判斷數據是否存在競爭、線程是否安全的主要依據。先行發生是Java內存,模型中定義的兩項操作之間的偏序關系,如果操作A先行發生于操作B,那么操作A產生的影響能夠被操作B觀察到。
口訣:如果兩個操作之間具有happen-before關系,那么前一個操作的結果就會對后面的一個操作可見。是Java內存模型中定義的兩個操作之間的偏序關系。
常見的happen-before規則:
1.程序順序規則:一個線程中的每個操作,happen-before在該線程中的任意后續操作。(注解:如果只有一個線程的操作,那么前一個操作的結果肯定會對后續的操作可見。)程序順序規則中所說的每個操作happen-before于該線程中的任意后續操作并不是說前一個操作必須要在后一個操作之前執行,而是指前一個操作的執行結果必須對后一個操作可見,如果不滿足這個要求那就不允許這兩個操作進行重排序
2.鎖規則:對一個鎖的解鎖,happen-before在隨后對這個鎖加鎖。(注解:這個最常見的就是synchronized方法和syncronized塊)
3.volatile變量規則:對一個volatile域的寫,happen-before在任意后續對這個volatile域的讀。該規則在CurrentHashMap的讀操作中不需要加鎖有很好的體現。
4.傳遞性:如果A happen-before B,且B happen-before C,那么A happen - before C.
5.線程啟動規則:Thread對象的start()方法happen-before此線程的每一個動作。
6.線程終止規則:線程的所有操作都happen-before對此線程的終止檢測,可以通過Thread.join()方法結束,Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
7.線程中斷規則:對線程interrupt()方法的調用happen-before發生于被中斷線程的代碼檢測到中斷時事件的發生。
12. 描述一下java類加載和初始化的過程?
JAVA類的加載機制:Java類加載分為5個過程,分別為:加載,鏈接(驗證,準備,解析),初始化,使用,卸載。
加載:加載主要是將.class文件通過二進制字節流讀入到JVM中。在加載階段,JVM需要完成3件事:1)通過classloader在classpath中獲取XXX.class文件,將其以二進制流的形式讀入內存。2)將字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構;3)在內存中生成一個該類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。
鏈接
2.1. 驗證 主要確保加載進來的字節流符合JVM規范。驗證階段會完成以下4個階段的檢驗動作:
1)文件格式驗證
2)元數據驗證(是否符合Java語言規范)
3)字節碼驗證(確定程序語義合法,符合邏輯)
4)符號引用驗證(確保下一步的解析能正常執行
2.2. 準備 準備是連接階段的第二步,主要為靜態變量在方法區分配內存,并設置默認初始值。
2.3. 解析 解析是連接階段的第三步,是虛擬機將常量池內的符號引用替換為直接引用的過程。
初始化 初始化階段是類加載過程的最后一步,主要是根據程序中的賦值語句主動為類變量賦值。當有繼承關系時,先初始化父類再初始化子類,所以創建一個子類時其實內存中存在兩個對象實 例。
使用 程序之間的相互調用。
卸載 即銷毀一個對象,一般情況下中有JVM垃圾回收器完成。代碼層面的銷毀只是將引用置為null。
13. 吞吐量優先和響應時間優先的回收器是哪些?
- 吞吐量優先:Parallel Scavenge+Parallel Old(多線程并行)
- 響應時間優先:cms+par new(并發回收垃圾)
14. 什么叫做阻塞隊列的有界和無界,實際中有用過嗎?
- ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列,線程池,生產者消費者
- LinkedBlockingQueue:一個由鏈表結構組成的無界阻塞隊列,線程池,生產者消費者
- PriorityBlockingQueue:一個支持優先級排序的無界阻塞隊列,可以實現精確的定時任務
- DelayQueue:一個使用優先級隊列實現的無界阻塞隊列,可以實現精確的定時任務
- SynchronousQueue:一個不存儲元素的阻塞隊列,線程池
- LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列
- LinkedBlockingDeque:一個由鏈表結構組成的雙向無界阻塞隊列,可以用在“工作竊取”模式 中
15. jvm監控系統是通過jmx做的么?
一般都是,但是要是記錄比較詳細的性能定位指標,都會導致進入 safepoint,從而降低了線上應用性能例如 jstack,jmap打印堆棧,打印內存使用情況,都會讓 jvm 進入safepoint,才能獲取線程穩定狀態從而采集信息。同時,JMX暴露向外的接口采集信息,例如使用jvisualvm,還會涉及rpc和網絡消耗,以及JVM忙時,無法采集到信息從而有指標斷點。這些都是基于 JMX 的外部監控很難解決的問題。所以,推薦使用JVM內部采集 JFR,這樣即使在JVM很忙時,也能采集到有用的信息
16. 內存屏障的匯編指令是啥?
- 硬件內存屏障 X86
- sfence: store| 在sfence指令前的寫操作當必須在sfence指令后的寫操作前完成。
- lfence: load | 在lfence指令前的讀操作當必須在lfence指令后的讀操作前完成。
- mfence: modify/mix | 在mfence指令前的讀寫操作當必須在mfence指令后的讀寫操作前完成。2.原子指令,如x86上的”lock …” 指令是一個Full Barrier,執行時會鎖住內存子系統來確保執行順序,甚至跨多個CPU。Software Locks通常使用了內存屏障或原子指令來實現變量可見性和保持程序順序。3.JVM級別如何規范(JSR133)
LoadLoad屏障:對于這樣的語句Load1; LoadLoad; Load2, 在Load2及后續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2, 在Store2及后續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2, 在Store2及后續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。
StoreLoad屏障:對于這樣的語句Store1; StoreLoad; Load2, 在Load2及后續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。
本文轉載自微信公眾號「 牧小農」,可以通過以下二維碼關注。轉載本文請聯系 牧小農公眾號。