標籤:類資訊 傳統 nbsp out frame 環境 動態對象 情況 預設
第一章:走進Java概述Java技術體系Java發展史Java虛擬機器發展史
- 1996年 JDK1.0,出現Sun Classic VM
- HotSpot VM, 它是 Sun JDK 和 OpenJDK 中所帶的虛擬機器,最初並不是Sun開發
- Sun Mobile- Embedded VM/ Meta- Circular VM
- BEA JRockit/ IBM J9 VM JRockit曾號稱世界上最快的java虛擬機器,BEA公司發布.J9屬於IBM主要扶持的虛擬機器
- Azul VM/ BEA Liquid VM 我們平時所提及的“ 高效能Java虛擬機器” 一般 是指 HotSpot、 JRockit、 J9這類在通用平台上啟動並執行商用虛擬機器,但其實 Azul VM和 BEA Liquid VM 這類特定硬體平台專有的虛擬機器才是“ 高效能” 的武器。
- Apache Harmony/ Google Android Dalvik VM
- Microsoft JVM 及其他
展望JAVA技術的未來
- 模組化
- 混合語言
- 多核並行
- 進一步豐富文法
- 64位虛擬機器
第二章:Java記憶體地區與記憶體溢出異常JVM運行時的資料區
共有五個,線程共用的有 堆(Heap)、方法去(Method Area), 線程私人的有 虛擬機器棧(VM Stack)、本地方法棧(Native Method Stack)和程式計數器。
- 程式計數器
程式計數器(Program Counter Register) 是一 塊 較小的記憶體空間,它可以看作是當前線程所執行的位元組碼的行號 指標。 如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的 虛擬機器位元組碼指令的地址; 如果正在執行的是 Native 方法,這個 計數器值則為空白( Undefined)。 此記憶體地區是唯一一個在 Java虛擬 機規範中沒有規定任何OutOfMemoryError情況的地區。
- Java虛擬機器棧
與程式計數器一樣,虛擬機器棧也是線程私人的,它的 生命週期與線程相同。虛擬機器棧描述的是Java方法執行 的記憶體模型:每個方法在執行的同時都會建立一個棧 幀( Stack Frame) 用於儲存 局部 變 量表、操 作數棧、動態 連結、方法出口等資訊。 每一個方法 從調用直至執行完成的過程,就對應著一個棧幀在虛擬 機棧中入棧到出棧的過程。局部變數表存放了編譯期可知的各種基礎資料型別 (Elementary Data Type)(boolean、byte、char、short、int、float、long、double)、 對象引用和returnAddress 類型。
在 JAVA 虛擬機器規範中, 對這個地區規定了兩種異常 狀況: 如果線程請求的棧深度大於虛擬機器所允許的 深度, 將拋出StackOverflowError 異常;如果虛擬機器 棧可以動態擴充(當前大部分的Java虛擬機器都可動態 擴充, 只不過Java虛擬機器規範中也允許固定長度的虛擬 機 棧),如果擴充時無法申請到足夠的記憶體,就會拋出OutOfMemoryError 異常。
- 本地方法棧
本地方法棧與虛擬機器棧所發揮的作用是非常相似的, 它們之間的區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是 位元組 碼) 服務,而本地方法棧則為虛擬機器使 用到的Native 方法服務。
與 虛擬 機 棧 一樣,本地方法棧地區也會拋出 StackOverflowError 和 OutOfMemoryError異常。
- Java堆 Java堆是虛擬機器所管理的記憶體中最大的一塊,是 被所有線程共用的一塊記憶體地區,在虛擬機器啟動時建立。此記憶體地區的唯一目的就是存放對象執行個體,幾乎所有的對象執行個體 都在這裡分配記憶體。
Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“ GC 堆”(Garbage Collected Heap)。
從記憶體回收的角度來看,由於現在收集器 基本都採用分代收集演算法,所以Java堆中還可以細分為: 新生代和老年代; 再細緻 一點的有Eden空間、From Survivor 空間、 To Survivor 空間等。
根據Java虛擬機器規範的規定, Java堆可以 處於物理上不連續的記憶體空間中,只要邏輯上 是連續的即可,就像我們的磁碟空間一樣。在實現時, 既可以實現成固定大小的,也可以是可擴充的,不過當前主流的虛擬機器都是按照可擴充來實現的(通過- Xmx 和- Xms 控制)。如果在堆中沒有記憶體完成執行個體分配, 並且堆也無法再擴充時,將會拋出OutOfMemoryError異常。
- 方法區 方法區用於儲存已被虛擬機器載入的類資訊、 常量、靜態 變數、即時編譯器編譯後的 代碼等資料。 雖然 JAVA 虛擬機器規範把 方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non- Heap( 非 堆),目的應該是與堆區分開來。
很多人都更願意把方法區稱為“ 永久代”( Permanent Generation),本質上兩者並不 等價,等價, 僅僅是因為 HotSpot 虛擬 機 的設計團隊選擇把 GC分代收集擴充至 方法 區, 或者說 使用 永久代來實現方法區 而已, 這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分記憶體,能夠省去專門為方法區編寫記憶體管理代碼的工作,並非就如永久帶就是就是進入了方法區。(Java 8已經將移除了永久代更改為中繼資料區)
- 運行時常量池
Runtime Constant Pool, 該地區屬於方法區的一部分。 Class 檔案中除了有類的版本、欄位、方法、介面等 描述資訊外,常量池,用於存放編譯期產生的各種字面量和符號引用,這部分內容將在類載入後進入方法區的運行時常量池中存放。
運行時常量池相對於Class 檔案常量池的 另外 一個重要特徵是具備動態性,Java語言並不 要求常量一定只有編譯期才產生,也就是並非 預置入Class檔案中常量池的內容才能進入 方法區運行時 常量 池, 運行 期間 也可 能將 新的 常量 放入 池 中, 這種 特性 被 開發 人員 利用 得比 較多 的 便是 String 類 的 intern() 方法。 當常量池無法 再申請到記憶體時會拋出 OutOfMemoryError 異常。
HotSpot虛擬機器對象探秘
以Java堆為例,探訪對象的建立、記憶體布局和定位。
- 對象的建立
- 虛擬機器遇到一條new指令時, 首先 將去 檢查 這個 指令 的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、解析和初始化過。 如果沒有,那必須先執行相應的類載入過程。
- 通過類載入之後,從Java堆中划出一塊記憶體給新生的對象。記憶體的分配方式有多種,由Java堆是否規整決定,最終由採用的垃圾收集器決定。
- 在分配記憶體是考慮到並發和線程問題,除了同步處理保證原子性之外,有一種方法是本地線程分配緩衝(Thread Local Allocation Buffer, TLAB):是把記憶體配置 的動作按照線程劃分在不同的空間之中進行,哪個 線程 要 分配 記憶體, 就在哪個線程的TLAB上分配, 只有 TLAB 用完並分配新的TLAB 時, 才需要同步鎖定。 虛擬機器是否使用TLAB,可以通過- XX:+/- UseTLAB 參數來設定。
- 將記憶體空間初始化零值,如果使用了TLAB則可以在分配時初始化。
- 對對象進行必要的設定, 例如這個對象是哪個類的 執行個體、 如何才能找到類的中繼資料資訊、對象的雜湊碼、對象的GC分代年齡等資訊。這些資訊存放在對象的對象頭( Object Header)之中。
對象的記憶體布局
- 在 HotSpot 虛擬機器中,對象在記憶體中儲存的布局可以分為3塊地區: 對象頭( Header)、執行個體資料( Instance Data)和對齊填充( Padding)。
- 對象頭:兩部分:第一部分用於儲存物件自身的運行時資料, 如雜湊碼(HashCode)、 GC分代年齡、鎖狀態 標誌、線程持有的鎖、偏向線程ID、偏向時間戳記等,32位和64位虛擬機器中中分別為32位和64位,官方成為“Mark Word”。第二部分類型指標,即對象指向它中繼資料的指標,虛擬機器通過指標來確定是哪個類的執行個體。
- 執行個體部分試對象真正儲存的有效資訊,程式中各種類型的欄位內容。
- 對其填充並不是必然存在的,僅僅起著預留位置的作用。保證對象的大小是8位元組的倍數。
對象的訪問定位
Java程式需要通過棧上的reference資料來操作堆上的 具體對象。 由於reference類型在Java虛擬機器規範中只 規定了一個指向對象的引用,並沒有定義這個引用應該 通過何種方式去定位、訪問堆中的對象的具體 位置, 所以對象訪問方式也是取決於虛擬機器實現而定 的。 目前 主流的訪問方式有使用控制代碼和直接指標兩種。
- 控制代碼:那麼 Java 堆 中將 會 劃分 出 一塊 記憶體 來作為控制代碼池, reference中儲存的就是對象的控制代碼 地址, 而控制代碼中包含了對象執行個體資料與類型資料 各自的具體地址資訊。
- 直接指標:如果使用直接指標訪問, 那麼Java 堆 對象的布局中就必須考慮如何放置訪問類型 資料的相關資訊, 而reference中儲存的直接就是 對象地址。
- 這兩種對象訪問方式各有優勢, 使用控制代碼來訪問的 最大好處就是reference中儲存的是穩定的控制代碼地址,在 對象被移動(垃圾收集時移動對象是非常普遍的行為) 時只會改變控制代碼中的執行個體資料指標,而reference本身 不需要修改。使用直接指標訪問方式的最大好處就是 速度更快,它節省了一次指標定位的時間開銷,由於 對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。就本書討論的主要虛擬機器Sun HotSpot而言,它是使用第二種方式進行對象訪問的,但從整個軟體開發的範圍來看,各種 語言和架構使用控制代碼也很很常見。
實戰 OutOfMemoryError異常
- Java堆溢出 Java堆用於儲存物件執行個體,只要不斷的建立對象並且保證GC Roots 到 對象之間有可達途徑保證不被來及回收這些對象,就可以造成堆記憶體溢出。
- 虛擬機器棧和本地方法棧溢出 對於 HotSpot 來說, 雖然- Xoss 參數( 設定 本地 方法 棧 大小) 存在, 但 實際上 是 無效 的, 棧 容量 只 由- Xss 參數 設定。
如果 線程 請求 的 棧 深度 大於 虛擬 機 所 允許 的 最大 深度, 將 拋出 StackOverflowError 異常。
如果 虛擬 機 在 擴充 棧 時 無法 申請 到 足夠 的 記憶體 空間, 則 拋出 OutOfMemoryError 異常。
假如一台 電腦記憶體有2G,JVM提供參數來控制Java 堆 和 方法 區 的 這 兩部分 記憶體 的 最大值。 剩餘 的 記憶體 為 2GB( 作業系統 限制) 減去 Xmx( 最大 堆 容量), 再 減去 MaxPermSize( 最大 方法 區 容量), 程式 計數器 消耗 記憶體 很小, 可以 忽略 掉。 如果 虛擬 機 進程 本身 耗費 的 記憶體 不計 算在 內, 剩下 的 記憶體 就 由 虛擬 機 棧 和 本地 方法 棧“ 瓜分” 了。 每個 線程 分配 到 的 棧 容量 越大, 可以 建立 的 線程 數量 自然 就 越少, 建立 線程 時 就 越 容易 把 剩下 的 記憶體 耗盡。
- 方法區和常量池溢出 String. intern()是一個Native方法,它的作用是:如果 字串常量池中已經包含一個等於此String對象的字串, 則返回代表池中這個字串的String對象;否則,將此String對象包含的字串添加到常量池中,並且返回 此String對象的引用。 在JDK 1. 6及之前的版本中, 由於常量池分配在永久代內,我們可以通過- XX:PermSize 和- XX:MaxPermSize 限制方法區大小, 從而間接限制其中常量池的容量。
- 本機直接記憶體溢出 DirectMemory容量可通過- XX: MaxDirectMemorySize 指定, 如果不指定, 則預設與 Java堆最大值(- Xmx 指定)一樣。
第三章:垃圾收集器與記憶體配置策略
垃圾收集(Garbage Collection,GC),最開始誕生於1960的Lisp.
程式計數器、虛擬機器棧、本地方法棧隨線程而生,隨線程而滅,這三個地區不考慮記憶體回收。Java堆和方法區是主要進行記憶體回收的地區。
判斷對象是否已經死亡
- 引用計數演算法(Reference Counting)
給對象中添加一個引用計數器, 每當有一個地方引用它時, 計數器值就加1; 當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。
優點:判定效率高 缺點:無法解決對象之間相互循環參考的問題。
舉個 簡單 的 例子, 請看 代碼 清單 3- 1 中的 testGC() 方法: 對象 objA 和 objB 都有 欄位 instance, 賦值 令 objA. instance= objB 及 objB. instance= objA, 除此之外, 這 兩個 對象 再無 任何 引用, 實際上 這 兩個 對象 已經 不可能 再被 訪問, 但是 它們 因為 互相 引用 著 對方, 導致 它們 的 引用 計數 都不 為 0, 於是 引用 計數 演算法 無法 通知 GC 收集 器 回收 它們.
可達性分析演算法(Reachability Analysis)這個演算法的基本思路就是通過一系列的稱為"GC Roots"的對象作為起始 點, 從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈( Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用 圖論的話來說,就是從GCRoots到這個對象不可達)時,則證明此對象是停用。
再談引用(引用的定義及分類) 在 JDK 1. 2 以前,Java中的引用的定義很傳統:如果 reference 類型的資料中儲存的數值代表的是另外一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。
在JDK 1. 2之後, Java對引用的概念進行了擴充,將引用分為強引用( Strong Reference)、軟引用( Soft Reference)、 弱引用( Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。
強引用:只要強引用還在永遠不會被記憶體回收,定義方式通常為 A a = new A().
軟引用:描述一些還有用但非必要的屬性。在系統將要發生記憶體溢出 異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果 這次回收還沒有足夠的記憶體, 才會拋出記憶體溢出異常。
軟引用:它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠, 都會回收掉只被弱 引用關聯的對象。
虛引用:也成為幽靈引用或者幻影引用,最弱。為一個對象設定虛引用關聯的唯一目的就是能在這個對象 被收集器回收時收到一個系統通知。
生存還是死亡
要真正宣告一個對象死亡,至少要經曆兩次標記過程:如果對象在進行 可達性分析後發現沒有與GCRoots相串連的引用鏈,那它將會被第一次標記並且進行一次篩選, 篩選的條件是此對象是否有必要執行 finalize() 方法。 當對象沒覆蓋finalize() 方法, 或者finalize() 方法已經被虛擬機器調用過,虛擬機器將這兩種情況都視為“ 沒有必要 執行”。 如果這個對象被判定為有必要執行 finalize()方法, 那麼 這個對象將會放置在一個叫做F-Queue的隊列之中,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer線程去執行它。
finalize()方法是對象逃脫死亡命運的最後一次機會。
回收方法區
JAVA 虛擬機器規範中確實說過可以不要求虛擬機器在方法區實現垃圾收集, 而且在方法區中進行垃圾收集的“ 性價比”一般比較低:在堆中,尤其是 在新生代中, 常規應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。
永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。 回收 廢棄常量與回收Java堆中的對象非常類似。
如何判斷一個類是否無用:
- 該類所有的執行個體都已經被回收,也就是Java堆中不存在該類的任何執行個體。
- 載入該類的ClassLoader已經被回收。
- 該類對應的java. lang. Class 對象沒有在任何地方被引用, 無法 在任何地方通過反射訪問該類的方法。
垃圾收集演算法3.1 標記-清除演算法
最基礎的收集演算法是“ 標記- 清除”( Mark- Sweep)演算法,如同它的 名字一樣, 演算法分為“ 標記” 和“ 清除” 兩個階段: 首先標記出 所有 需要回收的對象, 在標記完成後統一回收所有被標記的對象。
不足:它的主要不足有兩個: 一個是效率問題, 標記和清除兩個過程 的效率都不高;另一個是空間 問題, 標記清除之後會產生大量 不連續 的記憶體片段, 空間片段太多可能會導致以後在程式運行過程中需要 分配較大對象時, 無法找到足夠的連續記憶體而不得不提前觸發另一次 垃圾收集。
3.2 複製演算法
為瞭解決效率問題, 一種稱為“ 複製”( Copying) 的收集演算法出現 了, 它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體配置時也就不用考慮記憶體片段等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,運行高效。只是這種演算法的代價是記憶體縮小為了原來的一半,未免太高了一點。
現在的商業虛擬機器都採用這種收集演算法來回收新生代, IBM公司的專門研究表明,新生代中的對象98% 是“ 朝 生 夕 死” 的, 所以並不需要按照 1: 1 的比例來劃分 記憶體 空間,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。 當回收時,將Eden 和Survivor 中 還存活著的對象 一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的 Survivor 空間。HotSpot虛擬機器預設的Eden和Survivor的大小為8:1
缺點:在對象存活率比較高時就要進行較多的複製操作,效率會變低。
3.3 標記-整理演算法
(Mark-Compact)老年代一般採取該演算法。
演算法,標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動, 然後直接 清理掉端邊界以外的記憶體。
3.4 分代收集演算法
當前商業虛擬機器的垃圾收集都採用“ 分 代 收集”( Generational Collection)演算法,這種演算法並沒有什麼新的思想,只是根據對象存活周期的不同將記憶體劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製演算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“ 標記— 清理” 或者“ 標記— 整理” 演算法來進行回收。
HotSpot的演算法實現4.1枚舉根節點
在HotSpot的實現中, 是使用一組稱OopMap的資料結構 來達到這個目的的, 在類載入完成的時候, HotSpot 就把對象內什麼位移量上是什麼類型的資料計算出來, 在JIT編譯過程中,也會在特定的位置記錄下棧和 寄存器 中哪些位置是引用。這樣, GC在掃描時就可以直接得知 這些資訊了。
4.2 安全點
在 OopMap的協助下,HotSpot可以快速且準確地完成 GC Roots 枚舉,但一個很現實的問題隨之而來:可能導致 參考關聯性變化,或者說OopMap內容變化的指令非常多, 如果為每一條指令都產生對應的OopMap,那將會需要大量的額外空間, 這樣GC的空間成本將會變得很高。
實際上,HotSpot也的確沒有為每條指令都產生 OopMap, 前面已經 提到,只是 在“ 特定 的 位置” 記錄了 這些資訊, 這些位置稱為安全點( Safepoint),即 程式執行時並非在所有地方都能停頓下來開始 GC,只有 在到達安全點時才能暫停。
4.3 安全區域
Safepoint機制保證了程式執行時, 在不太長的時間內 就會遇到可進入 GC的Safepoint。
安全區域(Safe-Region)是指在一段程式碼片段之中,參考關聯性不會發生變化。在這個地區中的任意地方開始GC都是安全的。我們也可以把Safe-Region看做是被擴充了的Safepoint。
垃圾收集器
這裡討論的收集器基於JDK1.7Update14之後的HotSpot虛擬機器。
5.1 Serial收集器
Serial 收集器是最基本、發展曆史最悠久的收集器,曾經是虛擬機器新生代收集的唯一選擇。該收集器為單線程收集器,它在工作時會暫停其它線程。“Stop the world”指的就是這種情況。 優點:簡單而高效( 與其他收集器的單線程 比), 對於限定單個CPU的環境來說,Serial收集器由於沒有線程互動的開銷,專心做垃圾收集自然可以獲得最高的單線程收集。
5.2 ParNew收集器
ParNew收集器其實就是Serial收集器的多線程版本, 除了使用多條線程進行垃圾收集之外,其餘行為包括 Serial收集器可用的所有控制參數、收集演算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一樣, 在實現上,這兩種收集器也共用了相當多的代碼。
它是許多運行在Server模式下的虛擬機器中首選的新生代 收集器,其中有一個與效能無關但很重要的原因,原因 是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。
在 JDK 1. 5 時期, HotSpot 推 出了 一 款 在 強 互動 應用 中 幾乎 可 認為 有 劃時代 意義 的 垃圾 收集 器—— CMS 收集 器( Concurrent Mark Sweep, 本節 稍後 將 詳細 介紹 這 款 收集 器), 這 款 收集 器 是 HotSpot 虛擬 機中 第一 款 真正 意義上 的 並發( Concurrent) 收集 器, 它 第一次 實現 了 讓 垃圾 收集 線程 與 使用者 線程( 基本上) 同時 工作, 用 前面 那個 例子 的 話來 說, 就是 做 到了 在 你的 媽媽 打掃 房間 的 時候 你 還能 一邊 往 地上 扔 紙屑。 不幸 的 是, CMS 作為 老 年代 的 收集 器, 卻 無法 與 JDK 1. 4. 0 中 已經 存在 的 新生代 收集 器 Parallel Scavenge 配合 工作[ 1], 所以 在 JDK 1. 5 中 使用 CMS 來 收集 老 年代 的 時候, 新生代 只能 選擇 ParNew 或者 Serial 收集 器 中的 一個。
5.3 Parallel Scavenge收集器
Parallel Scavenge收集器是一個新生代收集器,它也是使用複製演算法的收集器,又是並行的多線程收集器。
Parallel Scavenge收集器的特點是它的關注點與其他收集器不同, CMS等收集器的關注點 是儘可能地縮短垃圾收集時使用者線程的停頓時間,而 Parallel Scavenge 收集器的目標則是達到一個可控制的輸送量(Throughput)。
5.4 Serial Old收集器
Serial Old 是 Serial 收集 器 的 老年 代版本,它同樣是一個單線程收集器, 使用“ 標記- 整理” 演算法。 這個收集器 的 主要意義也是在於給Client 模式下的 虛擬機器使用。如果在Serve 模式下,那麼 它主要還有兩大用途: 一種 用途 是在 JDK 1. 5 以及之前 的 版本中與 Parallel Scavenge收集器搭配使用[ 1], 另一種 用途就是作為CMS收集器的後備預案,在 並發收集發生Concurrent Mode Failure時 使用。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器 的老年代版本,使用多線程和“ 標記- 整理” 演算法。這個收集器是在JDK 1.6中 才開始提供的。
CMC收集器
CMS( Concurrent Mark Sweep)收集器 是 一種以擷取最短回收停頓時間為目標的 收集器。 目前很大一部分的Java 應用 集中在互連網站或者 B/ S 系統的服務 端上,這類應用尤其重視服務的響應速度, 希望系統停頓時間最短, 以給使用者帶來 較好的體驗。
從名字(包含" Mark Sweep") 上就可以 看出, CMS 收集器是基於“ 標記— 清除” 演算法實現的,它的運作過程相對於前面 幾種收集器來說更複雜一些, 整個過程 分為 4 個 步驟, 包括:
- 初始 標記( CMS initial mark)
- 並發 標記( CMS concurrent mark)
- 重新 標記( CMS remark)
- 並發 清除( CMS concurrent sweep)
CMS是一款優秀的收集器,它的主要優點 在 名字上已經體現出來了: 並發收集、低 停頓,Sun公司的一些官方文檔中也稱之為 並發低停頓收集器( Concurrent Low Pause Collector)。
缺點:
- CMS 收集器對CPU資源非常敏感。
- CMS 收集器無法處理浮動垃圾( Floating Garbage),可能出現" Concurrent Mode Failure" 失敗而導致 另一次Full GC的產生。
- CMS 是一款基於“ 標記— 清除” 演算法 實現的收集器,收集結束時會有大量空間 片段產生。空間片段過多時,將會給大 對象分配帶來很大麻煩, 往往會出現老年 代還有很大空間剩餘, 但是無法找到 足夠大的連續空間來分配當前對象, 不得不 提前 觸發 一次 Full GC。
5.7 G1收集器
G1( Garbage- First 收集器是當今收集 器技術發展的最前沿成果之一,早在JDK 1. 7 剛剛確立項目目標, Sun 公司給出 的 JDK 1. 7 RoadMap 裡面,它就被視為 JDK 1. 7 中HotSpot 虛擬機器的一個重要進化 特徵。
G1 是一 款 面向 服務 端 應用 的 垃圾 收集 器。 HotSpot 開發 團隊 賦予 它的 使命 是( 在 比較 長期 的) 未來 可以 替換 掉 JDK 1. 5 中 發布 的 CMS 收集 器。 與其 他 GC 收集 器 相比, G1 具備 如下 特點。
5.8理解GC日誌
虛擬機器提供了- XX:+ PrintGCDetails 這個 收集器日誌參數, 告訴虛擬機器在發生垃圾 收集行為時列印記憶體回收日誌, 並且在 進程退出的時候輸出當前的記憶體各地區 分配情況。 在實際應用中, 記憶體回收 日誌一般是列印到檔案後通過日誌工具 進行分析, 不過本實驗的日誌並不 多,直接閱讀就能看得很清楚。
記憶體配置與回收策略對象優先在Eden分配
新生代GC( Minor GC): 指發生在 新生代的垃圾收集動作, 因為Java對象 大多都具備朝生夕滅的 特性, 所以 Minor GC 非常 頻繁,一般回收速度也比較快。
老年代GC( Major GC/ Full GC): 指發生在老年代的 GC,出現了Major GC, 經常會伴隨至少一次的Minor GC( 但非 絕對的,在 Parallel Scavenge 收集器 的收集策略裡就有直接進行 Major GC 的 策略 選擇 過程)。** Major GC的速度 一般 會 比 Minor GC慢10倍以上。**
大對象直接進入老年代
所謂的大對象是指, 需要大量連續記憶體 空間的 Java 對象, 最典型的大對象 就是 那種很長的字串以及數組。 大對象對 虛擬機器的記憶體配置來說就是一個壞訊息,經常出現大對象容易導致記憶體還有不少空 間時就提前觸發垃圾收集以擷取足夠的 連續 空間 來“ 安置” 它們。
長期存活的對象進入老年代
如果對象在Eden 出生並經過第一次 Minor GC 後仍然存活, 並且能被 Survivor 容納的 話,將被移動到 Survivor 空間中, 並且 對象年齡設為1。 對象在 Survivor 區 中 每“ 熬過” 一次 Minor GC,年齡就增加 1 歲, 當它的年齡增加到一定程度( 預設 為 15 歲), 就將會被晉陞到老年代 中。 對象晉陞老年代的年齡閾值, 可以通過參數- XX: MaxTenuringThreshold 設定。
動態對象年齡判定
如果在 Survivor 空間中相同年齡所有對象大小的總和大於Survivor 空間的一半,年齡 大於或等於該年齡的對象就可以直接進入老年代, 無須等到 MaxTenuringThreshold 中 要求的年齡。
空間分配擔保
在發生Minor GC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼MinorGC可以確保是安全的。如果不成立,則虛擬機器會查看HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於曆次晉陞到老年代對象的平均大小,如果大於,將嘗試著進行一次Minor GC,儘管這次 Minor GC是有風險的; 如果小於,或者HandlePromotionFailure 設定不允許冒險, 那這時也要改為進行一次Full GC。
第四章:虛擬機器效能監控與故障處理
給一個系統定位問題的時候,知識、 經驗是關鍵基礎,資料是依據,工具 是運用知識處理資料的手段。 這裡說的資料包括: 作業記錄、 異常堆棧、 GC 日誌、 線程 快照( threaddump/ javacore 檔案)、 堆 轉儲快照( heapdump/ hprof 檔案) 等。 經常使用適當的虛擬機器監控和 分析的工具可以加快我們分析資料、 定位解決問題的速度。
第五章:調優案例分析與實戰第六章:類檔案結構第七章:虛擬機器載入類機制第八章:虛擬機器位元組碼執行引擎第九章: 類載入及執行子系統的案例與實戰第十章:早期(編譯期)最佳化第十一章: 晚期(運行期)最佳化第十二章:Java記憶體模型與線程第十三章:安全執行緒與鎖最佳化
《深入理解Java虛擬機器:JVM進階屬性與最佳實務》讀書筆記(更新中)