1. 程式人生 > >《深入理解Java虛擬機:JVM高級屬性與最佳實踐》讀書筆記(更新中)

《深入理解Java虛擬機:JVM高級屬性與最佳實踐》讀書筆記(更新中)

pen 內存區域 深度 span 進化 ria 最短 描述 core

第一章:走進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)和程序計數器。

  1. 程序計數器
    程序計數器(Program Counter Register) 是一 塊 較小的內存空間,它可以看作是當前線程所執行的字節碼的行號 指示器。 如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的 虛擬機字節碼指令的地址; 如果正在執行的是 Native 方法,這個 計數器值則為空( Undefined)。 此內存區域是唯一一個在 Java虛擬 機規範中沒有規定任何OutOfMemoryError情況的區域。
  2. Java虛擬機棧
    與程序計數器一樣,虛擬機棧也是線程私有的,它的 生命周期與線程相同。虛擬機棧描述的是Java方法執行 的內存模型:每個方法在執行的同時都會創建一個棧 幀( Stack Frame) 用於存儲 局部 變 量表、操 作數棧、動態 鏈接、方法出口等信息。 每一個方法 從調用直至執行完成的過程,就對應著一個棧幀在虛擬 機棧中入棧到出棧的過程。局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、 對象引用和returnAddress 類型。
    在 Java 虛擬機規範中, 對這個區域規定了兩種異常 狀況: 如果線程請求的棧深度大於虛擬機所允許的 深度, 將拋出StackOverflowError 異常;如果虛擬機 棧可以動態擴展(當前大部分的Java虛擬機都可動態 擴展, 只不過Java虛擬機規範中也允許固定長度的虛擬 機 棧),如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError 異常。
  3. 本地方法棧
    本地方法棧與虛擬機棧所發揮的作用是非常相似的, 它們之間的區別不過是虛擬機棧為虛擬機執行Java方法(也就是 字節 碼) 服務,而本地方法棧則為虛擬機使 用到的Native 方法服務。
    與 虛擬 機 棧 一樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError異常。
  4. Java堆 Java堆是虛擬機所管理的內存中最大的一塊,是 被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例 都在這裏分配內存。
    Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“ GC 堆”(Garbage Collected Heap)。
    從內存回收的角度來看,由於現在收集器 基本都采用分代收集算法,所以Java堆中還可以細分為: 新生代和老年代; 再細致 一點的有Eden空間、From Survivor 空間、 To Survivor 空間等。
    根據Java虛擬機規範的規定, Java堆可以 處於物理上不連續的內存空間中,只要邏輯上 是連續的即可,就像我們的磁盤空間一樣。在實現時, 既可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(通過- Xmx 和- Xms 控制)。如果在堆中沒有內存完成實例分配, 並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。
  5. 方法區 方法區用於存儲已被虛擬機加載的類信息、 常量、靜態 變量、即時編譯器編譯後的 代碼等數據。 雖然 Java 虛擬機規範把 方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non- Heap( 非 堆),目的應該是與堆區分開來。
    很多人都更願意把方法區稱為“ 永久代”( Permanent Generation),本質上兩者並不 等價,等價, 僅僅是因為 HotSpot 虛擬 機 的設計團隊選擇把 GC分代收集擴展至 方法 區, 或者說 使用 永久代來實現方法區 而已, 這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分內存,能夠省去專門為方法區編寫內存管理代碼的工作,並非就如永久帶就是就是進入了方法區。(Java 8已經將移除了永久代更改為元數據區)
  6. 運行時常量池
    Runtime Constant Pool, 該區域屬於方法區的一部分。 Class 文件中除了有類的版本、字段、方法、接口等 描述信息外,常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。
    運行時常量池相對於Class 文件常量池的 另外 一個重要特征是具備動態性,Java語言並不 要求常量一定只有編譯期才產生,也就是並非 預置入Class文件中常量池的內容才能進入 方法區運行時 常量 池, 運行 期間 也可 能將 新的 常量 放入 池 中, 這種 特性 被 開發 人員 利用 得比 較多 的 便是 String 類 的 intern() 方法。 當常量池無法 再申請到內存時會拋出 OutOfMemoryError 異常。

HotSpot虛擬機對象探秘

以Java堆為例,探訪對象的創建、內存布局和定位。

  1. 對象的創建
  • 虛擬機遇到一條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堆中的對象非常類似。

如何判斷一個類是否無用:

  1. 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
  2. 加載該類的ClassLoader已經被回收。
  3. 該類對應的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高級屬性與最佳實踐》讀書筆記(更新中)