頭圖 | CSDN 下載自視覺中國
作者 | sowhat1412 責編 | 張文
想要提高程式設計師自身的內功心法無非就是數據結構跟算法 + 作業系統 + 計網 + 底層,而所有的 Java 代碼都是在 JVM 上運行的,了解了 JVM 好處就是:
寫出更好更健壯的代碼。提高 Java 的性能,排除問題。面試必問,要對知識有一定對深度。
簡述JVM 內存模型
從宏觀上來說 JVM 內存區域 分為三部分線程共享區域、線程私有區域、直接內存區域。
1.1、線程共享區域
堆區
堆區 Heap 是 JVM 中最大的一塊內存區域,基本上所有的對象實例都是在堆上分配空間。堆區細分為年輕代和老年代,其中年輕代又分為 Eden、S0、S1 三個部分,他們默認的比例是 8:1:1 的大小。
方法區:
在 《Java 虛擬機規範》中只是規定了有方法區這麼個概念跟它的作用。HotSpot 在 JDK8 之前 搞了個永久代把這個概念實現了。用來主要存儲類信息、常量池、靜態變量、JIT 編譯後的代碼等數據。PermGen(永久代)中類的元數據信息在每次 FullGC 的時候可能會被收集,但成績很難令人滿意。而且為 PermGen 分配多大的空間因為存儲上述多種數據很難確定大小。因此官方在 JDK8 剔除移除永久代。官方解釋移除永久代:
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.即:移除永久代是為融合 HotSpot JVM 與 JRockit VM 而做出的努力,因為 JRockit 沒有永久代,不需要配置永久代。元空間:
在 Java 中用永久代來存儲類信息,常量,靜態變量等數據不是好辦法,因為這樣很容易造成內存溢出。同時對永久代的性能調優也很困難,因此在 JDK8 中 把永久代去除了,引入了元空間 metaspace,原先的 class、field 等變量放入到 metaspace。
總結:
元空間的本質和永久代類似,都是對 JVM 規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過參數來指定元空間的大小。
1.2、直接內存區域
直接內存:
一般使用 Native 函數操作 C++代碼來實現直接分配堆外內存,不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。這塊內存不受 Java 堆空間大小的限制,但是受本機總內存大小限制所以也會出現 OOM 異常。分配空間後避免了在 Java 堆區跟 Native 堆中來回複製數據,可以有效提高讀寫效率,但它的創建、銷毀卻比普通 Buffer 慢。
PS:如果使用了 NIO,本地內存區域會被頻繁的使用,此時 jvm 內存 ≈ 方法區 + 堆 + 棧+ 直接內存
1.3、線程私有區域
程序計數器、虛擬機棧、本地方法棧跟線程的聲明周期是一樣的。
程序計數器
課堂上比如你正在看小說《誅仙》,看到 1412 章節時,老師喊你回答問題,這個時候你肯定要先應付老師的問題,回答完畢後繼續接著看,這個時候你可以用書籤也可以憑藉記憶記住自己在看的位置,通過這樣實現繼續閱讀。
落實到代碼運行時候同樣道理,程序計數器用於記錄當前線程下虛擬機正在執行的字節碼的指令地址。它具有如下特性:
線程私有:多線程情況下,在同一時刻所以為了讓線程切換後依然能恢復到原位,每條線程都需要有各自獨立的程序計數器。沒有規定 OutOfMemoryError:程序計數器存儲的是字節碼文件的行號,而這個範圍是可知曉的,在一開始分配內存時就可以分配一個絕對不會溢出的內存。執行Native方法時值為空:Native 方法大多是通過 C 實現,並未編譯成需要執行的字節碼指令,也就不需要去存儲字節碼文件的行號了。虛擬機棧
方法的出入棧:調用的方法會被打包成棧楨,一個棧楨至少需要包含一個局部變量表、操作數棧、楨數據區、動態連結。
動態連結:
當棧幀內部包含一個指向運行時常量池引用前提下,類加載時候會進行符號引用到直接引用的解析跟連結替換。
局部變量表:
局部變量表是棧幀重要組中部分之一。他主要保存函數的參數以及局部的變量信息。局部變量表中的變量作用域是當前調用的函數。函數調用結束後,隨著函數棧幀的銷毀。局部變量表也會隨之銷毀,釋放空間。
操作數棧:保存著Java虛擬機執行過程中數據
方法返回地址:方法被調用的位置,當方法退出時候實際上等同於當前棧幀出棧。
比如執行簡單加減法:
public class ShowByteCode {private String xx;private static final int TEST = 1;public ShowByteCode() { }public int calc() {int a = 100;int b = 200;int c = 300;return (a + b) * c; }}
執行javap -c *.class:
本地方法棧
跟虛擬機棧類似,只是為使用到的Native方法服務而已。
判斷對象是否存活
JVM 空間不夠就需要 Garbage Collection 了。
一般共享區的都要被回收比如堆區以及方法區。在進行內存回收之前要做的事情就是判斷那些對象是死的,哪些是活的。常用方法有兩種引用計數法跟可達性分析。
2.1、引用計數法
思路是給 Java 對象添加一個引用計數器,每當有一個地方引用它時,計數器 +1;引用失效則 -1,當計數器不為 0 時,判斷該對象存活;否則判斷為死亡(計數器 = 0)。
優點:實現簡單,判斷高效。
缺點:無法解決對象間相互循環引用的問題
class GcObject {public Object instance = null;}public class GcDemo {public static void main(String[] args) { GcObject object1 = new GcObject(); // step 1 GcObject object2 = new GcObject(); // step 2 object1.instance = object2 ;//step 3 object2.instance = object1; //step 4 object1 = null; //step 5 object2 = null; // step 6 }}
step1: GcObject實例1的引用計數+1,實例1引用數 = 1step2: GcObject實例2的引用計數+1,實例2引用數 = 1step3: GcObject實例2的引用計數+1,實例2引用數 = 2step4: GcObject實例1的引用計數+1,實例1引用數 = 2step5: GcObject實例1的引用計數-1,結果為 1 step6: GcObject實例2的引用計數-1,結果為 1如上分析發現實例 1 跟實例 2 的引用數都不為 0 而又相互引用,這兩個實例所佔有的內存則無法釋放。
2.2、可達性分析
很多主流商用語言(如 Java、C#)都採用引用鏈法判斷對象是否存活,大致的思路就是將一系列的 GC Roots 對象作為起點,從這些起點開始向下搜索。
在 Java 語言中,可作為 GC Roots 的對象包含以下幾種:
第一種是虛擬機棧中的引用的對象。在程序中正常創建一個對象,對象會在堆上開闢一塊空間,同時會將這塊空間的地址作為引用保存到虛擬機棧中,如果對象生命周期結束了,那麼引用就會從虛擬機棧中出棧,因此如果在虛擬機棧中有引用,就說明這個對象還是有用的,這種情況是最常見的。第二種是我們在類中定義了全局的靜態的對象,也就是使用了 static 關鍵字,由於虛擬機棧是線程私有的,所以這種對象的引用會保存在共有的方法區中,顯然將方法區中的靜態引用作為 GC Roots 是必須的。第三種便是常量引用,就是使用了 static final 關鍵字,由於這種引用初始化之後不會修改,所以方法區常量池裡的引用的對象也應該作為 GC Roots。第四種是在使用 JNI 技術時,有時候單純的 Java 代碼並不能滿足我們的需求,我們可能需要在 Java 中調用 C 或 C++的代碼,因此會使用 Native 方法,JVM 內存中專門有一塊本地方法棧,用來保存這些對象的引用,所以本地方法棧中引用的對象也會被作為 GC Roots。GC Root 步驟主要包含如下三步:
可達性分析
當一個對象到 GC Roots 沒有任何引用鏈相連時,則判斷該對象不可達。
注意: 可達性分析僅僅只是判斷對象是否可達,但還不足以判斷對象是否存活 / 死亡。
第一次標記 & 篩選
篩選的條件對象 如果沒有重寫 finalize 或者調用過 finalize,則將該對象加入到 F-Queue 中
第二次標記 & 篩選
當對象經過了第一次的標記 & 篩選,會被進行第二次標記 & 準備被進行篩選。經過 F-Queue 篩選後如果對象還沒有跟 GC Root 建立引用關係則被回收,屬於給個二次機會。
2.3、四大引用類型
強引用
強引用(StrongReference)是使用最普遍的引用。垃圾回收器絕對不會回收它,內存不足時寧願拋出 OOM 導致程序異常,平常的 new 對象就是。
軟引用
垃圾回收器在內存充足時不會回收軟引用(SoftReference)對象,不足時會回收它,特別適合用於創建緩存。
弱引用
弱引用(WeakReference)是在掃描到該對象時無論內存是否充足都會回收該對象。ThreadLocal 的 Key 就是弱引用。
虛引用
如果一個對象只具有虛引用(PhantomReference)那麼跟沒有任何引用一樣,任何適合都可以被回收。主要用跟蹤對象跟垃圾回收器回收的活動。
垃圾回收算法
為了揮手回收垃圾作業系統一般會使用標記清除、複製算法、標記整理三種算法,這三種各有優劣。簡單介紹下:
3.1、標記清除
原理:算法分為標記和清除兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。
缺點:標記清除之後會產生大量不連續的內存碎片,導致觸發 GC。
3.2、標記複製
原理:將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。
缺點:這種算法的代價是將內存縮小為了原來的一半,還要來回移動數據。
3.3、標記整理
原理:首先標記出所有需要回收的對象,在標記完成後,後續步驟是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。
缺點:涉及到移動大量對象,效率不高。
總結:
3.4 、三色標記跟讀寫屏障
前面說的三種回收算法都說到了先標記,問題是如何標記的呢?
接下來的知識點個人感覺面試應該問不到那麼深了,但是為了裝逼必須 Mark下!
CMS、G1 標記時候一般用的是三色標記法,根據可達性分析從 GC Roots 開始進行遍歷訪問,可達的則為存活對象,而最終不可達說明就是需要被 GC 對象。
大致流程是把遍歷對象圖過程中遇到的對象,按是否訪問過這個條件標記成以下三種顏色:
白色:尚未訪問過。
黑色:本對象已訪問過,而且本對象引用到的其他對象,也全部訪問過了。
灰色:本對象已訪問過,但是本對象引用到的其他對象,尚未全部訪問完。全部訪問後會轉換為黑色。
假設現在有白、灰、黑三個集合(表示當前對象的顏色),遍歷訪問過程:
初始時所有對象都在白色集合中。將 GC Roots 直接引用到的對象挪到灰色集合中。 從灰色集合中獲取對象:第一步將本對象引用到的其他對象,全部挪到灰色集合中,第二步將本對象挪到黑色集合裡面。 重複步驟3,直至灰色集合為空時結束。 結束後仍在白色集合的對象即為 GC Roots 不可達,可以嘗試進行回收。當 STW 時,對象間的引用是不會發生變化的,可以輕鬆完成標記。當支持並發標記時,對象間的引用可能發生變化,多標和漏標的情況就有可能發生。
浮動垃圾
狀況:GC 線程遍歷到 E(E是灰色),一個業務線程執行了 D.E = null,此時 E 應該被回收的。但是 GC 線程已經認為 E 是灰色了會繼續遍歷,導致 E 沒有被回收。
漏標
GC 線程遍歷到 E(灰色了)。業務線程執行了 E-->G 斷開,D-->G 連結的操作。GC 線程發現 E 無法到達 G,因為是黑色不會再遍歷標記了。最終導致漏標 G。
漏標的必備兩個條件:灰到白斷開,黑到白建立。
Object G = E.G; // 第一步 :讀Object E.G = null; // 第二步:寫Object D.G = G; // 第三步:寫
漏標解決方法:將對象 G 存儲到特定集合中,等並發標記遍歷完畢後再對集合中對象進行重新標記。
CMS方案
這裡比如開始 B 指向 C,但是後來 B 不指向 C,A 指向 D,最簡單的方法是將 A 變成灰色,等待下次進行再次遍歷。CMS 中可能引發 ABA 問題:
回收線程 m1 正在標記 A,屬性 A.1 標記完畢,正在標記屬性 A.2。業務線程 m2 把屬性 1 指向了 C,由於 CMS 方案此時回收線程 m3 把 A 標記位灰色。 回收線程 m1 認為所有屬性標記完畢,將 A 設置為黑色,結果 C 漏標。所以 CMS 階段需要重新標記。
讀寫屏障
漏標的實現是有三步的,JVM 加入了讀寫屏障,其中讀屏障則是攔截第一步,寫屏障用於攔截第二和第三步。
寫屏障 + SATB(原始快照) 來破壞 灰到白斷開。
寫屏障 + 增量更新 來破壞 黑到白建立。
讀屏障 一種保守方式來破壞灰到白斷開後白的存儲,此時用讀屏障 OK 的。
現代使用可達性分析的垃圾回收器幾乎都借鑑了三色標記的算法思想,儘管實現的方式不盡相同。
對於讀寫屏障,以 Java HotSpot VM 為例,其並發標記時對漏標的處理方案如下:
CMS:寫屏障 + 增量更新
G1:寫屏障 + SATB
ZGC:讀屏障
CMS 中使用的增量更新,在重新標記階段除了需要遍歷 寫屏障的記錄,還需要重新掃描遍歷 GC Roots(標記過的不用再標記),這是由於 CMS 對於 astore_x 等指令不添加寫屏障的原因。
GC 流程
核心思想就是根據各個年代的特點不同選用不同到垃圾收集算法。
年輕代:使用複製算法老年代:使用標記整理或者標記清除算法。為什麼要有年輕代:分代的好處就是優化 GC 性能。如果沒有分代每次掃描所有區域能累死 GC。因為很多對象幾乎就是朝生夕死的,如果分代的話,我們把新創建的對象放到某一地方,當 GC 的時候先把這塊存朝生夕死(80%以上)對象的區域進行回收,這樣就會騰出很大的空間出來。
4.1、年輕代
HotSpot JVM 把年輕代分為了三部分:1 個 Eden 區和 2 個 Survivor 區(分別叫 from 和 to)。默認比例為 8:1:1。
一般情況下,新創建的對象都會被分配到 Eden 區(一些大對象特殊處理),這些對象經過第一次 Minor GC 後,如果仍然存活,將會被移到 Survivor 區。對象在 Survivor 區中每熬過一次 Minor GC 年齡就會增加 1 歲,當它的年齡增加到一定次數(默認 15 次)時,就會被移動到年老代中。年輕代的垃圾回收算法使用的是複製算法。
年輕代 GC 過程:GC 開始前,年輕代對象只會存在於 Eden 區和名為 From 的 Survivor 區,名為 To 的 Survivor 區永遠是空的。如果新分配對象在 Eden 申請空間發現不足就會導致 GC。
yang GC:Eden 區中所有存活的對象都會被複製到 To,而在 From 區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值可以通過-XX:MaxTenuringThreshold 來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複製到 To 區域。經過這次 GC 後,Eden 區和 From 區已經被清空。這個時候,From 和 To 會交換他們的角色,也就是新的 To 就是上次 GC 前的 From,新的 From 就是上次 GC 前的 To。
不管怎樣都會保證名為 To 的 Survivor 區域是空的。Minor GC 會一直重複這樣的過程,直到 To 區被填滿,To 區被填滿之後,會將所有對象移動到年老代中。
這裡注意如果 yang GC 後空間還是不夠用則會空間擔保機制將數據送到 Old 區
卡表 Card Table:
為了支持高頻率的新生代回收,虛擬機使用一種叫做卡表(Card Table)的數據結構,卡表作為一個比特位的集合,每一個比特位可以用來表示年老代的某一區域中的所有對象是否持有新生代對象的引用。新生代 GC 時不用花大量的時間掃描所有年老代對象,來確定每一個對象的引用關係,先掃描卡表,只有卡表的標記位為1時,才需要掃描給定區域的年老代對象。而卡表位為 0 的所在區域的年老代對象,一定不包含有對新生代的引用。4.2、老年代
老年代 GC 過程:老年代中存放的對象是存活了很久的,年齡大於 15 的對象 或者觸發了老年代的分配擔保機制存儲的大對象。在老年代觸發的 gc 叫 major gc ,也叫 full gc。full gc 會包含年輕代的 gc。full gc 採用的是標記-清除或標記整理。在執行 full gc 的情況下,會阻塞程序的正常運行。老年代的 gc 比年輕代的 gc 效率上慢 10 倍以上。對效率有很大的影響。所以一定要儘量避免老年代 GC!
4.3、元空間
永久代的回收會隨著 full gc 進行移動,消耗性能。
每種類型的垃圾回收都需要特殊處理元數據。將元數據剝離出來,簡化了垃圾收集,提高了效率。
-XX:MetaspaceSize 初始空間的大小。達到該值就會觸發垃圾收集進行類型卸載,同時 GC 會對該值進行調整:
如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過 MaxMetaspaceSize 時,適當提高該值。
-XX:MaxMetaspaceSize:最大空間,默認是沒有限制的。
4.4 、垃圾回收流程總結
大致的 GC 回收流程如上圖,還有一種設置就是大對象直接進入老年代:
如果在新生代分配失敗且對象是一個不含任何對象引用的大數組,可被直接分配到老年代。通過在老年代的分配避免新生代的一次垃圾回收。設置了-XX:PretenureSizeThreshold 值,任何比這個值大的對象都不會嘗試在新生代分配,將在老年代分配內存。內存回收跟分配策略
優先在 Eden 上分配對象,此區域垃圾回收頻繁速度還快。大對象直接進入老生代。年長者(長期存活對象默認 15 次)跟 進入老生代。在 Survivor 空間中相同年齡所有對象大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象會群體進入老生代。空間分配擔保(擔保 minorGC),如果 Minor GC 後 Survivor 區放不下新生代仍存活的對象,把 Suvivor 無法容納的對象直接進人老年代。
垃圾收集器
5.1、垃圾收集器
堆 heap 是垃圾回收機制的重點區域。
我們知道垃圾回收機制有三種 minor gc、major gc 和 full gc。針對於堆的就是前兩種。年輕代的叫 minor gc,老年代的叫 major gc。
JDK7、JDK8 默認垃圾收集器 Parallel Scavenge(新生代)+ Parallel Old(老年代)JDK9 默認垃圾收集器 G1服務端開發常見組合就是 ParNew + CMS
工程化使用的時候使用指定的垃圾收集器組合使用,講解垃圾收集器前先普及幾個重要知識點:
STW
java 中 Stop-The-World 機制簡稱 STW。是指執行垃圾收集算法時,Java 應用程式的其他所有線程都被掛起(除了垃圾收集幫助器之外)。是 Java 中一種全局暫停現象。全局停頓,所有 Java 代碼停止,native 代碼雖然可以執行但不能與 JVM 交互,如果發生了 STW 現象多半是由於 gc 引起。
吞吐量
吞吐量 = 運行用戶代碼時間 / ( 運行用戶代碼時間 + 垃圾收集時間 )。例如:虛擬機共運行 100 分鐘,垃圾收集器花掉 1 分鐘,那麼吞吐量就是 99%
垃圾收集時間:垃圾回收頻率 * 單次垃圾回收時間
並行收集:指多條垃圾收集線程並行工作,但此時用戶線程仍處於等待狀態。
並發收集:指用戶線程與垃圾收集線程同時工作(不一定是並行的可能會交替執行)。用戶程序在繼續運行,而垃圾收集程序運行在另一個CPU上。
5.2、新生代
新生代有 Serial、ParNew、Parallel Scavenge 三種垃圾收集器。
5.3、老年代
老年代有 Serial Old、Parallel Old、CMS 三種垃圾收集器。
CMS
CMS(Concurrent Mark Sweep)比較重要這裡 重點說一下。
CMS 的初衷和目的:為了消除 Throught 收集器和 Serial 收集器在 Full GC 周期中的長時間停頓。是一種以獲取最短回收停頓時間為目標的收集器,具有自適應調整策略,適合網際網路站 跟 B/S 服務應用。
CMS 的適用場景:如果你的應用需要更快的響應,不希望有長時間的停頓,同時你的 CPU 資源也比較豐富,就適合適用 CMS 收集器。比如常見的 Server 端任務。
優點:並發收集、低停頓。
缺點:
CMS 收集器對 CPU 資源非常敏感:在並發階段,雖然不會導致用戶線程停頓,但是會佔用 CPU 資源而導致引用程序變慢,總吞吐量下降。無法處理浮動垃圾:由於 CMS 並發清理階段用戶線程還在運行,伴隨程序的運行自熱會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後, CMS 無法在本次收集中處理它們,只好留待下一次 GC 時將其清理掉。這一部分垃圾稱為浮動垃圾。如果內存放不下浮動垃圾這時 JVM 啟動 Serial Old 替代 CMS。空間碎片:CMS 是基於標記-清除算法實現的收集器,使用標記-清除算法收集後,會產生大量碎片。CMS 回收流程:
初始標記:引發 STW, 僅僅只是標記出 GC ROOTS 能直接關聯到的對象,速度很快。並發標記:不引發 STW,正常運行所有 Old 對象是否可鏈到 GC Roots重新標記:引發 STW,為了修正並發標記期間,因用戶程序繼續運作而導致標記產生改變的標記。這個階段的停頓時間會被初始標記階段稍長,但比並發標記階段要短。並發清除:不引發 STW,正常運行,標記清除算法來清理刪除掉標記階段判斷的已經死亡的對象。總結:
並發標記和並發清除的耗時最長但是不需要停止用戶線程。
初始標記和重新標記的耗時較短,但是需要停止用戶線程,所以整個 GC 過程造成的停頓時間較短,大部分時候是可以和用戶線程一起工作的。
5.4、G1
之前的 GC 收集器對 Heap 的劃分:
以前垃圾回收器是新生代 + 老年代,用了 CMS 效果也不是很好,為了減少 STW 對系統的影響引入了 G1(Garbage-First Garbage Collector),G1 是一款面向服務端應用的垃圾收集器,具有如下特點:
並行與並發:G1 能充分利用多 CPU、多核環境下的硬體優勢,可以通過並發的方式讓 Java 程序繼續執行。分代收集:分代概念在 G1 中依然得以保留,它能夠採用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次 GC 的舊對象來獲得更好的收集效果。 空間整合:G1 從整體上看是基於標記-整理算法實現的,從局部(兩個 Region 之間)上看是基於複製算法實現的,G1 運行期間不會產生內存空間碎片。 可預測停頓:G1 比 CMS 牛在能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內,消耗在垃圾收集上的時間不得超過 N 毫秒。G1 作為 JDK9 之後的服務端默認收集器,不再區分年輕代和老年代進行垃圾回收。
G1 默認把堆內存分為 N 個分區,每個 1~32M(總是 2 的冪次方)。並且提供了四種不同 Region 標籤 Eden、Survivor 、Old、 Humongous。
H 區可以認為是 Old 區中一種特列專門用來存儲大數據的,關於 H 區數據存儲類型一般符合下麵條件:
當 0.5 Region <= 當對象大小 <= 1 Region 時候將數據存儲到 H 區
當對象大小 > 1 Region 存儲到連續的 H 區。
同時 G1 中引入了 RememberSets、CollectionSets 幫助更好的執行 GC 。
RememberSets:RSet 記錄了其他 Region 中的對象引用本 Region 中對象的關係,屬於 points-into 結構(誰引用了我的對象) CollectionSets:Csets 是一次 GC 中需要被清理的 regions 集合,注意 G1 每次 GC 不是全部 region 都參與的,可能只清理少數幾個,這幾個就被叫做 Csets。在 GC 的時候,對於 old -> young 和 old -> old 的跨代對象引用,只要掃描對應的 CSet 中的 RSet 即可。G1 進行 GC 的時候一般分為 Yang GC 跟 Mixed GC。
Young GC:CSet 就是所有年輕代裡面的 Region
Mixed GC:CSet 是所有年輕代裡的 Region 加上在全局並發標記階段標記出來的收益高的 Region
Yang GC
標準的年輕代 GC 算法,整體思路跟 CMS 中類似。
Mixed GC
G1 中是沒有 Old GC 的,有一個把老年代跟新生代同時 GC 的 Mixed GC,它的回收流程:
初始標記:是 STW 事件,其完成工作是標記 GC ROOTS 直接可達的對象。標記位 RootRegion。根區域掃描 :不是 STW 事件,拿來 RootRegion,掃描整個 Old 區所有 Region,看每個 Region 的 Rset 中是否有 RootRegion。有則標識出來。 並發標記 :同 CMS 並發標記不需要 STW,遍歷範圍減少,在此只需要遍歷第二步被標記到引用老年代的對象 RSet。 最終標記 :同 CMS 重新標記會 STW ,用的 SATB 操作,速度更快。清除 :STW 操作,用 複製清理算法,清點出有存活對象的 Region 和沒有存活對象的 Region(Empty Region),更新 Rset。把 Empty Region收集起來到可分配 Region 隊列。回收總結:
經過 global concurrent marking,collector 就知道哪些 Region 有存活的對象。並將那些完全可回收的 Region(沒有存活對象)收集起來加入到可分配 Region 隊列,實現對該部分內存的回收。對於有存活對象的 Region,G1 會根據統計模型找處收益最高、開銷不超過用戶指定的上限的若干 Region 進行對象回收。這些選中被回收的 Region 組成的集合就叫做 collection set,簡稱 Cset! 在MIX GC 中的 Cset = 所有年輕代裡的 region + 根據 global concurrent marking 統計得出收集收益高的若干 old region。 在 YGC 中的 Cset = 所有年輕代裡的 region + 通過控制年輕代的region 個數來控制 young GC 的開銷。 YGC 與 MIXGC 都是採用多線程複製清理,整個過程會 STW。G1 的低延遲原理在於其回收的區域變得精確並且範圍變小了。G1 提速點:
重新標記時 X 區域直接刪除。Rset 降低了掃描的範圍,上題中兩點。 重新標記階段使用 SATB 速度比 CMS 快。 清理過程為選取部分存活率低的 Region 進行清理,不是全部,提高了清理的效率。總結:
就像你媽讓你把自己臥室打掃乾淨,你可能只把顯眼而比較大的垃圾打掃了,犄角旮旯的你沒打掃。關於 G1 還有很多細節其實沒看到。
一句話總結 G1 思維:每次選擇性的清理大部分垃圾來保證時效性跟系統的正常運行。
New 個對象
一個 Java 類從編碼到最終完成執行,主要包括兩個過程:編譯、運行。
編譯:將我們寫好的.java 文件通過 Javac 命令編譯成.class 文件。
運行:把編譯生成的.class 文件交由 JVM 執行。
Jvm 運行 class 類的時候,並不是一次性將所有的類都加載到內存中,而是用到哪個就加載哪個,並且只加載一次。
6.1、類的生命周期
加載
加載指的是把 class 字節碼文件從各個來源通過類加載器裝載入內存中,這裡有兩個重點:
字節碼來源:一般的加載來源包括從本地路徑下編譯生成的.class 文件,從 jar 包中的.class 文件,從遠程網絡,以及動態代理實時編譯類加載器:一般包括啟動類加載器,擴展類加載器,應用類加載器,以及用戶的自定義類加載器(加密解密那種)。驗證
主要是為了保證加載進來的字節流符合虛擬機規範,不會造成安全錯誤。文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。
準備
給類靜態變量分配內存空間,僅僅是分配空間,比如 public static int age = 14,在準備後 age = 0,在初始化階段 age = 14,如果添加了 final 則在這個階段直接賦值為14。
解析
將常量池內的符號引用替換為直接引用。
初始化
前面在加載類階段用戶應用程式可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。此時才是真正開始執行類中定義的代碼 :執行 static 代碼塊進行初始化,如果存在父類,先對父類進行初始化。
使用
類加載完畢後緊接著就是為對象分配內存空間和初始化了:
為對象分配合適大小的內存空間為實例變量賦默認值設置對象的頭信息,對象 hash 碼、GC 分代年齡、元數據信息等執行構造函數(init)初始化。卸載
最終沒啥說等,就是通過 GC 算法回收對象了。
6.2、對象佔據字節
關於對象頭問題在 Synchronized 一文中已經詳細寫過了,一個對象頭包含三部分對象頭(MarkWord、classPointer)、實例數據Instance Data、對齊Padding,想看內存詳細佔用情況 IDEA 調用 jol-core 包即可。
問題一:new Object()佔多少字節
markword 8位元組 + classpointer 4 字節(默認用calssPointer壓縮) + padding 4位元組 = 16 字節如果沒開啟 classpointer 壓縮:markword 8 字節 + classpointer 8 字節 = 16 字節問題二:User (int id,String name) User u = new User(1,"李四")
markword 8位元組 + 開啟classPointer壓縮後classpointer 4 字節 + instance data int 4 字節 + 開啟普通對象指針壓縮後 String4 字節 + padding 4 = 24 字節
6.3、對象訪問方式
使用句柄:使用句柄來訪問的最大好處就是 reference 中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而 reference 本身不需要修改。
直接指針:reference 中存儲的直接就是對象地址。最大好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象的訪問在 Java 中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。
Sun HotSpot 使用直接指針訪問方式進行對象訪問的。
對象一定創建在堆上嗎
結論:不一定,看對象經過了逃逸分析後發現該變量只是用到方法區時,則 JVM 會自動優化,在棧上創建該對象。
7.1、逃逸分析
逃逸分析(Escape Analysis)簡單來講就是:Java Hotspot 虛擬機可以分析新創建對象的使用範圍,並決定是否在 Java 堆上分配內存。
7.2、標量替換
標量替換:JVM 通過逃逸分析確定該對象不會被外部訪問。那就通過將該對象標量替換分解在棧上分配內存,這樣該對象所佔用的內存空間就可以隨棧幀出棧而銷毀,就減輕了垃圾回收的壓力。
標量:不可被進一步分解的量,而 JAVA 的基本數據類型就是標量
聚合量:在 JAVA 中對象就是可以被進一步分解的聚合量。
7.3、棧上分配
JVM 對象分配在堆中,當對象沒有被引用時,依靠 GC 進行回收內存,如果對象數量較多會給 GC 帶來較大壓力,也間接影響了應用的性能。
為了減少臨時對象在堆內分配的數量,JVM 通過逃逸分析確定該對象不會被外部訪問。那就通過將該對象標量替換分解在棧上分配內存,這樣該對象所佔用的內存空間就可以隨棧幀出棧而銷毀,就減輕了垃圾回收的壓力。
7.4、同步消除
同步消除是 java 虛擬機提供的一種優化技術。
通過逃逸分析,可以確定一個對象是否會被其他線程進行訪問,如果對象沒有出現線程逃逸,那該對象的讀寫就不會存在資源的競爭,不存在資源的競爭,則可以消除對該對象的同步鎖。比如方法體內調用 StringBuffer。
逃逸分析結論:雖然經過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。
如果對象經過層層分析後發現 無法進行逃逸分析優化則反而耗時了,因此慎用。
類加載器
在連接階段一般是無法幹預的,大部分幹預類加載階段,對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在 Java 虛擬機中的唯一性,類加載時候重要三個方法:
loadClass() :加載目標類的入口,它首先會查找當前 ClassLoader 以及它的雙親裡面是否已經加載了目標類,找到直接返回 。findClass() :如果沒有找到就會讓雙親嘗試加載,如果雙親都加載不了,就會調用 findClass() 讓自定義加載器自己來加載目標類 。defineClass() :拿到這個字節碼之後再調用 defineClass() 方法將字節碼轉換成 Class 對象。8.1、雙親委派機制
定義:當某個類加載器需要加載某個.class 文件時,首先把這個任務委託給他的上級類加載器,遞歸這個操作,如果上級的類加載器沒有加載,自己才會去加載這個類。
作用:
可以防止重複加載同一個.class。通過委託去向上面問一問,加載過了,就不用再加載一遍。保證數據安全。 保證核心.class 不能被篡改,通過委託方式,不會去篡改核心.class。類加載器:
BootstrapClassLoader(啟動類加載器):c++編寫,加載 java 核心庫 java.*,JAVA_HOME/libExtClassLoader (標準擴展類加載器):java 編寫的加載擴展庫,JAVA_HOME/lib/extAppClassLoader(系統類加載器):加載程序所在的目錄,如 user.dir所在的位置的 ClassPathCustomClassLoader(用戶自定義類加載器):用戶自定義的類加載器,可加載指定路徑的 class 文件8.2、關於加載機制
雙親委派機制只是 Java 類加載的一種常見模式,還有別的加載機制哦,比如Tomcat 總是先嘗試去加載某個類,如果找不到再用上一級的加載器,跟雙親加載器順序正好相反。再比如當使用第三方框架 JDBC 跟具體實現的時候,反而會引發錯誤,因為 JDK 自帶的 JDBC 接口由啟動類加載,而第三方實現接口由應用類加載。這樣相互之間是不認識的,因此 JDK 引入了 SPI 機制,線程上下文加載器來實現加載(跟 Dubbo 的 SPI 不一樣哦)。
OOM 、CPU100%
系統性能分析常用指令:
9.1、OOM
為啥 OOM?:發生OOM 簡單來說可總結為兩個原因:
分配給 JVM 的內存不夠用。分配內存夠用,但代碼寫的不好,多餘的內存沒有釋放,導致內存不夠用。三種類型OOM
堆內存溢出:此種情況最常見 Java heap space。一般是先通過內存映像工具對 Dump 出來的堆轉儲快照,然後辨別到底是內存洩漏還是內存溢出。
內存洩漏
通過工具查看洩漏對象到 GC Roots 的引用鏈。找到洩漏的對象是通過怎麼樣的路徑與 GC Roots 相關聯的導致垃圾回收機制無法將其回收,最終比較準確地定位洩漏代碼的位置。
不存在洩漏
就是內存中的對象確實必須存活著,那麼此時就需要通過虛擬機的堆參數,從代碼上檢查是否存在某些對象存活時間過長、持有時間過長的情況,嘗試減少運行時內存的消耗。
虛擬機棧和本地方法棧溢出
在 HotSpot 虛擬機上不區分虛擬機棧和本地方法棧,因此棧容量只能由**-Xss**參數設定。在 Java 虛擬機規範中描述了兩種異常:
StackOverflowError :線程請求的棧深度超過了虛擬機所允許的最大深度,就會拋出該異常。
OutOfMemoryError:虛擬機在拓展棧的時候無法申請到足夠的空間,就會拋出該異常。
單線程環境下無論是由於棧幀太大還是虛擬機棧容量太小,當內存無法繼續分配的時候,虛擬機拋出的都是 StackOverflowError 異常。
多線程環境下為每個線程的棧分配的內存越大,每個線程獲得空間大則可建立的線程數減少了反而越容易產生 OOM 異常,因此,一般通過減少最大堆和減少棧容量來換取更多的線程數量。
永久代溢出:
PermGen space 即方法區溢出了。方法區用於存放 Class 的相關信息,如類名、訪問修飾符、常量池、欄位描述、方法描述等。當前的一些主流框架,如Spring、Hibernate,對於類進行增強的時候都會使用到 CGLib 這類字節碼技術,增強的類越多,就需要越大的方法區來保證動態生成 Class 可以加載入內存,這樣的情況下可能會造成方法區的 OOM 異常。
OOM 查看指令
通過命令查看對應的進程號:比如:jps 或者 ps -ef | grep 需要的任務輸入命令查看 gc 情況命令:jstat -gcutil 進程號刷新的毫秒數展示的記錄數,比如:jstat -gcutil 1412 1000 10(查看進程號 1412,每隔 1 秒獲取下,展示 10 條記錄)查看具體佔用情況:命令:jmap -histo 進程號 | more(默認展示到控制臺) 比如:jmap -histo 1412 | more 查看具體的classname,是否有開發人員的類,也可以輸出到具體文件分析
9.3 CPU 100%
線上應用導致 CPU 佔用 100%,出現這樣問題一般情況下是代碼進入了死循環,分析步驟如下:
找出對應服務進程 id:用 ps -ef | grep 運行的服務名字,直接 top 命令也可以看到各個進程 CPU 使用情況。查詢目標進程下所有線程的運行情況:top -Hp pid, -H 表示以線程的維度展示,默認以進程維度展示。對目標線程進行 10 進位到 16 進位轉換:printf 『%x\n』 線程 pid用 jstack 進程 id | grep 16 進位線程 id 找到線程信息,具體分析:jstack 進程 ID | grep -A 20 16 進位線程 id
GC 調優
一般項目加個 xms 和 xmx 參數就夠了。在沒有全面監控、收集性能數據之前,調優就是瞎調。
出現了問題先看自身代碼或者參數是否合理,畢竟不是誰都能寫 JVM 底層代碼的。一般要減少創建對象的數量,減少使用全局變量和大對象,GC 優化是到最後不得已才採用的手段。日常分析 GC 情況優化代碼比優化 GC 參數要多得多。一般如下情況不用調優的:
minor GC 單次耗時 < 50ms,頻率 10 秒以上。說明年輕代 OK。 Full GC 單次耗時 < 1 秒,頻率 10 分鐘以上,說明年老代 OK。GC 調優目的:GC 時間夠少,GC 次數夠少。
調優建議:
-Xms5m 設置 JVM 初始堆為 5M,-Xmx5m 設置 JVM 最大堆為 5M。-Xms 跟-Xmx 值一樣時可以避免每次垃圾回收完成後 JVM 重新分配內存。-Xmn2g:設置年輕代大小為 2G,一般默認為整個堆區的 1/3 ~ 1/4。- Xss 每個線程棧空間設置。-XX:SurvivorRatio,設置年輕代中 Eden 區與 Survivor 區的比值,默認=8,比值為 8:1:1。-XX:+HeapDumpOnOutOfMemoryError 當 JVM 發生 OOM 時,自動生成 DUMP 文件。-XX:PretenureSizeThreshold 當創建的對象超過指定大小時,直接把對象分配在老年代。-XX:MaxTenuringThreshold 設定對象在 Survivor 區最大年齡閾值,超過閾值轉移到老年代,默認 15。開啟 GC 日誌對性能影響很小且能幫助我們定位問題,-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:gc.log