JVM(Java虛擬機)簡單來說就是運行Java代碼的解釋器,作為螺絲釘程序員JVM其實瞭解下就差不多啦,不懂JVM內部細節照樣能寫出優質的代碼!但是一到造火箭、飛機的場景(面試)不懂JVM的你,會被面試官虐的體無完膚,本期內容列舉常見的JVM面試題:
- 說一JVM的內存模型是什麼樣子的?
- 什麼時候對象可以被收回?
- 常見的垃圾回收器算法有哪些,各有什麼優劣?
- 什麼時候對象會進入老年代?
- 什麼是空間分配擔保策略?
- 如何優化減少Full GC?
面對這一大波JVM面試題,你真的Hold住嗎?
JVM的內存模型是什麼樣子的?
JVM內存模型可以大致可劃分為線程私有區域和共享區域,線程私有區域由虛擬機棧、本地方法棧、程序計數器組成,而共享區域由堆、元數據空間(方法區)組成。
再有人問你JVM的內存模型就回想下上面的圖,但是知道JVM的內存模型的樣子還是不行的,還要知道他們分別幹什麼的。
虛擬機棧/本地方法棧
當你碰到過StackOverflowException這個異常的時候,有沒有思考下為什麼會出現這樣的異常呢?答案就在虛擬機棧中,JVM會為每個方法生成棧幀然後將棧幀壓入虛擬機棧中。
舉個粟子:假設JVM參數-Xss設置為1m,如果某個方法裡面創建一個128kb的數組,那這個方法在同一個線程中只能遞歸4次,再遞歸第五次的時候就會報StackOverflowException異常,因為虛擬機棧的大小隻有1m,每次遞歸都需要為方法在虛擬機棧中分配128kb的空間,很顯示到第五次的時候就空間不足了。
程序計數器
程序計數器是一個記錄著當前線程所執行的字節碼的行號指示器。JVM的多線程是通過CPU時間片輪轉(即線程輪流切換並分配處理器執行時間)算法來實現的。也就是說,某個線程在執行過程中可能會因為時間片耗盡而被掛起,而另一個線程獲取到時間片開始執行。
簡單的說程序計數器的主要功能就是記錄著當前線程所執行的字節碼的行號指示器。
方法區(元數據區)
方法區存儲了類的元數據信息、靜態變量、常量等數據。
堆(heap)
平常大家使用new關鍵字創建的對象都會進入堆中,堆也是GC重點照顧的區域,堆會被劃分為:新生代、老年代,而新生代還會被進一步劃分為Eden區和Survivor區:
新生代中的Eden區和Survivor區,是根據JVM回收算法來的,只是現在大部分都是使用的分代回收 算法,所以在介紹堆的時候會直接將新生代歸納為Eden區和Survivor區。
小結
JVM內存模型小結:
- JVM內存模型劃分為線程私有區域和共享區域
- 虛擬機棧/本地方法棧負責存放線程執行方法棧幀
- 程序計數器用於記錄線程執行指令的位置
- 方法區(元數據區)存儲類的元數據信息、靜態變量、常量等數據
- 堆(heap)使用new關鍵字創建的對象都會進入堆中,堆被劃分為新生代和老年代
什麼時候對象可以被收回?
JVM判斷對象回收有兩種方式:引用記數 、GC Roots,引用記數比較簡單,JVM為每個對象維護一個引用計數,假設A對象引用計數為零說明沒有任務對象引用A對象,那A對象就可以被回收了,但是引用計數有個缺點就是無法解決循環引用的問題。
GC Roots通過一系列的名為GC Roots的對象作為起始點,從這些節點開始向下搜索,搜索過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明對象是不可用的。
在Java中,可以作為GC Roots的對象包括下面幾種:
- 虛擬機棧中引用的對象;
- 方法區中類靜態屬性引用的對象;
- 方法區中的常量引用的對象;
- 本地方法棧中JNI(即一般說的Native方法)的引用的對象;
小結
總的來說就是當一個對象通過GC Roots搜索不到時,說明對象可以被回收了,但什麼時候回收還要看GC的心情!
常見的垃圾回收器算法有哪些,各有什麼優劣?
標記清除
這種算法分兩分:標記、清除兩個階段,
標記階段是從根集合(GC Root)開始掃描,每到達一個對象就會標記該對象為存活狀態,清除階段在掃描完成之後將沒有標記的對象給清除掉。
用一張圖說明:
這個算法有個缺陷就是會產生內存碎片,如上圖B被清除掉後會留下一塊內存區域,如果後面需要分配大的對象就會導致沒有連續的內存可供使用。
標記整理
標記整理就沒有內存碎片的問題了,也是從根集合(GC Root)開始掃描進行標記然後清除無用的對象,清除完成後它會整理內存。
這樣內存就是連續的了,但是產生的另外一個問題是:每次都得移動對象,因此成本很高。
複製算法
複製算法會將JVM推分成二等分,如果堆設置的是1g,那使用複製算法的時候堆就會有被劃分為兩塊區域各512m。給對象分配內存的時候總是使用其中的一塊來分配,分配滿了以後,GC就會進行標記,然後將存活的對象移動到另外一塊空白的區域,然後清除掉所有沒有存活的對象,這樣重複的處理,始終就會有一塊空白的區域沒有被合理的利用到。
兩塊區域交替使用,最大問題就是會導致空間的浪費,現在堆內存的使用率只有50%。
小結
JVM回收算法小結:
- 標記清除速度快,但是會產生內存碎片;
- 標記整理解決了標記清除內存碎片的問題,但是每次都得移動對象,因此成本很高;
- 複製算法沒有內存碎片也不需要移動對象,但是導致空間的浪費;
什麼時候對象會進入老年代?
新創建出來的對象一開始都會停留在新生代中,但隨著JVM的運行,有些存活的長的對象會慢慢的移動到老年代中。
根據對象年齡
JVM會給對象增加一個年齡(age)的計數器,對象每“熬過”一次GC,年齡就要+1,待對象到達設置的閾值(默認為15歲)就會被移移動到老年代,可通過-XX:MaxTenuringThreshold調整這個閾值。
一次Minor GC後,對象年齡就會+1,達到閾值的對象就移動到老年代,其他存活下來的對象會繼續保留在新生代中。
動態年齡判斷
根據對象年齡有另外一個策略也會讓對象進入老年代,不用等待15次GC之後進入老年代,他的大致規則就是,假如當前放對象的Survivor,一批對象的總大小大於這塊Survivor內存的50%,那麼大於這批對象年齡的對象,就可以直接進入老年代了。
如圖上的A、B、D、E這四個對象,假如Survivor 2是100m,如果A + B + D的內存大小超過50m,現在D的年齡是10,那E都會被移動到老年代。實際上這個計算邏輯是這樣的:年齡1 + 年齡2 + 年齡n的多個對象總和超過Survivor區的50%,那就會把年齡n以上的對象都放入老年代。
大對象直接進入老年代
如果設置了-XX:PretenureSizeThreshold這個參數,那麼如果你要創建的對象大於這個參數的值,比如分配一個超大的字節數組,此時就直接把這個大對象放入到老年代,不會經過新生代。
這麼做就可以避免大對象在新生代,屢次躲過GC,還得把他們來複制來複制去的,最後才進入老年代,這麼大的對象來回複製,是很耗費時間的。
什麼是空間分配擔保策略?
JVM在發生Minor GC之前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代所有對象的總空間,如果大於,則此次Minor GC是安全的如果小於,則虛擬機會查看HandlePromotionFailure設置項的值是否允許擔保失敗。如果HandlePromotionFailure=true,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小,如果大於則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的;如果小於或者HandlePromotionFailure=false,則改為進行一次Full GC。
如何優化減少Full GC?
將前面的一些問題總結下來,然後應用到線上,那JVM應該如何優化減少Full GC呢?以標準的4核8G機器為例說明,首先系統預留4G,其他4G按如下規則分配 :
- 堆內存:3g
- 新生代:1.5g
- 新生代Eden區:1228m
- 新生代Survivor區:153m
- 方法區:256m
- 虛擬機棧:1m/thread
設置參數如下:
-Xms3072m
-Xmx3072m
-Xmn1536m
-Xss=1m
-XX:PermSize=256m
-XX:MaxPermSize=256m
-XX:HandlePromotionFailure
-XX:SurvivorRatio=8
估算系統每秒佔用內存數量
在優化JVM之前,要先估算要系統每秒佔用的內存數量,如有個日活百萬的商場系統,每日下單量在20w左右,按照一天8個小時算,那訂單服務的每秒大概會有500個請求,然後粗略的估算下每個請求佔用多少內存,計算出每秒要花費多少內存。
假設是每秒500個請求,每個請求需要分配100k的空間,那1秒需要分配大約50m的內存。
計算下多長時間觸發一次Minor GC
按照之前的估算1秒需要分配大約50m的內存的話,Eden區的空間是1228m那平均每25秒就要執行一次Minor GC。
檢查下Survivor區是否足夠
按照上面的模型,每25秒就要執行一次Minor GC,GC執行期間並不能回收掉所有的新生代中的對象,那每秒50m那每次GC執行期間還會剩下大約100m無法回收的對象會進入Survivor區,但是別忘記JVM有
動態年齡判斷機制,這樣設置下來Survivor的空間明顯小了一點,所以將新生代設置2048m,才能避免觸發動態年齡判斷:-Xms3072m
-Xmx3072m
-Xmn2048m
...
大對象直接進入老年代
大對象一般是長期存活和使用的對象,一般來說設置1M的對象直接進入老年代,這樣避免大對象一直處於新生代中來回複製,所以加上PretenureSizeThreshold=1m參數。
...
-XX:PretenureSizeThreshold=1m
...
合理設置對象年齡閾值
Minor GC後默認躲過15次垃圾回收後自動升入老年代,按照我們的評估25秒觸發一次Minor GC,如果按照MaxTenuringThreshold參數的默認值,躲過15次GC後,應該是6分鐘之後的事了,結合當前業務場景這裡可以降低一點,讓那些本應該進入老年代的對象,儘快的進入老年代,避免複製成本和浪費新生代空間,從而導致新生代Survivor空間不足,引發Full GC。
...
-XX:MaxTenuringThreshold=6
...
閱讀更多 Java識堂 的文章