【轉載】JVM 學習——垃圾收集器與內存分配策略


本文主要是對《深入理解java虛擬機 第二版》第三章部分做的總結,文章中大部分內容都來自這章內容,也是博客 JVM 學習的第二部分。

簡述

說到垃圾收集(Garbage Collection,GC),很多人可能會認為這是 Java 自有的特性,曾經我也一度這樣想,后來才知道 GC 的歷史要遠遠長於 Java,它第一次真正使用是在 Lisp 中,現在,像 python、go 等都有自己的垃圾收集器。在 GC 最開始設計時,人們在思考 GC 時就需要完成三件事情:

  1. 哪些內存需要進行回收?
  2. 什么時候對這些內存進行回收?
  3. 如何進行回收?

經過將近半個多世紀的發展,內存的動態分配與垃圾回收技術現在已經非常成熟,看起來是進入半自動化時代,但是我們依然需要去學習 GC 和內存分配,因為,當需要排查各種內存溢出、內存泄露問題時,當垃圾收集成為系統達到更高並發量的瓶頸時,我們就需要對這一塊進行必要的監控和調節。

回到 Java 語言,在前面介紹的 Java 內存運行時區域的各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅。棧中的棧幀隨着方法的進入和退出而有條不絮地執行着出棧和入棧操作,每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的,因此,這幾塊區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因為方法結束或者線程結束時,內存自然就跟着回收了。而 Java 堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間時才能知道會創建哪些對象,這部分的內存和回收都是動態的,垃圾回收器主要關注的也是這部分的內存。

判斷對象是否已死

Java 的堆里存放的幾乎所有的對象實例,在進行垃圾回收前,第一件事情就是要確定哪些對象還”存活”着、哪些對象已經”死去”(即不可能再被任何途徑使用的對象)。

判斷的方法

引用計數算法(Reference Counting)

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。   

可達性分析算法

基本思想:通過一系列的稱為 GC Roots 的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈時,則證明此對象是不可用的。

在 Java 中,可作為 GC Roots 的對象包括下面幾種:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  2. 方法區中類靜態屬性引用的對象。
  3. 方法區中常量引用的對象。
  4. 本地方法棧中 JNI(即一般說的 Native 方法)引用的對象。

兩種方法對比

