JVM的內存管理機制

1、JVM的內存區域

對於C、C++程序員來講,在內存管理領域,他們既擁有每個對象的「全部權」,又擔負着每個對象生命開始到終結的維護責任。html

對Java程序員來講,在虛擬機的自動內存管理機制的幫助下,再也不須要爲每一個new操做去寫匹對的 delete/free 代碼,不容易出現內存泄露和內存溢出的問題。java

 

一、內存區域

根據《Java虛擬機規範(Java SE 7版)》規定,Java虛擬機所管理的內存將包括如下幾個運行時數據區域,如圖:程序員

線程私有的內存區域:web

  • 程序計數器:可看作當前線程執行字節碼的行號指示器,字節碼解釋器工做時經過改變計數器的值來選擇下一條所需執行的字節碼指令
  • 虛擬機棧:Java方法執行的棧幀,用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法從調用至執行完成的過程,都對應一個棧幀在虛擬機棧的入棧到出棧的過程
    • 局部變量表:存放編譯期可知的基本數據類型(boolean、byte、char、int等)、對象引用(reference類型)和 returnAddress類型(指向一條字節碼指令的地址)
  • 本地方法棧:Native方法執行的棧幀

全部線程共享的內存區域:  算法

  • :存放對象實例和數組
  • 方法區:存儲被虛擬機加載的Class類信息、final常量、static靜態變量、即時編譯器編譯後的代碼等數據
    • 運行時常量池:存放編譯生成的各類字面量和符號引用,運行期間也可能將新的常量放入池中

 

二、對象的建立

在語言層面,建立對象(例如:clone,反序列化)一般是一個 new 關鍵字,而在虛擬機中,對象建立的過程是如何呢?數組

在虛擬機遇到 new 指令時:安全

1. 類加載:確保常量池中存放的是已解釋的類,且對象所屬類型已經初始化過,若是沒有,則先執行類加載數據結構

2. 爲新生對象分配內存:對象所需內存大小在類加載時能夠肯定,將肯定大小的內存從Java堆中劃分出來併發

  • 分配空閒內存方法:
    • 指針碰撞:假如堆是規整的,用過的內存和空閒的內存各一邊,中間使用指針做爲分界點,分配內存時則將指針移動對象大小的距離
    • 空閒列表:假如堆是不規整的,虛擬機須要維護哪些內存塊是可用的列表,分配時候從列表中找出足夠大的空閒內存劃分,並更新列表記錄
  • 對象建立在併發狀況下保證線程安全:例如,正在給對象A分配內存,指針還沒修改,對象B同時使用了原來的指針來分配內存
    • CAS配上失敗重試
    • 本地線程分配緩衝TLAB(ThreadLocal Allocation Buffer):將內存分配動做按線程劃分到不一樣空間中進行,即每一個線程在Java堆中預先分配一小塊內存

3. 將分配的內存空間初始化爲零值:保證對象的實例在Java代碼中能夠不賦值就可直接使用,能訪問到這些字段的數據類型對應的零值(例如,int類型參數默認爲0)oracle

4. 設置對象頭:設置對象的類的元數據信息、哈希碼、GC分代年齡等

5. 執行<init>方法初始化:將對象按照程序員的意願初始化

 

三、對象的內存佈局

在HotSpot虛擬機中,對象在內存中存儲的佈局分爲3個區域,以下圖所示:

  • 對象頭(Header)
    • MarkWord:存儲對象自身的運行時數據,例如:哈希碼HashCode、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID等。考慮空間效率,MarkWord設計爲非固定的數據結構,它根據對象的不一樣狀態複用本身的空間,以下圖所示:

         

    • 指向Class的指針:即對象指向它的類的元數據的指針,虛擬機經過這個指針來肯定是哪一個類的實例
    • 若是對象是Java數組,對象頭中還須要一塊記錄數組長度的數據
  • 實例數據(Instance Data):對象真正存儲的有效信息,也是程序代碼中定義的各類類型字段的內容
  • 對齊填充(Padding):起佔位符的做用。由於HotSpot VM的要求對象起始地址必須是8字節的整數倍,也就是對象的大小必須是8字節的整數倍。當對象實例數據部分沒有對齊時,須要對齊填充來補充

 

四、內存溢出異常

除程序計數器外,JVM其餘幾個運行時區域均可能發生OutOfMemoryError異常。

