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類)
9. JVM8為什么要增加元空間,帶來什么好處?
原因:
- 字串存在永久代中,容易出現性能問題和記憶體溢位,
- 類及方法的資訊等比較難確定其大小,因此對于永久代的大小指定比較困難,太小容易出現永久代溢
出,太大則容易導致老年代溢位, - 永久代會為 GC 帶來不必要的復雜度,并且回收效率偏低,
元空間的特點:
- 每個加載器有專門的存盤空間,
- 不會單獨回收某個類,
- 元空間里的物件的位置是固定的,
- 如果發現某個加載器不再存貨了,會把相關的空間整個回收,
10. 堆G1垃圾收集器有了解么,有什么特點
G1的特點:
- G1的設計原則是"首先收集盡可能多的垃圾(Garbage First)",因此,G1并不會等記憶體耗盡(串行、并行)或者快耗盡(CMS)的時候開始垃圾收集,而是在內部采用了啟發式演算法,在老年代找出具有高收集收益的磁區進行收集,同時G1可以根據用戶設定的暫停時間目標自動調整年輕代和總堆大小,暫停目標越短年輕代空間越小、總空間就越大;
- G1采用記憶體磁區(Region)的思路,將記憶體劃分為一個個相等大小的記憶體磁區,回收時則以磁區為單位進行回收,存活的物件復制到另一個空閑磁區中,由于都是以相等大小的磁區為單位進行操作,因此G1天然就是一種壓縮方案(區域壓縮);
- G1雖然也是分代收集器,但整個記憶體磁區不存在物理上的年輕代與老年代的區別,也不需要完全獨立的survivor(to space)堆做復制準備,G1只有邏輯上的分代概念,或者說每個磁區都可能隨G1的運行在不同代之間前后切換;
- G1的收集都是STW的,但年輕代和老年代的收集界限比較模糊,采用了混合(mixed)收集的方式,即每次收集既可能只收集年輕代磁區(年輕代收集),也可能在收集年輕代的同時,包含部分老年代磁區(混合收集),這樣即使堆記憶體很大時,也可以限制收集范圍,從而降低停頓,
- 因為G1建立可預測的停頓時間模型,所以每一次的垃圾回收時間都可控,那么對于大堆(16G左右)的垃圾收集會有明顯優勢
11. 介紹一下垃圾回收演算法?
標記-清除

缺點: 產生記憶體碎片,如上圖,如果清理了兩個1kb的物件,再添加一個2kb的物件,無法放入這兩個位置
標記-整理(老年代)

缺點:移動物件開銷較大
復制(新生代)

12. 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發生于被中斷執行緒的代碼檢測到中斷時事件的發生,
13. 描述一下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,
15. 吞吐量優先和回應時間優先的回收器是哪些?
- 吞吐量優先:Parallel Scavenge+Parallel Old(多執行緒并行)
- 回應時間優先:cms+par new(并發回收垃圾)
16. 什么叫做阻塞佇列的有界和無界,實際中有用過嗎?
- ArrayBlockingQueue:一個由陣列結構組成的有界阻塞佇列,執行緒池,生產者消費者
- LinkedBlockingQueue:一個由鏈表結構組成的無界阻塞佇列,執行緒池,生產者消費者
- PriorityBlockingQueue:一個支持優先級排序的無界阻塞佇列,可以實作精確的定時任務
- DelayQueue:一個使用優先級佇列實作的無界阻塞佇列,可以實作精確的定時任務
- SynchronousQueue:一個不存盤元素的阻塞佇列,執行緒池
- LinkedTransferQueue:一個由鏈表結構組成的無界阻塞佇列
- LinkedBlockingDeque:一個由鏈表結構組成的雙向無界阻塞佇列,可以用在“作業竊取”模式
中
17. jvm監控系統是通過jmx做的么?
一般都是,但是要是記錄比較詳細的性能定位指標,都會導致進入 safepoint,從而降低了線上應用性能
例如 jstack,jmap列印堆疊,列印記憶體使用情況,都會讓 jvm 進入safepoint,才能獲取執行緒穩定狀態從而采集資訊,
同時,JMX暴露向外的介面采集資訊,例如使用jvisualvm,還會涉及rpc和網路消耗,以及JVM忙時,無法采集到資訊從而有指標斷點,這些都是基于 JMX 的外部監控很難解決的問題,所以,推薦使用JVM內部采集 JFR,這樣即使在JVM很忙時,也能采集到有用的資訊
18. 記憶體屏障的匯編指令是啥?
1.硬體記憶體屏障 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的寫入對所有處理器可見,
我是牧小農,怕什么真理無窮,進一步有進一步的歡喜,大家加油!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/280736.html
標籤:其他
上一篇:你居然不會狄杰斯特演算法?驚了!