引用計數法 可達性分析
優點 實現簡單,效率高(很少使用這種方法) 在主流的商業程序語言(Java、C#等)的主流實現中,都使用這種方法
缺點 無法解決對象之間相互循環引用問題(主流的 JVM 都沒有使用這種方法) 實現稍微有些復雜

對象的四種引用

在 Java 中,如果僅僅把對象分為引用和沒有被引用這兩種狀態,那么在一些場景下就無能為力了,比如:我們希望有這樣一類對象,當內存空間充足時,則能保留在內存之中,而如果內存空間在進行垃圾回收后還是非常緊張,則可以拋棄這些對象。因此,在 JDK1.2 之后,Java 就對引用的概念進行了擴充,將引用非為一下四種:

引用類型 定義 聲明方式 回收條件
強引用( Strong Reference) 強引用就是指在程序代碼之中普遍存在的 類似於Object obj= new Object() 這類的引用 只要強引用還在,永不會回收
軟引用( Soft Reference) 軟引用是用來描述一些還有用但並非必需的對象 使用SoftReference類來聲明 系統將要發生內存溢出異常之前,將會把這些對象列入回收范圍,進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
弱引用( Weak Reference) 弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些 使用 WeakReference類實現弱引用 被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾回收器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象
虛引用(WeakReference) 它是最弱的一種引用關系,一個引用是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。 使用PhantomReference類來實現虛引用 為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知

生存還是死亡

要真正宣告一個對象死亡,至少要經歷兩次標記過程:

  1. 如果對象在進行可達性分析后發現沒有與 GC Roots 相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize() 方法;
  2. 當對象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機調用過,虛擬機將這兩種情況都視為沒有必要執行
  3. 如果對象要在 finalize() 中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可。 任何一個對象的 finalize() 方法都只會被系統自動調用一次。

這里有兩點要注意:

  1. 如果一個對象被判定有必要執行 finalize() 方法,那這個對象會先被放置在一個叫做 F-Queue 的隊列中,並由虛擬機自動建立的、低優先級的 Finalizer 線程去執行它。這里的 “執行” 指的是虛擬機會觸發這個方法,但不會承諾等待它運行結束,原因是:如果一個對象在執行 finalize() 時運行緩慢,或者發生死循環,將很有可能導致 F-Queue 隊列中其他對象永久處於等待,甚至整個內存回收系統崩潰。
  2. 不鼓勵大家使用這種方法來拯救對象。相反,建議大家盡量避免使用它,因為它不是 C/ C++ 中的析構函數,而是 Java 剛誕生時為了使 C/ C++ 程序員更容易接受它所做出的一個妥協。它的運行代價高昂,不確定性大,無法保證各個對象的調用順序。 關閉外部資源,使用 try- finally 或者其他方式都可以做得更好、更及時,所以筆者大家完全可以忘掉 Java 語言中有這個方法的存在。

回收方法區

很多人認為方法區(或者 HotSpot 的永久代)是沒有垃圾收集的,Java 虛擬機規范中確實說過可以不要求虛擬機在方法區實現垃圾收集,而且在方法區中進行垃圾收集的 “性價” 一般比較低。

永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。

判斷一個常量是否是 “廢棄常量” 比較簡單,而要判定一個類是否是 “無用的類” 的條件則相對苛刻很多。類需要同時滿足下面 3 個條件才能算是“無用的類”:

  1. 該類所有的實例都已經被回收;
  2. 加載該類的 ClassLoader 已經被回收;
  3. 該類對應的 java. lang. Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

是否對類進行回收, HotSpot 虛擬機提供了 -Xnoclassgc 參數進行控制,還可以使用 -verbose: class以及 -XX:+ TraceClassLoading- XX:+ TraceClassUnLoading 查看類加載和卸載信息。

在大量使用反射、動態代理、 CGLib 等 ByteCode 框架、動態生成 JSP 以及 OSGi 這類頻繁自定義 ClassLoader 的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

垃圾收集算法

本節主要是介紹一下垃圾收集算法的思想,並不涉及具體的實現。

標記-清除算法

標記-清除(Mark-Sweep)算法,有兩個階段

  1. 首先標記所有需要回收的對象;
  2. 在標記完成后統一進行回收。

執行過程如下圖所示。

mark-sweepmark-sweep

復制算法

它將可用內存按容量划分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。 這種算法的代價是將內存縮小為了原來的一半,未免太高了一點。

算法執行過程如下圖所示

copycopy

現在的商業虛擬機都采用這種收集算法來回收新生代。將內存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor[ 1]。 當回收時,將 Eden 和 Survivor 中還存活着的對象一次性地復制到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間。

HotSpot 虛擬機默認 Eden 和 Survivor 的大小比例是 8: 1。 當 Survivor 空間不夠用時,需要依賴其他內存(這里指老年代)進行分配擔保( Handle Promotion)。 如果另外一塊 Survivor 空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。

標記-整理算法

標記-整理算法讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。

算法執行過程如下圖所示

mark-compactmark-compact

分代收集算法

當前商業虛擬機的垃圾收集都采用分代收集( Generational Collection) 算法。

一般是把 Java 堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。

  • 在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。
  • 而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。

算法對比

算法 優點 缺點
標記-清除 最基礎的算法,不是一般的簡單 一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之后會產生大量不連續的內存碎片
復制 實現簡單,運行高效 減少了內存使用空間;而且在對象存活率較高時需要進行較多的復制操作(不適合老年代)
標記-整理 根據老年代的特點提出的一種算法,適合老年代 只適合於某些特定情況
分代收集 使用多種收集算法,根據各自的特點選用不同的收集算法 在具體的實現上比前面的更加復雜

HotSpot 的算法實現

上面介紹的基礎的理論,這一節講述一下 HotSpot 虛擬機如何實現這些算法的。

枚舉根節點

當執行系統停頓下來后,並不需要一個不漏地檢查完所有執行上下文和全局的引用位置,虛擬機應當是有辦法直接得知哪些地方存放着對象引用。在 HotSpot 的實現中,是使用一組稱為 OopMap 的數據結構來達到這個目的的。

安全點

在 OopMap 的協助下, HotSpot 可以快速且准確地完成 GC Roots 枚舉,但一個很現實的問題隨之而來:可能導致引用關系變化,或者說 OopMap 內容變化的指令非常多,如果為每一條指令都生成對應的 OopMap,那將會需要大量的額外空間,這樣 GC 的空間成本將會變得更高。

實際上,HotSpot 並沒有為每條指令都生成 OopMap,而只是在 “特定的位置” 記錄了這些信息,這些位置稱為安全點(Safepoint),即程序執行時並非在所有地方都能停頓下來開始 GC,只有在達到安全點時才能暫停。

Safepoint 的選定既不能太少以至於讓 GC 等待時間太長,也不能多余頻繁以至於過分增大運行時的負載。所以,安全點的選定基本上是以 “是否具有讓程序長時間執行的特征” 為標准進行選定的——因為每條指令執行的時間非常短暫,程序不太可能因為指令流長度太長這個原因而過長時間運行,”長時間執行” 的最明顯特征就是指令序列復用,例如方法調用、循環跳轉、異常跳轉等,所以具有這些功能的指令才會產生 Safepoint。

對於 Safepoint, 另一個需要考慮的問題是如何在 GC 發生時讓所有線程(這里不包括執行 JNI 調用的線程)都“跑”到最近的安全點上再停頓下來: 搶先式中斷( Preemptive Suspension) 和主動式中斷( Voluntary Suspension)

  1. 搶占式中斷:它不需要線程的執行代碼主動去配合,在 GC 發生時,首先把所有線程全部中斷,如果有線程中斷的地方不在安全點上,就恢復線程,讓它 “跑” 到安全點上。
  2. 主動式中斷:當 GC 需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標志,各個線程執行時主動去輪詢這個標志,發現中斷標志為真時就自己中斷掛起。輪詢標志的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。

現在幾乎沒有虛擬機采用搶占式中斷來暫停線程從而響應 GC 事件

安全區域

在使用 Safepoint 似乎已經完美地解決了如何進入 GC 的問題,但實際上情況卻並不一定。Safepoint 機制保證了程序執行時,在不太長的時間內就會遇到可進入 GC 的 Safepoint。但如果程序在 “不執行” 的時候呢?所謂程序不執行就是沒有分配 CPU 時間,典型的例子就是處於 Sleep 狀態或者 Blocked 狀態,這時候線程無法響應 JVM 的中斷請求,JVM 也顯然不太可能等待線程重新分配 CPU 時間。對於這種情況,就需要安全區域(Safe Regin)來解決了。

在線程執行到 Safe Region 中的代碼時,首先標識自己已經進入了 Safe Region,那樣,當在這段時間里 JVM 要發起 GC 時,就不用管標識自己為 Safe Region 狀態的線程了。在線程要離開 Safe Region 時,它要檢查系統是否已經完成了根節點枚舉(或者是整個 GC 過程),如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開 Safe Region 的信號為止。

垃圾收集器

垃圾收集器是內存回收的具體實現,這里討論的收集器是 JDK 1.7 Update 14 之后的 HotSpot 虛擬機(目前 G1 仍然處於實驗狀態),這個虛擬機包含的所有收集器如下圖所示。

hotspothotspot

下面會介紹一下這幾種收集器的特性、基本原理和使用場景,並重點分析 CMS 和 G1 這兩個相對復雜的收集器,了解它們的部分運作細節。

注:這里只是介紹這些收集器,進行一下比較,但並非是挑選一個最好的收集器,目前到現在為止還沒有最好的收集器出現,更沒有萬能的收集器,我們只是選擇對具體應用最合適的收集器。

Serial 收集器

它曾是最基本、發展歷史最悠久的收集器,它是一個單線程的收集器,但它的單線程的意義並不僅僅說明它只會是使用一個 CPU 或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。Stop The World 這個名字也許聽起來很酷,但這項工作實際上是由虛擬機在后台自動發起和自動完成的,在用戶不可見的情況下把用戶正常工作的線程全部停掉,這對很多應用來說都是難以接受的。下圖展示了 Serial/Serial old 收集器的運行過程。

serialserial

ParNew 收集器

ParNew 收集器其實就是 Serial 收集器的多線程版本。ParNew/Serial old 收集器的運行過程如下圖所示

ParNewParNew

ParNew 收集器除了多線程收集之外,其他與 Serial 收集器相比並沒有太多創新之處,但它卻是許多運行在 Server 模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是,除了 Serial 收集器外,目前只有它能與 CMS 收集器配合工作。(CMS收集器第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。)

CMS 作為老年代的收集器,卻無法與 JDK 1. 4. 0 中已經存在的新生代收集器 Parallel Scavenge 配合工作,只能選擇ParNew或者Serial收集器中的一個。ParNew 收集器也是使用 -XX:+UseConcMarkSweepGC 選項后的默認新生代收集器,也可以使用 -XX:+UseParNewGC 選項來強制指定它。

由於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個 CPU 的環境中都不能百分之百地保證可以超越 Serial 收集器。但是,當 CPU 的數量增加時,它對於 GC 時系統資源的有效利用還是很有好處的,它默認開啟的收集線程數與 CPU 的數量相同,在 CPU 非常多(使用超線程時)的環境下,可以使用 -XX:ParallelGCThreads 參數來限制垃圾收集的線程數。

Parallel Scavenge 收集器

Parallel Scavenge 收集器是一個新生代收集器,它也是使用復制算法的收集器,又是並行的多線程收集器。

它與其他收集器的不同之處在於:它的關注點與其他收集器不同。CMS 等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量( Throughput)。

所謂吞吐量就是 CPU 用於運行用戶代碼的時間與 CPU 總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間),虛擬機總共運行了 100 分鍾,其中垃圾收集花掉 1 分鍾,那吞吐量就是 99%。