1. 堆內存溢出,OutOfMemoryError:java heap space

    緣由:Java堆用於存儲對象實例,只要不斷建立對象,並保證GC Roots到對象間有可達路徑避免這些對象的GC,當對象數量達到堆的最大容量限制後就會產生OOM

    解決方法

  • 經過參數 -XX:HeapDumpOnOutOfMemoryError 可讓虛擬機在內存溢出異常時Dump當前內存堆轉儲快照
  • 經過內存映像分析工具(如:Eclipse Memory Analyzer)對Dump出的堆轉儲快照分析,判斷是內存泄露仍是內存溢出
  • 若是是內存泄露:經過工具查看泄露對象的類型信息和它們到 GC Roots 的引用鏈信息,分析GC收集器沒法自動回收它們的緣由,定位內存泄露的代碼位置
  • 若是是內存溢出:檢查堆參數 -Xms和-Xmx,看是否可調大;代碼上檢查某些對象生命週期過長,持有時間過長的狀況,嘗試減小程序運行期間內存消耗

2. 棧內存溢出,StackOverflowError

    緣由

  • StackOverFlowError異常:線程請求的棧深度大於虛擬機所容許的最大深度
  • OutOfMemoryError異常:虛擬機擴展棧時沒法申請足夠的內存空間

    解決方法

  • 檢查代碼中是否有死遞歸;配置 -Xss 增大每一個線程的棧內存容量,但會減小工做線程數,須要權衡

 

2、垃圾回收策略

一、對象存活判斷

堆中存放着幾乎全部的對象實例,GC收集器在對堆進行回收前,首先要肯定哪些對象是「存活」的,哪些是「死去」的

1. 引用計數法

給每一個對象添加一個引用計數器,每當有地方引用它時,計數器 +1;引用失效時,計數器 -1。當計數器爲0時對象就再也不被引用。

但主流Java虛擬機沒有采用這種算法,主要緣由是:它難以解決對象之間循環引用的問題

2. 可達性分析算法

經過一系列稱爲「GC Roots」的對象做爲起始點,從這些節點向下搜索,搜索的路徑稱爲引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連(即從 GC Roots 到該對象不可達),則此對象是不可用的,會判斷爲可回收對象。

Java中,可做爲 GC Roots 的對象包括:

  • 棧(棧幀中的本地變量表)中引用的對象
  • 方法區中類 static 靜態屬性引用的對象
  • 方法區中 final 常量引用的對象
  • 本地方法棧中 JNI 引用的對象 

 

二、垃圾回收區域

垃圾回收主要是回收堆內存。在堆中,新生代常規應用進行一次GC通常可回收 70%~95% 的空間,永久代的 GC效率遠低於此

方法區進行垃圾回收的「性價比」通常比較低,主要回收兩部份內容:廢棄常量和無用的類

  • 廢棄常量回收:假如常量池的字符串,例如:「abc」,當前系統沒有任何一個String對象引用這個字面量,則「abc」常量會被清理出常量池。常量池中的其餘類、方法、字段的符號引用與此相似
  • 無用的類回收:類須要知足下面3個條件纔算是「無用的類」
    • 該類的堆中全部實例都被回收
    • 加載該類的 Class Loader 已被回收
    • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方反射訪問該類的方法

堆外內存是把內存對象分配在Java虛擬機的堆之外的內存,包括JVM自身運行過程當中分配的內存,JNI 裏分配的內存、java.nio.DirectByteBuffer 分配的內存等,這些內存直接受操做系統管理。這樣能必定程度的減小GC對應用程序的影響。但 JVM 不直接管理這些堆外內存,存在 OOM 的風險,能夠在 JVM 啓動參數加上 -XX:MaxDirectMemorySize,對申請的堆外內存大小進行限制

DirectByteBuffer 對象表示堆外內存,DirectByteBuffer 對象中持有 Cleaner 對象,它惟一保存了堆外內存的數據、開始地址、大小和容量。在建立完後的下一次 Full GC 時, Cleaner對象會對堆外內存回收

 

三、垃圾回收算法

① 標記-清除算法

標記-清除算法分爲「標記」階段和「清除」階段。標記階段是把全部活動對象都作上標記。清除階段是把那些沒有標記的對象(非活動對象)回收

它主要有兩個不足:

  1. 效率問題:標記和清除兩個過程的效率都不高
  2. 空間問題:標記清除以後會有大量不連續的內存碎片。空間碎片過多可能致使後續須要分配大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次 GC

 

複製算法

 

複製算法是將可用內存劃分爲大小相等的兩塊,每次只使用一塊,當一塊內存用完了,就將存活的對象複製到另外一塊上,而後將已使用的內存空間一次清理掉。

