1.JVM 的組成#
- 類加載器:加載.class 文件進入 jvm 內存
- 執行時數據區:主要由程序計數器、虛擬機棧、本地方法棧、方法區、堆組成
- 執行引擎:對 JVM 指令進行解析,翻譯成機器碼,解析完成後提交到操作系統中
- 本地庫接口:供 java 調用的融合了不同開發語言的原生庫
- 本地方法庫:Java 本地方法的具體實現
2. 類加載器#
2.1 分類
- 根加載器:加載本地方法類
- 擴展加載器:加載 jdk 內部實現的擴展類
- 系統加載器:加載程序中的類文件
2.2 雙親委派機制
加載一個 class 文件至 jvm 內存中時,子類加載器會將加載請求派送至父類加載器,由父類加載器首先進行加載,如果父類加載器無法加載 class 文件則由子類加載器加載。
系統加載器擴展加載器根加載器
3. 執行引擎#
3.1 翻譯#
- 即時編譯器:對於程序中的熱點代碼,即時編譯器會保存對應代碼的機器碼,在後續遇到相同代碼的時候使用保存的機器碼進行替代。
- 字節碼解釋器:將字節碼解釋成機器碼
4. 運行時數據區#
4.1 堆#
4.1.1 組成部分
- 新生代:包含 eden、survivor(to、from)兩個區
- 老年代
4.1.2 垃圾回收機制
如何判斷垃圾
- 引用計數法:為每一個對象分配一個引用值,表示該對象被引用的次數,會造成循環引用問題。
- 可達性分析:從 gc roots 開始向下遍歷,沒有被遍歷到的對象則是垃圾;
gc roots 包括虛擬機棧中棧幀引用的局部變量,本地方法棧中引用的局部變量,方法區中類的靜態變量,方法區中常量池中的變量以及 synchronized 加鎖對象。
如何回收垃圾
標記清除算法、標記複製算法、標記整理算法、分代收集算法
分類
新生代垃圾回收器:serial、parallel new、parallel scavenge
老年代垃圾回收器:serial old、parallel old、cms
混合:G1
什麼時候開啟垃圾回收
- young GC
當 young gen 中的 eden 區分配滿的時候觸發。注意 young GC 中有部分存活對象會晉升到 old gen,所以 young GC 後 old gen 的佔用量通常會有所升高。 - major GC
當老年代區達到一定閾值的時候進行(一般只有 cms 才執行 major gc?) - full GC
- 當創建一個大對象,Eden 區域當中放不下這個大對象,會直接保存在老年代當中,如果老年代空間也不足,就會觸發 Full GC
- 當元空間滿的時候,也會出發 Full GC
- 在新生代回收內存時,由 Eden 區和 Survivor From 區把存活的對象向 Survivor To 區複製時,對象大小大於 Survivor To 空間的可用內存,則把多出的對象轉存到老年代 (這個過程稱為分配擔保);這個時候老年代的可用內存小於該對象大小,則會判斷是否允許擔保失敗;如果允許,那麼就會判斷老年代最大可用連續空間是否大於歷次晉升到老年代的對象大小,如果大於,則嘗試一次 young gc;否則,便會觸發 Full GC
- 調用 System.gc ()
CMS
- 三色標記法
- 主要流程
- 初始標記:會 stw,對老年區中的 gc roots 以及年輕區中直接指向的老年區對象標記為灰色。
- 並發標記:不會 stw,對標記為灰色的對象進行遍歷,遍歷的對象標記為灰色,入口對象標記為黑色,重複以上步驟。
- 並發預處理:不會 stw,對並發標記中產生的 dirty card 進行重新標記。
- 重新標記:會 stw,我認為這裡主要是解決錯檢問題,cms 會設置一個寫屏障將修改了內部引用的對象重新標記為灰色,重新標記對這些灰色對象進行遍歷,防止錯檢。
- 並發清理:不會 stw,清除所有白色對象。
- 缺點
- 佔用 cpu 資源
- 產生浮動垃圾:並發清理階段用戶仍在產生垃圾,需要下次 gc 進行清理
- 內存碎片:CMS 採用標記清除算法,會產生大量內存碎片
- 並發失敗:由於浮動垃圾的存在,因此 CMS 必須預留一部分空間來裝載這些新產生的垃圾。CMS 不能像 Serial Old 收集器那樣,等到 Old 區填滿了再來清理。在 JDK5 時,CMS 會在老年代使用了 68% 的空間時激活,預留了 32% 的空間來裝載浮動垃圾,這是一個比較偏保守的配置。如果實際引用中,老年代增長的不是太快,可以通過
-XX:CMSInitiatingOccupancyFraction
參數適當調高這個值。到了 JDK6,觸發的閾值就被提升至 92%,只預留了 8% 的空間來裝載浮動垃圾。如果 CMS 預留的內存無法容納浮動垃圾,那麼就會導致「並發失敗」,這時 JVM 不得不觸發預備方案,啟用 Serial Old 收集器來回收 Old 區,這時停頓時間就變得更長了。
G1
- 分區
G1 採用了分區 (Region) 的思路,將整個堆空間分成若干個大小相等的內存區域,每次分配對象空間將逐段地使用內存。因此,在堆的使用上,G1 並不要求對象的存儲一定是物理上連續的,只要邏輯上連續即可;每個分區也不會確定地為某個代服務,可以按需在年輕代和老年代之間切換。啟動時可以通過參數 - XX=n 可指定分區大小 (1MB~32MB,且必須是 2 的冪),默認將整堆劃分為 2048 個分區。
在 G1 中,還有一種特殊的區域,叫 Humongous 區域。 如果一個對象佔用的空間超過了分區容量 50% 以上,G1 收集器就認為這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1 劃分了一個 Humongous 區,它用來專門存放巨型對象。如果一個 H 區裝不下一個巨型對象,那麼 G1 會尋找連續的 H 分區來存儲。為了能找到連續的 H 區,有時候不得不啟動 Full GC。 - 對象分配策略
- TLAB (Thread Local Allocation Buffer) 線程本地分配緩衝區:
TLAB 為線程本地分配緩衝區,它的目的為了使對象盡可能快的分配出來。如果對象在一個共享的空間中分配,我們需要采用一些同步機制來管理這些空間內的空閒空間指針。在 Eden 空間中,每一個線程都有一個固定的分區用於分配對象,即一個 TLAB。分配對象時,線程之間不再需要進行任何的同步。 - Eden 區中分配:
對 TLAB 空間中無法分配的對象,JVM 會嘗試在 Eden 空間中進行分配。如果 Eden 空間無法容納該對象,就只能在老年代中進行分配空間。 - Humongous 區分配
-
young gc 過程
- 初始標記:會 stw,標記所有的 gc roots 直接關聯的對象(即,gc roots 本身)
- 處理 & 更新 rset:處理 RSet 的信息並且掃描,將老年代對象持有年輕代對象的相關引用都加入到 GC Roots 下
- 清理階段:stw,打包所有 young region 為 cset,如果預測回收時間小於給定的容忍時間,那麼 g1 會增加 eden 區的 region 數,不進行清理;否則,如果預測回收時間大於給定容忍時間,那麼 g1 會執行標記複製操作,將所有存活對象複製到空的 survivor 中。
-
mixed gc 過程
-
初始標記:該過程是和 young GC 的暫停過程一起的
-
根區域掃描
-
並發標記階段:出現了引用修改(不包含新分配內存給對象),那麼寫屏障會把這些引用的原始值捕獲下來,記錄在 log buffer 中。而後再處理。後續的所有的標記,都是從原來的值出發,而不是從新的值出發的。處理新創建的對象,G1 採用了不同的方式。G1 用了兩個 TAMS 變量了判斷新創建的對象。一個叫做 previous TAMS,一個叫做 next TAMS。位於兩者之間的對象就是新分配的對象。
-
重新標記:stw,處理每個線程遺留的 SATB write barrier 的對象引用。
-
清理階段