停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量則可以高效率地利用 CPU 時間,盡快完成程序的運算任務,主要適合在后台運算而不需要太多交互的任務。

Parallel Scavenge 收集器提供了兩個參數用於精確控制吞吐量:

  1. 控制最大垃圾收集停頓時間, -XX:MaxGCPauseMillis,設置時間小一點並不能使用系統的收集速度更快,因為 GC 停頓時間縮短是以犧牲吞吐量和新生代空間來換取的;
  2. 直接設置吞吐量大小, -XX:GCTimeRatio GC,CTimeRatio是指垃圾收集時間占總時間的比率。

Parallel Scavenge 收集器經常稱為 “吞吐量優先” 收集器。Parallel Scavenge 收集器還提供一個參數 -XX:+ UseAdaptiveSizePolicy,當這個參數打開后,就不需要收工指定一些細節參數了(如:新生代的大小等),虛擬機會動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC 自適應的調解策略(GC Ergonomics)。自適應調節策略也是 Parallel Scavenge 收集器與 ParNew 收集器的一個重要區別。

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同樣是一個單線程收集器,使用 “標記-整理” 算法。

這個收集器的主要意義在於給 Client 模式下的虛擬機使用,如果在 Server 模式下,那么它主要還有兩大用途:

  1. 在 JDK1.5 以及之前的版本中與 Parallel Scavenge 收集器搭配使用;
  2. 作為 CMS 收集器的后備預案,在並發收集發生 Concurrent Mode Failure 時使用。