這樣分配內存時不用考慮內存碎片等複雜狀況,但代價是將內存縮小爲原來的一半。當對象存活率較高時,就要較多的複製操做,效率也會下降。

如今的商業虛擬機都採用複製算法來回收新生代。IBM專門的研究代表:新生代中對象 98% 是「朝生夕死」的,全部不須要 1:1 來劃分空間,HotSpot虛擬機是將內存分爲1塊大的 Eden 和 2塊小的 Survivor 空間,大小比例爲 8:1:1。每次使用 Eden 和 其中一塊 Survivor。當回收時,將 Eden 和 一塊 Survivor 中的存活對象複製到另外一塊 Survivor 上,最後清理掉剛纔的 Eden 和 Survivor。新生代每次可利用的整個新生態內存的 90%,10% 會被浪費掉。但當每次回收多餘 10% 對象存活時,即剩下一個 Survivor 空間不夠時,須要老年代內存擔保,這些對象將直接進入老年代中。 

 

③ 標記-整理算法

標記-整理算法在「標記」階段和標記-清除同樣,但後續是讓全部存活對象都向一端移動,而後清理掉端邊界外的內存

 

④ 分代算法

根據對象存活週期的不一樣將內存劃分爲幾塊看,通常把堆分爲「年輕代」和「老年代」,根據各個年代的特色採用適當的收集算法。

新生態中,每次 GC 只有少許的對象存活,就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集

老年代中,對象存活率高、沒有額外的擔保空間,就必須使用「標記-清除」或「標記-整理」算法

 

四、垃圾回收器比較

垃圾回收算法性能:

  • 吞吐量:運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。吞吐量越高,CPU利用越高效,則算法越好
  • 最大暫停時間:因 GC 而暫停應用程序線程的最長時間。暫停時間越短,則算法越好

高吞吐量和低暫停時間不可兼得。爲了得到最大吞吐量,JVM 必須儘量少地運行 GC,只有在無可奈何的狀況下才運行GC,好比:新生代或者老年代已經滿了。可是推遲運行 GC 的結果是,每次運行 GC 時須要作的事情會不少,好比有更多的對象積累在堆上等待回收,所以每次的 GC 時間則會變高,由此引發的平均和最大暫停時間也會很高

垃圾收集器是內存回收算法的具體實現。本文主要介紹 HotSpot 虛擬機中的垃圾回收器,如圖所示:

若是兩個收集器之間存在連線,說明他們可搭配使用。各垃圾回收器的功能比較以下表:

該選用哪種垃圾回收器?

1. 客戶端程序: 通常使用 -XX:+UseSerialGC (Serial + Serial Old). 特別注意, 當一臺機器上起多個 JVM, 每一個 JVM 也能夠採用這種 GC 組合

2. 吞吐率優先的服務端程序(計算密集型): -XX:+UseParallelGC 或者 -XX:+UseParallelOldGC

3. 響應時間優先的服務端程序: -XX:+UseConcMarkSweepGC

4. 響應時間優先同時也要兼顧吞吐率的服務端程序:-XX:+UseG1GC

 

五、CMS垃圾回收器

CMS(Concurrent Mark Sweep)垃圾收集器是以最短回收停頓時間爲目標的垃圾收集器。通常B/S或互聯網站的服務端比較重視響應速度,但願系統的停頓時間最短,從而帶給用戶更好的體驗,CMS就比較符合這類應用的需求。

 

① 執行過程

CMS是基於 "標記-清除" 算法實現的,由上文『複製GC算法』中所描述,新生代98%對象是朝生夕死的,因此將新生代分爲1個Eden和2個survivor區(默認內存大小是8:1:1),每次使用Eden和一個survivor區,回收時,將活着的對象拷貝到剩餘的一個survivor區,並清理以前使用的Eden和survivor區的空間。

 

它運做過程分爲如下幾個階段:

一、初始標記(須要 Stop The World):標記 GC Roots 能直接關聯到的對象,速度很快

二、併發標記(和用戶線程一塊兒工做):GC Roots Tracing的過程,例如:A是GC Root關聯到的對象,A引用B,A在初始階段標記出來,這個階段就是標記B對象

三、併發預清理(和用戶線程一塊兒工做):併發查找在併發標記階段,重新生代晉升到老年代的對象、或直接在老年代分配的大對象、或被用戶線程更新的對象,來減小 "從新標記" 階段的工做量

