接着前幾天的兩篇文章,繼續解析JVM面試問題,送給年后想要跳槽的小伙伴
萬萬沒想到,面試中,連 ClassLoader類加載器 也能問出這么多問題.....
三、GC垃圾回收
1、GC是什么?為什么要GC
GC:垃圾收集,GC能幫助我們釋放jvm內存,可以一定程度避免OOM問題,但是也無法完全避免。Java的GC是自動工作的,不像C++需要主動調用。
當new對象的時候,GC就開始監控這個對象的地址大小和使用情況了,通過可達性分析算法尋找不可達的對象然后進行標記看看是否需要GC回收掉釋放內存。
2、你能保證GC執行嗎?
不能,我只能通過手動執行System.gc()
方法通知GC執行,但是他是否執行的未知的。
3、對象的引用類型有哪幾種,分別介紹下
強引用
發生GC的時候不會回收強引用所關聯的對象。比如new就是強引用。
軟引用
有用但非必須的對象,在OOM之前會把這些對象列進回收范圍之中進行第二次回收,若第二次回收還沒有足夠的內存,則會拋出OOM。也就是第一次快要發生OOM的時候不會立馬拋出OOM,而是會回收掉這些軟引用,然后再看內存是否足夠,若還不夠才會拋出OOM。
弱引用
有用但非必須的對象,比軟引用更弱一些,只要開始GC,不管你內存夠不夠,都會將 弱引用所關聯的對象給回收掉。
虛引用
也叫幽靈引用/幻影引用,無法通過虛引用獲得對象,他的意義在於能在這個對象被GC掉時收到一個系統通知,僅此而已。
4、垃圾收集算法有哪些
標記清除
分為兩步:標記和清除。
首先需要標記出所有需要回收的對象,然后進行清除回收變為可用內存。
缺點:效率低,會產生垃圾碎片 。
標記清除01
標記清除
復制算法
將可用堆內存按照容量分為大小相等的兩塊,每次只用一塊,當這塊內存快用完了,就將還存活着的對象復制到另一塊上面,然后再把已使用過的內存一次清理掉。
年輕代from/to(s1/s2)采取的就是此種算法。老年代一般不會采取此種算法,因為老年代都是大對象且存活的久的,空間壓縮一半代價略高。
優點:效率較高、不會產生碎片。
缺點:將內存縮小為原來的一半,代價略高。
標記整理
分為兩步:標記和整理。
整理其實也是兩步:整理+清除。
整理讓所有存活的對象都移動到一端,然后清理掉邊界以外的內存。
優點:不會產生碎片問題,適合年老代的大對象存儲,不像復制算法那樣浪費空間。
缺點:效率趕不上復制算法。
分代算法
並不是新算法,而是根據對象存活周期的不同將內存划分為幾塊,一般是新生代和老年代,新生代基本采用復制算法,老年代采用標記整理算法。
5、為什么要分代
因為在不進行對象存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間會相對較長,也有很多對象完全沒必要遍歷,比如大對象存活的時間更長,遍歷下來發現不需要回收,這樣更浪費時間。所以才有了分代,分治的思想,進行區域划分,把不同生命周期的對象放在不同的區域,不同的區域采取最適合他的垃圾回收方式進行回收。
6、分代垃圾回收是怎么工作的
分代回收基於這樣一個理念:不同的對象的生命周期是不一樣的,因此根據對象存活周期的不同將內存划分為幾塊,一般是新生代和老年代,新生代基本采用復制算法,老年代采用標記整理算法。這樣來提高回收效率。
新生代執行流程:
- 把 Eden + From Survivor(S1) 存活的對象放入 To Survivor(S2) 區;
- 清空 Eden 和S1 區;
- S1 和 S2 區交換,S1 變 S2,S2變S1。
每次在S1到S2移動時都存活的對象,年齡就 +1,當年齡到達 15(默認配置是 15)時,升級為老年代。大對象也會直接進入老年代。
老年代當空間占用到達某個值之后就會觸發全局垃圾收回,一般使用標記整理的執行算法。
7、垃圾回收器有哪些
Serial
采取復制算法,用於新生代,單線程收集器,所以在他工作時會產生StopTheWorld。單線程情況下效率更高,比如用於GUI小程序
ParNew
采取復制算法,用於新生代,是Serial的多線程版本,多個GC線程同時工作,但是也會產生StopTheWorld,因為不能和工作線程並行。
Parallel Scavenge
采取復制算法,用於新生代,和ParNew一樣,所以也會產生STW,多線程收集器,他是吞吐量優先的收集器,提供了很多參數來調節吞吐量。
Serial Old
采取標記整理算法,用於老年代,單線程收集器,所以在他工作時會產生StopTheWorld。單線程情況下效率更高,比如用於GUI小程序
Parallel Old
采取標記整理算法,用於老年代,Parallel Scavenge收集器的老年代版本,吞吐量優先。
CMS
采取標記清除算法,老年代並行收集器,號稱以最短STW時間為目標的收集器,並發高、停頓低、STW時間短的優點。主流垃圾收集器之一。
G1
采取標記整理算法,並行收集器。對比CMS的好處之一就是不會產生內存碎片,此外,G1收集器不同於之前的收集器的一個重要特點是:G1回收的范圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的范圍僅限於新生代或老年代。而且他的STW停頓時間是可以手動控制一個長度為M毫秒的時間片段(可以用JVM參數 -XX:MaxGCPauseMillis指定),設置完后垃圾收集的時長不得超過這個(近實時)。
8、詳細介紹一下 CMS 垃圾回收器?
采取標記清除算法,老年代並行收集器,號稱以最短STW時間為目標的收集器,並發高、停頓低、STW時間短的優點。主流垃圾收集器之一。
主要分為四階段:
- 初始標記:只是標記一下 GC Roots 能直接關聯的對象,速度很快,仍然需要暫停所有的工作線程。所以此階段會STW,但時間很短。
- 並發標記:進行 GC Roots 跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。不會STW。
- 重新標記:為了修正在並發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,仍然需要暫停所有的工作線程。STW時間會比第一階段稍微長點,但是遠比並發標記短,效率也很高。
- 並發清除:清除GC Roots不可達對象,和用戶線程一起工作,不需要暫停工作線程。
所以CMS的優點是:
- 並發高
- 停頓低
- STW時間短。
缺點:
-
對cpu資源非常敏感(並發階段雖然不會影響用戶線程,但是會一起占用CPU資源,競爭激烈的話會導致程序變慢)。
-
無法處理浮動垃圾,當剩余內存不能滿足程序運行要求時,系統將會出現 Concurrent Mode Failure,失敗后而導致另一次Full GC的產生,由於CMS並發清除階段用戶線程還在運行,伴隨程序的運行自然會有新的垃圾產生,這一部分垃圾是出現在標記過程之后的,CMS無法在本次去處理他們,所以只好留在下一次GC時候將其清理掉。
-
內存碎片問題(因為是標記清除算法)。當剩余內存不能滿足程序運行要求時,系統將會出現 Concurrent Mode Failure,臨時 CMS 會采用 Serial Old 回收器進行垃圾清除,此時的性能將會被降低。
9、詳細介紹一下 G1 垃圾回收器?
采取標記整理算法,並行收集器。
特點:
- 並行與並發執行:利用多CPU的優勢來縮短STW時間,在GC工作的時候,用戶線程可以並行執行。
- 分代收集:無需其他收集器配合,自己G1會進行分代收集。
- 空間整合:不會像CMS那樣產生內存碎片。
- 可預測的停頓:可以手動控制一個長度為M毫秒的時間片段(可以用JVM參數 -XX:MaxGCPauseMillis指定),設置完后垃圾收集的時長不得超過這個(近實時)。
原理:
G1並不是簡單的把堆內存分為新生代和老年代兩部分,而是把整個堆划分為多個大小相等的獨立區域(Region),新生代和老年代也是一部分不需要連續Region的集合。G1跟蹤各個Region里面的垃圾堆積的價值大小,在后台維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。
補充:
Region不是孤立的,也就是說一個對象分配在某個Region中,他並非只能被本Region中的其他對象引用,而是整個堆中任意的對象都可以相互引用,那么在【可達性分析法】來判斷對象是否存活的時候也無需掃描整個堆,Region之間的對象引用以及其他手機其中新生代和老年代之間的對象引用虛擬機都是使用Remembered Set來避免全堆掃描的。
步驟:
- 初始標記:僅僅標記GCRoots能直接關聯到的對象,且修改TAMS的值讓下一階段用戶程序並發運行時能正確可用的Region中創建的新對象。速度很快,會STW。
- 並發標記:進行 GC Roots 跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。不會STW。
- 最終標記:為了修正在並發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,仍然需要暫停所有的工作線程。STW時間會比第一階段稍微長點,但是遠比並發標記短,效率也很高。
- 篩選回收:首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計划。
10、GC日志分析
11、Minor GC與Full GC分別在什么時候發生
新生代內存(Eden區)不夠用時候發生Minor GC也叫YGC。
Full GC發生情況:
- 老年代被寫滿
- 持久代被寫滿
- System.gc()被顯示調用(只是會告訴需要GC,什么時候發生並不知道)
12、新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么區別?
- 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1
新生代垃圾回收器一般采用的是復制算法,復制算法的優點是效率高,缺點是內存利用率低;老年代回收器一般采用的是標記-整理的算法進行垃圾回收。標記整理很適合大對象,不會產生空間碎片。
13、棧上分配是什么意思
JVM允許將線程私有的對象分配在棧上,而不是分配在堆上。分配在棧上的好處是棧上分配不需要考慮垃圾回收,因為出棧的時候對象就順帶着一起出去了,沒了,而不需要垃圾回收器的介入,從而提高系統性能。
補充1:對象逃逸。
逃逸的目的是判斷對象的作用域是否有可能逃出函數體。例如下面的代碼就顯示了一個逃逸的對象:
private User user; private void hello(){ user = new User(); }
對象實例 user 是類的成員變量,可以被任何線程訪問,因此它屬於逃逸對象。但如果我們將代碼稍微改動一下,該對象就可以線程非逃逸的了。
private void hello(){ User user = new User(); }
可以看到 user 實例作用域只在 hello 函數中,不會被其他線程訪問到,也不會訪問。所以該 user 實例對象的作用域只在該函數中,因此它並未發生逃逸。對於這樣的情況,虛擬機就有可能將其分配在棧上,而不在堆上。
補充2:TLAB,自行Google。
簡單點說,就是將本來應該分配在堆中的對象,讓其分配在線程私有的棧上。通過這種方式,減少垃圾回收的壓力,提高虛擬機的運行效率。
14、簡述下對象的分配規則
- 對象優先分配在Eden區,如果Eden區沒有足夠的空間時,虛擬機執行一次YGC。並將還活着的對象放到from/to區,若本次YGC后還是沒有足夠的空間,則將啟用分配擔保機制在老年代中分配內存。
- 大對象直接進入老年代(大對象是指需要大量連續內存空間的對象)。這樣做的目的是避免在Eden區和兩個Survivor區之間發生大量的內存拷貝(新生代采用復制算法收集內存)。
- 長期存活的對象進入老年代。虛擬機為每個對象定義了一個年齡計數器,如果對象經過了1次YGC那么對象會進入Survivor區,之后每經過一次YGC那么對象的年齡加1,直到達到閥值對象進入老年區。默認閾值是15。可以通過
-XX:MaxTenuringThreshold
參數來設置。 - 動態判斷對象的年齡。如果Survivor區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代。無需等到
-XX:MaxTenuringThreshold
參數要求的年齡。
動態年齡判斷是有歧義的,要想面試加分,必看這個
https://www.jianshu.com/p/989d3b06a49d
- 空間分配擔保。每次進行YGC時,JVM會計算Survivor區移至老年區的對象的平均大小,如果這個值大於老年區的剩余值大小則進行一次Full GC,如果小於檢查HandlePromotionFailure設置,如果true則只進行YGC,如果false則進行Full GC。
推薦好文
強大,10k+點贊的 SpringBoot 后台管理系統竟然出了詳細教程!