Serial Old 收集器的工作過程如下圖所示

serialserial

Parallel old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多線程和 “標記-整理” 算法。 這個收集器是在 JDK 1. 6 中才開始提供的。在此之前,如果新生代選擇了 Parallel Scavenge 收集器,老年代除了 Serial Old( PS MarkSweep) 收集器外別無選擇(還記得上面說過 Parallel Scavenge 收集器無法與 CMS 收集器配合工作嗎?)。由於老年代 Serial Old 收集器在服務端應用性能上的拖累,這種組合的吞吐量甚至還不一定有 ParNew 加 CMS 的組合“給力”。

知道 Parallel old 收集器出現后,”吞吐量優先”收集器終於有了比較名副其實的應用組合,在注重吞吐量以及 CPU 資源敏感的場合,都可以優先考慮 Parallel Scavenge 加 Parallel old 收集器,Parallel old 收集器的工作過程如下圖所示

Parallel OldParallel Old

CMS 收集器

CMS(Concurrent Mark Sweep)收集器,以獲取最短回收停頓時間為目標,多數應用於互聯網站或者B/S系統的服務器端上。

CMS 是基於 “標記—清除” 算法實現的,整個過程分為4個步驟:

  1. 初始標記(CMS initial mark)
  2. 並發標記(CMS concurrent mark)
  3. 重新標記(CMS remark)
  4. 並發清除(CMS concurrent sweep)