四、從新標記(須要 Stop The World):修正『併發標記』和『併發預清理』用戶線程與GC線程併發執行,用戶線程產生了新對象,將這些對象從新標記。這階段 STW 時間會比『初始標記』階段長一些,但遠比『併發標記』的時間短。暫停用戶線程,GC線程從新掃描堆中的對象,進行可達性分析,標記活着的對象

五、併發清理(和用戶線程一塊兒工做):移除不用的對象,回收他們佔用的堆空間。此時會產生新的垃圾,在這次GC沒法清除,只好等到下次清理,這些垃圾名爲:浮動垃圾

六、併發重置:從新設置 CMS 內部的數據結構,準備下一次 CMS 生命週期的使用


併發標記階段修改了對象如何處理?

上述 CMS GC過程當中第3個步驟:併發預清理,如何處理併發標記階段被修改的對象呢?初始標記階段的引用爲 A → B → C,併發標記時,引用關係由用戶程序改成 A → C,B再也不引用C ,因爲C在 "併發標記" 階段沒法被標記,就會被回收,而這是不容許的。

這能夠經過三色標記法解決,將GC中的對象分爲三種狀況:

  • 黑色:自身和它的子對象都掃描完成的對象,不會當成垃圾對象,不會被GC  
  • 灰色:對象自己被掃描,但還沒掃描完成子對象
  • 白色:尚未掃描過的對象,掃描完全部對象後,最終爲白色的爲不可達對象,會被當作垃圾對象

初始標記時,A 會被標記爲灰色(正在掃描 A 相關),而後掃描 A 的引用,將 B 標記爲灰色,而後 A 就掃描完成了,變爲黑色

併發標記時,若是用戶線程將 A 引用改成了 C,即 A → C,此時 CMS 在寫屏障(Write Barrier)裏發現有一個白色對象的引用(C)被賦值到黑色對象(A)的字段裏,那就會將 C 這個白色對象設置爲灰色,即增量更新(Imcremental update)。


出現老年代引用新生代的對象,GC 時如何處理?

JVM採用卡片標記(Card Marking)方法,避免 Minor GC 時須要掃描整個老年代。作法是:將老年代按照必定大小分片,每一片對應 Cards 中一項,若是老年代的對象發生了修改或指向了新生代對象,就將這個老年代的 Card 標記爲 dirty。Young GC 時,dirty card 加入待掃描的 GC Roots 範圍,避免掃描整個老年代

 

② CMS的優缺點

優勢:

一、併發收集、低停頓,Sun公司的一些官方文檔也稱之爲併發低停頓收集器(Concurrent Low Pause Collector)

缺點:

一、對CPU資源很是敏感:在併發階段,它雖然不會致使用戶線程停頓,但會由於佔用一部分線程(或CPU資源)而致使應用程序變慢,總吞吐量下降

二、產生空間碎片:基於「標記-清除」算法實現,意味着收集結束後會有大量空間碎片產生,給大對象分配帶來麻煩

三、須要更大的堆空間:CMS標記階段應用程序還在繼續執行,就會有堆空間繼續分配的狀況,爲保證 CMS 將堆回收完以前還有空間分配給正在運行的程序,必須預留一部分空間

 

③ CMS調優策略

-XX:CMSInitiatingOccupancyFraction=70 : 該值表明老年代堆空間的使用率,默認值是92,假如設置爲70,就表示第一次 CMS 垃圾收集會在老年代佔用 70% 時觸發。過大會使 STW 時間過程,太小會影響吞吐率

-XX:+UseCMSCompactAtFullCollection,-XX:CMSFullGCsBeforeCompaction=4:執行4次不壓縮的 Full GC 後,會執行一次內存壓縮的過程,用來消除內存碎片

-XX:+ConcGCThreads:併發 CMS 過程運行時的線程數,CMS 默認回收線程數是 (CPU+3) / 4。更多的線程會加快併發垃圾回收過程,但會帶來額外的同步開銷。

年輕代調優:Young GC 頻次高,則增大新生代;Young GC 時間長,則減小新生代。儘可能在 Young GC 時候回收大部分垃圾

 

六、G1

G1(Garbage-First)是一款面向服務端應用的垃圾收集器,G1的設計初衷是最小化 STW 中斷時間,一般限制 GC 停頓時間比最大化吞吐率更重要。在Java9裏,G1已經成爲默認的垃圾回收器。 

 

① 執行過程

