JAVA面試——JVM知識
1、什麼情況下會發生棧記憶體溢位。 【1】、執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常。遞迴的呼叫一個簡單的方法,不斷累積就會丟擲StackOverflowError異常。 【2】、如果虛擬機器在動態擴充套件棧時無法申請到足夠的記憶體空間,則丟擲OutOfMemoryError異常。無限迴圈的建立執行緒,並對每個執行緒增加記憶體。則會丟擲OutOfMemoryError異常。 注意:在多執行緒的情況下,給每個執行緒的棧分配的記憶體越大,越容易產生記憶體溢位異常。作業系統為每個程序分配的記憶體是有限制的,虛擬機器提供了引數來控制Java堆和方法區這兩部分共享記憶體的最大值,忽略程式計數器的記憶體消耗(很小),以及程序本身消耗的記憶體,剩下的記憶體便給了虛擬機器棧和本地方法棧。每個執行緒分配到的棧容量越大,可以建立的執行緒數量自然就越少。因此,如果是建立過多的執行緒導致的記憶體溢位,在不能減少執行緒數的情況下,就只能通過減少最大堆和每個執行緒的棧容量來換取更多的執行緒。結合下圖理解學習:
2、JVM的記憶體結構,Eden和Survivor比例。 JVM記憶體結構主要由三部分組成:堆空間、方法區和棧。堆記憶體是JVM虛擬機器中最大的一部分,它由年輕代和老年代組成。而年輕代有被分為三部分,Eden空間、From Survivor空間和To Survivor空間。預設情況下年輕代按照8:1:1的比例分配。方法區儲存類資訊、常量、靜態變數等資料,是執行緒共享的區域。為了與Java堆區分,方法區有一個別名Non-Heap(非堆)。棧分為Java虛擬機器棧和本地方法棧主要用於方法的執行。
3、JVM記憶體為什麼要分成新生代,老年代,持久代。新生代中為什麼要分為Eden和Survivor。
4、JVM中一次完整的GC流程是怎樣的,說說你知道的幾種主要的JVM引數。 Java GC機制主要完成3件事:確定哪些記憶體需要回收,確定什麼時候需要執行GC,如何執行GC。
-Xms | 初始堆大小 |
-Xmx | 最大堆大小 |
-Xmn | 最小堆大小 |
-XX:newSize | 新生代初始值 |
-XX:newRatio |
3表示:年輕代:老年代=1:3 |
-XX:permSize | 持久代初始值 |
-XX:survivorRatio |
Eden與Survivor的比例。 8表示:Eden:Survivor = 8 : 2(Form:To=1:1) |
5、你知道哪幾種垃圾收集器,各自的優缺點,重點講下cms和G1,包括原理,流程,優缺點。 目前比較常用的垃圾收集器和它們直接搭配使用的情況如下,上面是新生代收集器,下面則是老年代收集器,根據不同業務場景進行選取。 ● Serial收集器:是一個新生代收集器,單執行緒執行,使用複製演算法。它在進行垃圾收集時,必須暫停其他所有的工作執行緒(使用者執行緒)也就是Stop The World。是JVM Client模式下預設的新生代收集器。對於限定單個CPU的環境來說,Serial收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。 ● ParNew收集器:可看作Serial收集器的多執行緒版本,除使用多條執行緒進行垃圾收集之外,其餘行為與Serial收集器一樣。 使用方式可以使用-XX:+UseConcMarkSweepGC,或者是使用-XX:+UseParNewGC來強制開啟,可以通過-XX:ParallelGCThreads 來調整或者限制垃圾收集的執行緒數量。 ● Parallel Scavenge收集器:也是新生代收集器,也是使用複製演算法的收集器,又是並行多執行緒收集器。Parallel Scavenge收集器的特點是它關注的點與其他收集器不同,CMS等收集器的關注點是儘可能地縮短垃圾收集的時間,從而降低使用者執行緒的停頓時間,而parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。吞吐量= 程式執行時間/(程式執行時間 + 垃圾收集時間),虛擬機器總共運行了100分鐘。其中垃圾收集花掉1分鐘,那吞吐量就是99%。 Parallel Scavenge提供了兩個引數用來精確控制,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis引數以及直接設定吞吐量大小的-XX:GCTimeRatio引數。 MaxGCPauseMillis引數允許的值是一個大於0的毫秒數,收集器將盡可能地保證記憶體回收花費的時間不超過設定值。不過大家不要認為如果把這個引數的值設定得稍小一點就能使得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300MB新生代肯定比收集500MB快吧,這也直接導致垃圾收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,現在變成5秒收集一次、每次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了。 GCTimeRatio引數的值應當是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,相當於是吞吐量的倒數。 Parallel Scavenge收集器也經常稱為“吞吐量優先”收集器。除上述兩個引數之外,Parallel Scavenge收集器還有一個引數-XX:+UseAdaptiveSizePolicy值得關注。這是一個開關引數,當這個引數開啟之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代物件年齡(-XX:PretenureSizeThreshold)等細節引數了,虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略(GC Ergonomics) ● Serial Old:是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用“標記-整理”演算法。這個收集器的主要意義也是在於給Client模式下的虛擬機器使用。如果在Server模式下,那麼它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。這兩點都將在後面的內容中詳細講解。 ● Parallel Old是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。這個收集器是在JDK1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外別無選擇(還記得上面說過Parallel Scavenge收集器無法與CMS收集器配合工作嗎?)。由於老年代Serial Old收集器在服務端應用效能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果,由於單執行緒的老年代收集中無法充分利用伺服器多CPU的處理能力,在老年代很大而且硬體比較高階的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”。 直到Parallel Old收集器出現後,“吞吐量優先”收集器終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。 ● CMS(Concurrent Mark Sweep)收集器:是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS收集器就非常符合這類應用的需求。 從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基於“標記—清除”演算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些,整個過程分為4個步驟,包括:① 初始標記(CMS initial mark)② 併發標記(CMS concurrent mark)③ 重新標記(CMS remark)④ 併發清除(CMS concurrent sweep)其中,初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是為了修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。 CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。由於CMS併發清理階段使用者執行緒還在執行著,伴隨程式執行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。也是由於在垃圾收集階段使用者執行緒還需要執行,那也就還需要預留有足夠的記憶體空間給使用者執行緒使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程式運作使用。在JDK 1.5的預設設定下,CMS收集器當老年代使用了68%的空間後就會被啟用,這是一個偏保守的設定,如果在應用中老年代增長不是太快,可以適當調高參數-XX:CMSInitiatingOccupancyFraction的值來提高觸發百分比,以便降低記憶體回收次數從而獲取更好的效能,在JDK 1.6中,CMS收集器的啟動閾值已經提升至92%。要是CMS執行期間預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機器將啟動後備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說引數-XX:CMSInitiatingOccupancyFraction設定得太高很容易導致大量“Concurrent Mode Failure”失敗,效能反而降低。 CMS是一款基於“標記—清除”演算法實現的收集器,如果讀者對前面這種演算法介紹還有印象的話,就可能想到這意味著收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大物件分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前物件,不得不提前觸發一次Full GC。為了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關引數(預設就是開啟的),用於在CMS收集器頂不住要進行FullGC時開啟記憶體碎片的合併整理過程,記憶體整理的過程是無法併發的,空間碎片問題沒有了,但停頓時間不得不變長。虛擬機器設計者還提供了另外一個引數-XX:CMSFullGCsBeforeCompaction,這個引數是用於設定執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的(預設值為0,表示每次進入Full GC時都進行碎片整理)。 ● G1收集器:是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是(在比較長期的)未來可以替換掉JDK 1.5中釋出的CMS收集器。與其他GC收集器相比,G1具備如下特點。 ☞ 並行與併發:G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java執行緒執行的GC動作,G1收集器仍然可以通過併發的方式讓Java程式繼續執行。 ☞ 分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間、熬過多次GC的舊物件以獲取更好的收集效果。 ☞ 空間整合:與CMS的“標記—清理”演算法不同,G1從整體來看是基於“標記—整理”演算法實現的收集器,從區域性(兩個Region之間)上來看是基於“複製”演算法實現的,但無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片,收集後能提供規整的可用記憶體。這種特性有利於程式長時間執行,分配大物件時不會因為無法找到連續記憶體空間而提前觸發下一次GC。 ☞ 可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特徵了。 在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。
6、垃圾回收演算法的實現原理。 1)、標記-清除:是現代垃圾回收演算法的基本思想。標記-清除將垃圾回收分為兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段,首先通過根節點,標記所有從根節點開始可達的物件。因此,未標記的物件就是未被引用的物件。然後,在清除階段,清除所有未被標記的物件。 2)、標記-壓縮:標記-壓縮演算法適合用於存活物件較多的場合,如老年代。在標記-清除演算法的基礎上做了一些優化。和標記-清除演算法一樣,標記-壓縮演算法也首先需要從根節點開始,對所有可達物件做一次標記。但之後,並不是簡單的清理未標記的物件,而是將所有的存活物件壓縮到記憶體的另一端,之後,清理邊界外所有的物件。
3)、複製演算法:①、與標記演算法相比,複製演算法是一種相對高效的回收方法。 ②、不適用存活物件較多的場合,比如老年代。 ③、將原有的記憶體空間分為兩塊,每次只是用一塊,在垃圾回收時,將正在適用的記憶體存活物件複製一份到未使用的記憶體中,之後,清除正在適用的記憶體塊中的所有物件,交換兩個記憶體的角色,完成垃圾回收。
8、JVM記憶體模型的相關知識瞭解多少,比如重排序,記憶體屏障,happen-before,主記憶體,工作記憶體等。 ♣ 重排序:在執行程式時,為了提高效能,編譯器和處理器會對指令做重排序。(譬如:a=1;b=2;重排後b=2;a=1;)。 ◀ 編譯器優化重排序:編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。 ◀ 指令級並行的重排序:如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。 ◀ 記憶體系統的重排序:處理器使用快取和讀寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。 ♣ 記憶體屏障(Memory Barrier):又稱記憶體柵欄,為了保障執行順序和可見性的一條cpu指令。 ◀ 保障執行順序:編譯器和CPU能夠重排序指令,保證最終相同的結果,嘗試優化效能。插入一條Memory Barrier會告訴編譯器和CPU:不管什麼指令都不能和這條Memory Barrier指令重排序。 ◀ Memory Barrier保證可見性是指:強制刷出各種CPU cache,如一個 Write-Barrier(寫入屏障)將刷出所有在 Barrier 之前寫入cache 的資料,因此,任何CPU上的執行緒都能讀取到這些資料的最新版本。例如:Volatile就是基於Memory Barrier實現的。 ♣ happen-before:操作之間的記憶體可見性。在JMM中,如果一個操作的執行結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係,這個的兩個操作既可以在同一個執行緒,也可以在不同的兩個執行緒中。 ◀ 程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中任意的後續操作。 ◀ 監視器鎖規則:對一個鎖的解鎖操作,happens-before於隨後對這個鎖的加鎖操作。 ◀ volatile域規則:對一個volatile域的寫操作,happens-before於任意執行緒後續對這個volatile域的讀。 ◀ 傳遞性規則:如果 A happens-before B,且 B happens-before C,那麼A happens-before C。 ♣ 主記憶體:共享變數儲存的區域即是主記憶體。 ♣ 工作記憶體:每個執行緒copy的本地記憶體,儲存了該執行緒以讀/寫共享變數的副本。
9、簡單說說你瞭解的類載入器,可以打破雙親委派麼,怎麼打破。 ☞ 類的載入過程:JVM將類載入過程分為三個步驟:裝載(Load),連結(Link)和初始化(Initialize)連結又分為三個步驟: ● 裝載:查詢並載入類的二進位制資料; ● 連結:驗證:確保被載入類的正確性; 準備:為類的靜態變數分配記憶體,並將其初始化為預設值; 解析:把類中的符號引用轉換為直接引用; ● 初始化:為類的靜態變數賦予正確的初始值; ☞ JVM的類載入是通過ClassLoader及其子類來完成的,類的層次關係和載入順序可以由下圖來描述:
☞ 打破雙親委派機制則不僅要繼承ClassLoader類,還要重寫loadClass和findClass方法。預設的loadClass方法是實現了雙親委派機制的邏輯,即會先讓父類載入器載入,當無法載入時才由自己載入。為了破壞雙親委派機制必須重寫loadClass方法。
10、講講JAVA的反射機制。 JAVA反射機制是在執行狀態中,對於任意一個實體類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意方法和屬性;這種動態獲取資訊以及動態呼叫物件方法的功能稱為java語言的反射機制。
11、你們線上應用的JVM引數有哪些。
-server | Server模式啟動 |
-Xms6000M | 初始堆記憶體6000m |
-Xmx6000M | 最大堆記憶體6000m |
-Xmn500M | 最小堆記憶體500m |
-XX:PermSize=500M | 永久代500m |
-XX:MaxPermSize=500M | 最大永久代500m |
-XX:SurvivorRatio=65536 | 設定年輕代中Eden區與Survivor區的比值 |
-XX:MaxTenuringThreshold=0 | 設定垃圾最大年齡(在年輕代的存活次數)。如果設定為0的話,則年輕代物件不經過Survivor區直接進入年老代 |
-Xnoclassgc | 關閉class的垃圾回收功能,即虛擬機器載入的類,即便是不使用,沒有例項也不會回收 |
-XX:+DisableExplicitGC | 不響應 System.gc() 程式碼 |
-XX:+UseParNewGC | 設定年輕代為併發收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設定,所以無需再設定此引數 |
-XX:+UseConcMarkSweepGC | 即CMS收集,設定老年代為併發收集 |
-XX:+UseCMSCompactAtFullCollection | 開啟記憶體空間的壓縮和整理,在Full GC後執行。可能會影響效能,但可以消除記憶體碎片 |
-XX:CMSFullGCsBeforeCompaction=0 | 由於併發收集器不對記憶體空間進行壓縮和整理,所以執行一段時間並行收集以後會產生記憶體碎片,記憶體使用效率降低。此引數設定執行0次Full GC後對記憶體空間進行壓縮和整理,即每次Full GC後立刻開始壓縮和整理記憶體 |
-XX:+CMSClassUnloadingEnabled | 允許對類元資料進行回收 |
-XX:+CMSParallelRemarkEnabled | 開啟並行收集 |
-XX:CMSInitiatingOccupancyFraction=90 | 允許90%的Survivor區被佔用(JVM預設為50%)。提高對於Survivor區的使用率 |
-XX:SoftRefLRUPolicyMSPerMB=0 | 軟引用物件在最後一次被訪問後能存活0毫秒(JVM預設為1000毫秒) |
-XX:+PrintClassHistogram | 按下 Ctrl+Break 後列印堆記憶體中類例項的柱狀資訊,同JDK的 jmap -histo 命令 |
-XX:+PrintGCDetails | 每次GC時列印詳細資訊 |
-XX:+PrintGCTimeStamps | 列印每次GC的時間戳 |
-XX:+PrintHeapAtGC | 在進行GC的前後打印出堆的資訊 |
-Xloggc:log/gc.log | 日誌檔案的輸出路徑 |
12、g1和cms區別,吞吐量優先和響應優先的垃圾收集器選擇。 CMS是以獲取最短回收停頓時間為目標的收集器。基於標記-清除演算法實現。比較佔用cpu資源,切易造成碎片。 G1是面向服務端的垃圾收集器,是jdk9預設的收集器,基於標記-整理演算法實現。可利用多核、多cpu,保留分代,實現可預測停頓,可控。具體可參考5
14、請解釋如下jvm引數的含義:-server -Xms512m -Xmx512m -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxTenuringThreshold=20 -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly。
-server | Server模式啟動 |
-Xms512m | 初始堆記憶體512m |
-Xmx512m | 最大堆記憶體512m |
-Xss1024K | 執行緒棧空間1m |
-XX:PermSize=256m | 永久代256m |
-XX:MaxPermSize=512m | 最大永久代512m |
-XX:MaxTenuringThreshold=20 | 最大轉為老年代檢查次數20 |
-XX:CMSInitiatingOccupancyFraction=80 | CMS回收開啟時機:記憶體佔用80% |
-XX:+UseCMSInitiatingOccupancyOnly | 命令JVM不基於執行時收集的資料來啟動CMS垃圾收集週期 |