有以下幾個特點:

  • 其中,初試標記、重新標記這兩個步驟仍然需要 “Stop The World”;
  • 初始標記只是標記一下 GC Roots 能直接關聯到的對象,速度很快;
  • 並發標記階段就是進行 GC Roots Tracing 的過程;
  • 重新標記階段則是為了修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初試標記階段稍長一些,但遠比並發標記的時間短。

CMS 收集器的運作步驟如下圖所示,在整個過程中耗時最長的並發標記和並發清除過程收集器線程都可以與用戶線程一起工作,因此,從總體上看,CMS 收集器的內存回收過程是與用戶線程一起並發執行的。

cmscms

  • 優點
    1. 並發收集、低停頓, Sun 公司的一些官方文檔中也稱之為並發低停頓收集器( Concurrent Low Pause Collector)。
  • 缺點
    1. CMS 收集器對 CPU 資源非常敏感。
    2. CMS 收集器無法處理浮動垃圾( Floating Garbage),可能出現 “Concurrnet Mode Failure” 失敗而導致另一次 Full GC 的產生。如果在應用中老年代增長不是太快,可以適當調高參數 -XX: CMSInitiatingOccupancyFraction 的值來提高觸發百分比,以便降低內存回收次數從而獲取更好的性能。要是 CMS 運行期間預留的內存無法滿足程序需要時,虛擬機將啟動后備預案:臨時啟用 Serial Old 收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數 -XX: CM SInitiatingOccupancyFraction 設置得太高很容易導致大量” Concurrent Mode Failure” 失敗,性能反而降低。
    3. 收集結束時會有大量空間碎片產生,空間碎片過多時,將會給大對象分配帶來很大麻煩,往往出現老年代還有很大空間剩余,但是無法找到足夠大的連續空間來分配當前對象,不得不提前進行一次 Full GC。CMS 收集器提供了一個 -XX:+UseCMSCompactAtFullCollection 開關參數(默認就是開啟的),用於在 CMS 收集器頂不住要進行 Full GC 時開啟內存碎片的合並整理過程,內存整理的過程是無法並發的,空間碎片問題沒有了,但停頓時間不得不變長。

G1 收集器

  G1 是一款面向服務器應用垃圾收集器,與其他GC收集器想必,G1具備以下特點:

  1. 並行與並發:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短 Stop The World 停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1 收集器仍然可以通過並發的方式讓Java程序繼續執行;
  2. 分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新創建的對象和已經存活了一半時間、熬過多次GC的舊對象以獲取更好的收集效果。
  3. 空間整合:與CMS的 “標記-清理” 算法不同,G1從整體上看是基於“標記-整理”算法實現的收集器,從局部(兩個Region之間)上來看是基於“復制”算法實現,無論如何,這兩種算法都意味着G1運行期間不會產生內存空間碎片,收集后能提供規整的可用內存。
  4. 可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,小號在垃圾收集上的時間不能超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特征了。
      
    下圖展示 G1 收集器的運行步驟

G1G1

