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

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

  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().框架

軟引用:描述一些還有用但非必要的屬性。在系統將要發生內存溢出 異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是 此次回收尚未足夠的內存, 纔會拋出內存溢出異常。jvm

軟引用:它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠, 都會回收掉只被弱 引用關聯的對象。模塊化

虛引用:也成爲幽靈引用或者幻影引用,最弱。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象 被收集器回收時收到一個系統通知。

生存仍是死亡

要真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象在進行 可達性分析後發現沒有與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內存模型與線程

第十三章:線程安全與鎖優化

相關文章
相關標籤/搜索