滿滿的一整篇,全是 JVM 核心知識點!

2021-01-10 CSDN

頭圖 | 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

相關焦點

  • 化學「必修一」超全知識點整理,滿滿的乾貨!
    高一馬上要結束了,很多同學對高中化學必修一的內容還是很模糊,「必修一」在整個高中學習中佔有重要的地位,如果你必修一都學不會,那在以後的化學學習中就想掉進「坑裡」,無法翻身。高中化學必修一在高考中佔的分值也比較大,不要等到高三總複習回頭撿高一的內容,會耽誤你整個總複習的進度,而且效果不好。建議在高一、高二把所學的知識點吃透,等到高三總複習時,你會很輕鬆的查漏補缺,快速上分。
  • 程式設計師:深入理解JVM,從JVM層面來講Java多態
    我們使用javap -c DynamicDispatch.class 命令輸出這段代碼的字節碼:Compiled from "DynamicDispatch.java"public class jvm.DynamicDispatch {public jvm.DynamicDispatch
  • 2020考研數學高數要掌握的核心知識點梳理:微分方程
    2020考研數學高數要掌握的核心知識點梳理:微分方程 2019-05-01 14:51:26| 來源:廣東考研信息
  • 依法治國的核心是什麼 高中政治知識點總結
    依法治國的核心是什麼 高中政治知識點總結 2019-04-29 19:30:59 來源:網絡資源
  • 2020考研數學高數要掌握的核心知識點梳理:多元函數微分學
    2020考研數學高數要掌握的核心知識點梳理:多元函數微分學 考研數學複習備考的初期,基礎知識是複習的重點,為了幫助大家更好的進行基礎知識的積累,以下是中公考研小編整理的關於「2020考研數學高數要掌握的核心知識點梳理:多元函數微分學」相關資訊文章,一起關注一下吧~
  • 2020考研數學高數要掌握的核心知識點梳理:級數
    2020考研數學高數要掌握的核心知識點梳理:級數 2019-05-01 14:52:04| 來源:廣東考研信息
  • 高中生物常見易錯知識點最全總結
    高中生物常見易錯知識點最全總結高中生物是理科生高考中必考的科目,高中生物複習時必背的小知識點有哪些,下面有途網小編給大家整理了高中生物常見易錯知識點最全總結,希望對你有幫助。
  • 高中生物複習小知識點最全總結
    高中生物複習小知識點最全總結高中生物是理科生高考中必考的科目,高中生物複習時必背的小知識點有哪些,下面有途網小編給大家整理了高中生物複習小知識點最全總結,希望對你有幫助。
  • 高中生物易錯小知識點最全總結
    高中生物易錯小知識點最全總結高中生物是理科生高考中必考的科目,高中生物複習時必背的小知識點有哪些,下面有途網小編給大家整理了高中生物易錯小知識點最全總結,希望對你有幫助。
  • 高中化學必修一最全知識點,清北學霸總結,幫你搞定化學
    高中化學是理科生學習中很重要的一科,不過化學考試內容裡需要硬性記憶的知識點實在是太多了,很容易遺漏,對於高一的學子們一定要知道自己的弱項和失分項,才能更好的學好化學。為了能讓同學們掌握自己的化學學習情況,我們清北學霸們整理了一份高中化學必修一最全知識點,涵蓋了高一必考的化學公式,知識點總結,實驗原理。同學們趕快拿筆記本記錄下來,查缺補漏,爭取在高一將化學拿下,考試的時候得高分。
  • 腦血管解剖思維導圖 滿滿的乾貨!
    5.大腦前動脈的分段、質支和中央支、臨床知識點6.大腦中動脈的分段、中央支和皮質支、臨床知識點本文整理自鄭州大學第二附屬醫院焦義明主治醫師的《腦血管解剖(一)原標題:腦血管解剖思維導圖,滿滿的乾貨!
  • 備戰期末|人教版高中生物必修一/二/三最全知識點匯總!可列印!
    給各位同學整理了一套人教版高中生物必修一,二,三冊的全部知識點匯總。生物這門課偏文科,很多知識點只要牢記概念和公式,基本都能得出正確答案。對於選修科目選生物的高中生,這個階段對於高中全冊的知識點的「熟練於心」就顯得特別重要!
  • 三題直指初二物理核心知識點!期末必考!中考必考!
    初二物理核心知識點是什麼?如果我直接這麼問初二的同學們,有多少同學能回答的比較準確呢?所謂的核心知識點,就是能夠概括前面所學知識,並且屬於中考必考、重要的知識點!初二物理的核心知識點主要包括「三種性質的力」、「壓強」、「浮力」、「簡單機械」、「功和功率」!在這些核心知識點所構成的題中,基本概括了初二物理的大部分內容,也在中考物理試卷上佔據了獨一無二的老大位置!
  • 第四篇:C語言中指針與字符串核心知識點梳理
    這就涉及到本文要講到的第一個核心概念:指針。重點包括:指針處理一維數組、動態內存分配等。C語言的基本數據類型中有一個char的關鍵詞,可以存儲單個的字符。那麼,像漢字以及由多個字符組成的內容,又該如何存儲呢?
  • 讀一讀英文版《半生緣》!我是如何讀英文書的?
    我平時比較忙,確實不像學生時期那樣可以細品一本書。現在我看書更多是為了提升英語和翻譯水平,當然文字背後的故事和文化也是非常引人入勝的。就拿我現在看的《半生緣》來說吧,我是這樣閱讀的:第一章節我先看完中文,再看英文,看英文的時候,有生詞或不太好理解的地方我還是會查,包括查單詞和再看看中文,這樣看也還是比較慢,但是收穫很大。
  • 文科高中生物會考必考知識點最全總結
    文科高中生物會考必考知識點最全總結在高中生物學習過程中,由於課程的信息量驟然增加,使複習難度加大,很多學生在複習階段由於複習不得法而導致基礎知識依然有欠缺,能力也沒得到相應的提升,學習成績沒有提高反而下降,最終導致高考成績很不理想。下面有途網小編給大家分享一下文科高中生物會考必考知識點最全總結,歡迎閱讀。
  • 高中生物知識點最全整理 你想要的都在這
    高中生物知識點最全整理 你想要的都在這下面是有途網小編為即將進入高三總複習的同學整理的高中生物知識點,希望能對大家有所幫助。高中生物知識點最全整理1、染色體組型:也叫核型,是指一種生物體細胞中全部染色體的數目、大小和形態特徵。觀察染色體組型最好的時期是有絲分裂的中期。
  • 清華老教授總結:高中生物177個核心知識點大匯總!高考複習必備
    高中生物是理科中最需要背誦的一科,學好生物,要掌握基本的生物知識點。而高中三年的生物知識內容複雜繁多,這給很多考生造成了很大的壓力。其實,學習生物,不僅僅需要平時的背誦記憶,還需要掌握正確的學習方法,好的學習方法勝過無數個日夜的死記硬背。
  • 【知識點】最全初三全冊化學知識點總結,收藏!
    原標題:【知識點】最全初三全冊化學知識點總結,收藏! 臨近考試,化學的知識點太多,不知如何記,感覺身體被掏空了?小編為大家整理了有針對性的初三化學考點知識點考試肯定能用到!
  • 2021年中考物理知識點:全反射現象及條件
    中考網整理了關於2021年中考物理知識點:全反射現象及條件,希望對同學們有所幫助,僅供參考。   全反射現象及條件   1.定義:光從光密介質射入光疏介質,當入射角增大到某一角度時,折射光線將消失,只剩下反射光線的現象.   2.條件:   (1)光從光密介質射入光疏介質.