G1收集器的運作大致可划分為以下幾個步驟:

  1. 初始標記(Initial Marking):僅僅只是標記一下 GC Roots 能直接關聯到的對象,並且修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序並發運行時,能在正確可用的 Region 中創建新對象,這階段需要停頓線程,但耗時很短;
  2. 並發標記(Concurrent Marking):從 GC Roots 開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序並發執行;
  3. 最終標記(Final Marking):最終標記則是為了修正在並發標記期間因用戶程序繼續運行而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程 Remembered Set Logs 里面,最終標記需要把 Remembered Set Logs 的數據合並到 Remembered Set 中,這階段需要停頓線程,但是可並行執行;
  4. 篩選回收(Live Data Counting and Evacuation):篩選回收階段首先對各個 Region 的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來指定回收計划,根據 Sun 公司透露的信息來看,這個階段是可以做到與用戶程序並發執行。

垃圾收集器對比

垃圾收集器 特性 使用場景
Serial 收集器 復制算法;單線程;新生代;簡單而高效;需要進行 stop the world。 它是虛擬機運行在 Client 模式下的默認新生代收集器
ParNew 收集器 復制算法;Serial 的多線程版本;新生代;默認的線程數與 CPU 數一致 它是許多運行在 Server 模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是,除了 Serial 收集器外,目前只有它能與 CMS 收集器配合工作。
Parallel Scavenge 收集器 復制算法;並行多線程;新生代;吞吐量優先原則;有自適應調節策略 適合后台運算而不需要太多交互的任務
Serial Old 收集器 標記-整理算法;老年代;單線程; 這個收集器的主要意義在於給 Client 模式下的虛擬機使用
Parallel Old 收集器 標記-整理;老年代;多線程;與 parallel scavenge 收集器結合實現吞吐量優先 與 Parallel Scavenge 結合使用,適用那些注重吞吐量以及對 CPU 資源敏感的場合
CMS 收集器 標記-清除;老年代;並發收集、低停頓;有三個缺點(參見上面) 非常適合那些重視響應速度,希望系統停頓時間最短的應用
G1 收集器 分代收集;空間整合;可預測的停頓 面向服務器應用垃圾收集器

垃圾收集器參數總結

參數 描述
-XX:+UseSerialGC Jvm運行在Client模式下的默認值,打開此開關后,使用Serial + Serial Old的收集器組合進行內存回收
-XX:+UseParNewGC 打開此開關后,使用ParNew + Serial Old的收集器進行垃圾回收
-XX:+UseConcMarkSweepGC 使用ParNew + CMS + Serial Old的收集器組合進行內存回收,Serial Old作為CMS出現“Concurrent Mode Failure”失敗后的后備收集器使用。
-XX:+UseParallelGC Jvm運行在Server模式下的默認值,打開此開關后,使用Parallel Scavenge + Serial Old的收集器組合進行回收
-XX:+UseParallelOldGC 使用Parallel Scavenge + Parallel Old的收集器組合進行回收
-XX:SurvivorRatio 新生代中Eden區域與Survivor區域的容量比值,默認為8,代表Eden:Subrvivor = 8:1
-XX:PretenureSizeThreshold 直接晉升到老年代對象的大小,設置這個參數后,大於這個參數的對象將直接在老年代分配
-XX:MaxTenuringThreshold 晉升到老年代的對象年齡,每次Minor GC之后,年齡就加1,當超過這個參數的值時進入老年代
-XX:UseAdaptiveSizePolicy 動態調整java堆中各個區域的大小以及進入老年代的年齡
-XX:+HandlePromotionFailure 是否允許新生代收集擔保,進行一次minor gc后, 另一塊Survivor空間不足時,將直接會在老年代中保留
-XX:ParallelGCThreads 設置並行GC進行內存回收的線程數
-XX:GCTimeRatio GC 時間占總時間的比列,默認值為99,即允許1%的GC時間,僅在使用Parallel Scavenge 收集器時有效
-XX:MaxGCPauseMillis 設置GC的最大停頓時間,在Parallel Scavenge 收集器下有效
-XX:CMSInitiatingOccupancyFraction 設置CMS收集器在老年代空間被使用多少后出發垃圾收集,默認值為68%,僅在CMS收集器時有效,-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSCompactAtFullCollection 由於CMS收集器會產生碎片,此參數設置在垃圾收集器后是否需要一次內存碎片整理過程,僅在CMS收集器時有效
-XX:+CMSFullGCBeforeCompaction 設置CMS收集器在進行若干次垃圾收集后再進行一次內存碎片整理過程,通常與UseCMSCompactAtFullCollection參數一起使用
-XX:+UseFastAccessorMethods 原始類型優化
-XX:+DisableExplicitGC 是否關閉手動System.gc
-XX:+CMSParallelRemarkEnabled 降低標記停頓
-XX:LargePageSizeInBytes 內存頁的大小不可設置過大,會影響Perm的大小,-XX:LargePageSizeInBytes=128m
-XX:+PrintGCDetails 告訴虛擬機在發送垃圾收集行為時打印內存回收日志,並在進程退出的時候輸出當前的內存各區域分配情況