G1的內存佈局和其餘垃圾收集器有很大區別,它將整個Java堆分爲 n 個大小相等的 Region,每一個 Region 佔有一塊連續的虛擬內存地址。新生代和老年代再也不是物理隔離,而是一部分 Region 的集合。

Region的大小能夠經過 -XX:G1HeapRegionSize 指定,若是未設置,默認將堆內存平均分爲 2048 份。G1仍屬於分代收集器,除了Eden、Survivor、Old區外,還有 Humongous 區用於專門存放巨型對象(一個對象佔用空間>50%分區容量),減小短時間存在的巨型對象對垃圾收集器形成的負面影響。

G1 的運做過程分爲如下幾個步驟:

一、全局併發標記:基於 STAB(snapshot-at-the-beginning)形式的併發標記,標記完成後,G1 基本知道了哪一個區域是空的,它首先會收集哪些產出大量空閒空間的區域,這也是它命名爲 Garbage-First 的緣由

  1.1  初始標記(STW,耗時很短):標記 GC Roots 能直接關聯到的對象,將它們的字段壓入掃描棧

  1.2  併發標記(與用戶線程併發執行,耗時較長):GC 線程不斷從掃描棧中取出引用,而後遞歸標記,直至掃描棧清空

  1.3  最終標記(STW):從新標記『併發標記』期間因用戶程序執行而致使引用發生變更的那一部分標記(寫入屏障 Write Barrier 標記的對象)

  1.4  清理(STW):統計各個 Region 被標記存活的對象有多少,若是發現沒有存活對象的 Region,就會將其總體回收到可分配的 Region 中

二、拷貝存活對象:將一部分 Region 裏的存活對象拷貝到空 Region 裏去,而後回收本來的 Region 的空間。

G1 的 GC 能夠分爲 Young GC 和 Mixed GC 兩種類型。Young GC 是選定全部新生態的 Region,經過控制新生代的 Region 個數控制 Young GC 的開銷。Mixed GC 是選定全部新生代裏的 Region,外加根據『全局併發標記』統計的收益較高的若干老年代 Region,在用戶指定的停頓時間內儘量選擇收益較高的老年代 Region。G1 裏不存在 Full GC,老年代的收集全靠 Mixed GC 來完成。

在 G1 中,使用 Rememberd Set 跟蹤 Region 內的對象引用,來避免全堆掃描的。每一個 Region 都有一個與之對應的 Rememberd Set,當程序對 Reference 類型的數據進行寫操做時,會產生 Write Barrier 暫停中斷寫操做,檢查 Reference 引用的對象是否處於不一樣的 Region 之中,若是是,便經過 CardTable 把相關引用信息記錄到被引用對象所屬 Region 的 Rememberd Set 中。當 GC 時,在GC Root 的枚舉範圍中加入 Rememberd Set 便可保證不對全堆掃描,也不會遺漏。

 

② G1與CMS的比較

G1的設計目標是取消CMS收集器,G1與CMS相比,有一些顯而易見的優勢:

一、簡單可行的性能調優-XX:+UseG1GC -Xmx32g,使用這兩個參數便可應用於生產環境,表示開啓G1,堆最大內存爲32G;-XX:MaxGCPauseMillis=n 使用這個參數可設置指望的GC中暫停時間。取消老年代的物理空間劃分,無需對每一個代的空間進行大小設置

二、可預測的 STW 停頓時間:G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定 GC 的停頓時間不超過 n 毫秒。這是經過跟蹤各個 Region 裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護優先列表,每次根據容許的收集時間,優先回收價值最大的Region,保證了 G1 能在有限的時間內能夠獲取儘量高的收集效率

三、空間整合:G1 的兩個 Region 之間是基於『複製』算法實現,在運做期間不會產生內存碎片,分配大對象時不會由於沒法找到連續空間而提早出發 Full GC 

 

③ CMS調優策略

-XX:MaxGCPauseMillis=n:設置GC時最大暫停時間,這個目標不必定能知足,JVM會盡最大努力實現它,不建議設置的太小(<50ms)

-XX:InitiatingHeapOccupancyPercent=n:觸發G1啓動 Mixed GC,表示垃圾對象在整個 G1 堆內存空間的佔比

避免使用 -Xmn 或 -XX:NewRatio等其餘顯式設置年輕代大小的選項,固定年輕代大小會覆蓋暫停時間目標

 

參考

深刻理解Java虛擬機(第2版)

Getting Started with the G1 Garbage Collector

深刻理解 Java G1 垃圾收集器

Java Hotspot G1 GC的一些關鍵技術

相關文章
相關標籤/搜索