內存分配與回收策略

本節主要探討給對象分配內存的部分,對象主要分配在新生代的 Eden 區上,少數情況下也可能會直接分配在老年代中,分配的規則並不是百分之百固定的,取決於使用的哪種垃圾收集器組合以及 jvm 的參數設置。下面會介紹幾條最普遍的內存分配規則。

對象優先在Eden分配

大多數情況下,對象在新生代 Eden 區中分配。當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC

  1. 新生代 GC( Minor GC): 指發生在新生代的垃圾收集動作,因為 Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。
  2. 老年代 GC( Major GC/ Full GC): 指發生在老年代的 GC, 出現了 Major GC, 經常會伴隨至少一次的 Minor GC( 但非絕對的,在 Parallel Scavenge 收集器的收集策略里就有直接進行 Major GC 的策略選擇過程)。 Major GC 的速度一般會比 Minor GC 慢 10 倍以上。

堆空間分配例子:

-verbose: gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails -XX:SurvivorRatio=8

在運行時通過 -Xms20M-Xmx20M-Xmn10M 這 3 個參數限制了 Java 堆大小為 20MB, 不可擴展,其中 10MB 分配給新生代,剩下的 10MB 分配給老年代。-XX:SurvivorRatio=8 決定了新生代中 Eden 區與一個 Survivor 區的空間比例是 8: 1

大對象直接進入老年代

所謂的大對象是指:需要大量連續內存空間的 Java 對象,最典型的大對象就是那種很長的字符串以及數組。

大對象對虛擬機的內存分配來說是一個壞消息()遇到一個大對象更加壞的消息就是遇到一群“朝生夕滅”的“短命大對象”,寫程序的時候應當避免),經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來”安置”它們。

-XX:PretenureSizeThreshold 參數,令大於這個設置值的對象直接在老年代分配(避免了在 Eden 以及兩個 Survivor 區之間發送大量的內存復制)。 PretenureSizeThreshold 參數只對 Serial 和 ParNew 兩款收集器有效, Parallel Scavenge 收集器不認識這個參數。

長期存活的對象將進入老年代

如果對象在 Eden 出生並經過第一次 Minor GC 后仍然存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並且對象年齡設為 1。 對象在 Survivor 區中每熬過一次 Minor GC, 年齡就增加 1 歲,當它的年齡增加到一定程度(默認為 15 歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數 -XX: MaxTenuringThreshold 設置。

動態對象年齡判斷

為了適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到了 MaxTenuringThreshold 才能晉升老年代。如果在 Survivor 空間中相同年齡所有對象大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到 MaxTenuringThreshold 中要求的年齡。

空間分配擔保

在發生 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那么 Minor GC 可以確保是安全的。當大量對象在 Minor GC 后仍繞存活,就需要老年代進行空間分配擔保,把 Survivor 無法容納的對象直接進入老年代。如果老年代的判斷到剩余空間不足(根據以往每一次回收晉升到老年代對象容量的平均值作為經驗值),則進行一次 Full GC。



注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com