1. 程式人生 > >JVM自動記憶體管理機制--讀這篇就GO了

JVM自動記憶體管理機制--讀這篇就GO了

之前看過JVM的相關知識,當時沒有留下任何學習成果物,有些遺憾。這次重新複習了下,並通過部落格來做下筆記(只能記錄一部分,因為寫部落格真的很花時間),也給其他同行一些知識分享。

Java自動記憶體管理機制包含兩部分:記憶體分配和記憶體回收,要想理解記憶體分配和回收的機制,則需要了解下Java記憶體區域(Java執行時資料區),這篇隨筆將按照下面的線索進行逐步解析:

  1. Java執行時資料區
  2. 物件“已死”的判定演算法
  3. 垃圾收集演算法
  4. 垃圾收集器
  5. 結束語

好,接下來我們一一來看。

一、Java執行時資料區

根據《Java虛擬機器規範》的規定,Java虛擬機器所管理的記憶體將會包括如下幾個執行時資料區域

  • 程式計數器:用來記錄當前執行緒所執行的位元組碼指令的行號指示器。位元組碼計時器需要通過改變改值來選取下一條需要執行的位元組碼指定,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個指示器來完成。程式計數器是唯一一個沒有規定任何OutOfMemoryError情況的區域。
  • Java虛擬機器棧:虛擬機器棧描述的是Java方法執行的記憶體模型,每個方法執行時都會建立一個棧幀用來儲存區域性變量表(存放編譯器可知的各種基本資料型別、物件引用和returnAddress型別,所需的記憶體空間在編譯器完成分配)、運算元棧、動態連結、方法出口等資訊。Java虛擬機器棧有兩種異常情況:OutOfMemoryError(擴充套件時無法申請到足夠記憶體)和StackOverflowError(執行緒請求的棧深度大於虛擬機器所允許的深度)。
  • 本地方法棧:同Java虛擬機器棧類似,只不過Java虛擬機器棧為虛擬機器執行Java方法服務,本地方法棧為虛擬機器使用Native方法服務。HotSpot直接將兩個棧合二為一。也規定了兩種異常:OutOfMemoryError和StackOverflowError。
  • 堆:JVM所管理的記憶體中最大的一塊,也是GC管理的主要區域。理論上所有的物件例項和陣列都要在堆上分配。堆的大小是可以擴充套件的,通過-Xms和-Xms控制,並且堆無法擴充套件的時候就會報OutOfMemoryError異常。
  • 方法區:用來儲存JVM載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是為了和堆區分開來,它也叫Non-Heap(非堆)。方法區無法滿足記憶體分配需求時,報OutOfMemoryError異常。
  • 直接記憶體:並不是虛擬機器執行時資料區的一部分,也不是JVM規範中定義的記憶體區域,但是卻被經常使用。JDK1.4中新加入的NIO類,引入了基於通道和緩衝區的I/O方式,他可以直接分配對外記憶體,以提高效能。不收堆大小的限制,但是會受實體記憶體的約束。也會報OutOfMemoryError異常。

附棧到堆的關聯例子(基於HotSpot):

二、物件“已死”的判定演算法

 由於程式計數器、Java虛擬機器棧、本地方法棧都是執行緒獨享,其佔用的記憶體也是隨執行緒生而生、隨執行緒結束而回收。而Java堆和方法區則不同,執行緒共享,是GC的所關注的部分。

在堆中幾乎存在著所有物件,GC之前需要考慮哪些物件還活著不能回收,哪些物件已經死去可以回收。

有兩種演算法可以判定物件是否存活:

  1. )引用計數演算法:給物件中新增一個引用計數器,每當一個地方應用了物件,計數器加1;當引用失效,計數器減1;當計數器為0表示該物件已死、可回收。但是它很難解決兩個物件之間相互迴圈引用的情況。
  2. )可達性分析演算法:通過一系列稱為“GC Roots”的物件作為起點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連(即物件到GC Roots不可達),則證明此物件已死、可回收。Java中可以作為GC Roots的物件包括:虛擬機器棧中引用的物件、本地方法棧中Native方法引用的物件、方法區靜態屬性引用的物件、方法區常量引用的物件。

在主流的商用程式語言(如我們的Java)的主流實現中,都是通過可達性分析演算法來判定物件是否存活的。

三、垃圾收集演算法

1、標記-清除演算法

最基礎的演算法,分標記和清除兩個階段:首先標記處所需要回收的物件,在標記完成後統一回收所有被標記的物件。

它有兩點不足:一個效率問題,標記和清除過程都效率不高;一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片(類似於我們電腦的磁碟碎片),空間碎片太多導致需要分配大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾回收動作。

2、複製演算法

 為了解決效率問題,出現了“複製”演算法,他將可用記憶體按容量劃分為大小相等的兩塊,每次只需要使用其中一塊。當一塊記憶體用完了,將還存活的物件複製到另一塊上面,然後再把剛剛用完的記憶體空間一次清理掉。這樣就解決了記憶體碎片問題,但是代價就是可以用內容就縮小為原來的一半。

3、標記-整理演算法

複製演算法在物件存活率較高時就會進行頻繁的複製操作,效率將降低。因此又有了標記-整理演算法,標記過程同標記-清除演算法,但是在後續步驟不是直接對物件進行清理,而是讓所有存活的物件都向一側移動,然後直接清理掉端邊界以外的記憶體。

4、分代收集演算法

當前商業虛擬機器的GC都是採用分代收集演算法,這種演算法並沒有什麼新的思想,而是根據物件存活週期的不同將堆分為:新生代和老年代,方法區稱為永久代(在新的版本中已經將永久代廢棄,引入了元空間的概念,永久代使用的是JVM記憶體而元空間直接使用實體記憶體)。

這樣就可以根據各個年代的特點採用不同的收集演算法。

新生代中的物件“朝生夕死”,每次GC時都會有大量物件死去,少量存活,使用複製演算法。新生代又分為Eden區和Survivor區(Survivor from、Survivor to),大小比例預設為8:1:1。

老年代中的物件因為物件存活率高、沒有額外空間進行分配擔保,就使用標記-清除或標記-整理演算法。

  • 新產生的物件優先進去Eden區,當Eden區滿了之後再使用Survivor from,當Survivor from 也滿了之後就進行Minor GC(新生代GC),將Eden和Survivor from中存活的物件copy進入Survivor to,然後清空Eden和Survivor from,這個時候原來的Survivor from成了新的Survivor to,原來的Survivor to成了新的Survivor from。複製的時候,如果Survivor to 無法容納全部存活的物件,則根據老年代的分配擔保(類似於銀行的貸款擔保)將物件copy進去老年代,如果老年代也無法容納,則進行Full GC(老年代GC)。
  • 大物件直接進入老年代:JVM中有個引數配置-XX:PretenureSizeThreshold,令大於這個設定值的物件直接進入老年代,目的是為了避免在Eden和Survivor區之間發生大量的記憶體複製。
  • 長期存活的物件進入老年代:JVM給每個物件定義一個物件年齡計數器,如果物件在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納,將被移入Survivor並且年齡設定為1。沒熬過一次Minor GC,年齡就加1,當他的年齡到一定程度(預設為15歲,可以通過XX:MaxTenuringThreshold來設定),就會移入老年代。但是JVM並不是永遠要求年齡必須達到最大年齡才會晉升老年代,如果Survivor 空間中相同年齡(如年齡為x)所有物件大小的總和大於Survivor的一半,年齡大於等於x的所有物件直接進入老年代,無需等到最大年齡要求。

四、垃圾收集器

垃圾收集演算法是方法論,垃圾收集器是具體實現。JVM規範對於垃圾收集器的應該如何實現沒有任何規定,因此不同的廠商、不同版本的虛擬機器所提供的垃圾收集器差別較大,這裡只看HotSpot虛擬機器。

JDK7/8後,HotSpot虛擬機器所有收集器及組合(連線)如下:

1.Serial收集器

Serial收集器是最基本、歷史最久的收集器,曾是新生代手機的唯一選擇。他是單執行緒的,只會使用一個CPU或一條收集執行緒去完成垃圾收集工作,並且它在收集的時候,必須暫停其他所有的工作執行緒,直到它結束,即“Stop the World”。停掉所有的使用者執行緒,對很多應用來說難以接受。比如你在做一件事情,被別人強制停掉,你心裡奔騰而過的“羊駝”還數的過來嗎?

儘管如此,它仍然是虛擬機器執行在client模式下的預設新生代收集器:簡單而高效(與其他收集器的單個執行緒相比,因為沒有執行緒切換的開銷等)。

工作示意圖:

2.ParNew收集器

ParNew收集器是Serial收集器的多執行緒版本,除了使用了多執行緒之外,其他的行為(收集演算法、stop the world、物件分配規則、回收策略等)同Serial收集器一樣。

是許多執行在Server模式下的JVM中首選的新生代收集器,其中一個很重還要的原因就是除了Serial之外,只有他能和老年代的CMS收集器配合工作。

工作示意圖:

3.Parallel Scavenge收集器

新生代收集器,並行的多執行緒收集器。它的目標是達到一個可控的吞吐量(就是CPU執行使用者程式碼的時間與CPU總消耗時間的比值,即 吞吐量=行使用者程式碼的時間/[行使用者程式碼的時間+垃圾收集時間],這樣可以高效率的利用CPU時間儘快完成程式的運算任務,適合在後臺運算而不需要太多互動的任務。

4.Serial Old收集器

Serial 收集器的老年代版本,單執行緒,“標記整理”演算法,主要是給Client模式下的虛擬機器使用。

另外還可以在Server模式下:

  • JDK 1.5之前的版本中雨Parallel Scavenge 收集器搭配使用
  • 可以作為CMS的後背方案,在CMS發生Concurrent Mode Failure是使用

工作示意圖:

5.Parallel Old收集器

 Parallel Scavenge的老年代版本,多執行緒,“標記整理”演算法,JDK 1.6才出現。在此之前Parallel Scavenge只能同Serial Old搭配使用,由於Serial Old的效能較差導致Parallel Scavenge的優勢發揮不出來,尷了個尬~~

Parallel Old收集器的出現,使“吞吐量優先”收集器終於有了名副其實的組合。在吞吐量和CPU敏感的場合,都可以使用Parallel Scavenge/Parallel Old組合。組合的工作示意圖如下:

6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,停頓時間短,使用者體驗就好。

基於“標記清除”演算法,併發收集、低停頓,運作過程複雜,分4步:

  1. )初始標記:僅僅標記GC Roots能直接關聯到的物件,速度快,但是需要“Stop The World”
  2. )併發標記:就是進行追蹤引用鏈的過程,可以和使用者執行緒併發執行。
  3. )重新標記:修正併發標記階段因使用者執行緒繼續執行而導致標記發生變化的那部分物件的標記記錄,比初始標記時間長但遠比並發標記時間短,需要“Stop The World”
  4. )併發清除:清除標記為可以回收物件,可以和使用者執行緒併發執行

由於整個過程耗時最長的併發標記和併發清除都可以和使用者執行緒一起工作,所以總體上來看,CMS收集器的記憶體回收過程和使用者執行緒是併發執行的。

工作示意圖:

 CSM收集器有3個缺點:

  1. )對CPU資源非常敏感

    併發收集雖然不會暫停使用者執行緒,但因為佔用一部分CPU資源,還是會導致應用程式變慢,總吞吐量降低

    CMS的預設收集執行緒數量是=(CPU數量+3)/4;當CPU數量多於4個,收集執行緒佔用的CPU資源多於25%,對使用者程式影響可能較大;不足4個時,影響更大,可能無法接受。

  2. )無法處理浮動垃圾(在併發清除時,使用者執行緒新產生的垃圾叫浮動垃圾),可能出現"Concurrent Mode Failure"失敗。

    併發清除時需要預留一定的記憶體空間,不能像其他收集器在老年代幾乎填滿再進行收集;如果CMS預留記憶體空間無法滿足程式需要,就會出現一次"Concurrent Mode Failure"失敗;這時JVM啟用後備預案:臨時啟用Serail Old收集器,而導致另一次Full GC的產生;

  3. )產生大量記憶體碎片:CMS基於"標記-清除"演算法,清除後不進行壓縮操作產生大量不連續的記憶體碎片,這樣會導致分配大記憶體物件時,無法找到足夠的連續記憶體,從而需要提前觸發另一次Full GC動作。

7.G1收集器

G1(Garbage-First)是JDK7-u4才正式推出商用的收集器。G1是面向服務端應用的垃圾收集器。它的使命是未來可以替換掉CMS收集器。

G1收集器特性:

並行與併發:能充分利用多CPU、多核環境的硬體優勢,縮短停頓時間;能和使用者執行緒併發執行。
分代收集:G1可以不需要其他GC收集器的配合就能獨立管理整個堆,採用不同的方式處理新生物件和已經存活一段時間的物件。
空間整合:整體上看採用標記整理演算法,區域性看採用複製演算法(兩個Region之間),不會有記憶體碎片,不會因為大物件找不到足夠的連續空間而提前觸發GC,這點優於CMS收集器。
可預測的停頓:除了追求低停頓還能建立可以預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不超N毫秒,這點優於CMS收集器。

為什麼能做到可預測的停頓?

是因為可以有計劃的避免在整個Java堆中進行全區域的垃圾收集。
G1收集器將記憶體分大小相等的獨立區域(Region),新生代和老年代概念保留,但是已經不再物理隔離。
G1跟蹤各個Region獲得其收集價值大小,在後臺維護一個優先列表;
每次根據允許的收集時間,優先回收價值最大的Region(名稱Garbage-First的由來);
這就保證了在有限的時間內可以獲取儘可能高的收集效率。

物件被其他Region的物件引用了怎麼辦?

判斷物件存活時,是否需要掃描整個Java堆才能保證準確?在其他的分代收集器,也存在這樣的問題(而G1更突出):新生代回收的時候不得不掃描老年代?
無論G1還是其他分代收集器,JVM都是使用Remembered Set來避免全域性掃描: 每個Region都有一個對應的Remembered Set; 每次Reference型別資料寫操作時,都會產生一個Write Barrier暫時中斷操作; 然後檢查將要寫入的引用指向的物件是否和該Reference型別資料在不同的Region(其他收集器:檢查老年代物件是否引用了新生代物件); 如果不同,通過CardTable把相關引用資訊記錄到引用指向物件的所在Region對應的Remembered Set中; 進行垃圾收集時,在GC根節點的列舉範圍加入Remembered Set,就可以保證不進行全域性掃描,也不會有遺漏。

不計算維護Remembered Set的操作,回收過程可以分為4個步驟(與CMS較為相似):

  1. )初始標記:僅僅標記GC Roots能直接關聯到的物件,並修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式併發執行時能在正確可用的Region中建立新物件,需要“Stop The World”
  2. )併發標記:從GC Roots開始進行可達性分析,找出存活物件,耗時長,可與使用者執行緒併發執行
  3. )最終標記:修正併發標記階段因使用者執行緒繼續執行而導致標記發生變化的那部分物件的標記記錄。併發標記時虛擬機器將物件變化記錄線上程Remember Set Logs裡面,最終標記階段將Remember Set Logs整合到Remember Set中,比初始標記時間長但遠比並發標記時間短,需要“Stop The World”
  4. )篩選回收:首先對各個Region的回收價值和成本進行排序,然後根據使用者期望的GC停頓時間來定製回收計劃,最後按計劃回收一些價值高的Region中垃圾物件。回收時採用複製演算法,從一個或多個Region複製存活物件到堆上的另一個空的Region,並且在此過程中壓縮和釋放記憶體;可以併發進行,降低停頓時間,並增加吞吐量。

工作示意圖:

五、結束語

到此,本篇隨筆終於寫完了。

寫部落格真的很花時間,我用了三天的空閒時間才整理完這篇內容。各位覺得好的話,別忘了點贊哦~~

參考資料:

《深入理解Java虛擬機器:JVM高階特性與最佳實踐(第2版)》-- 周志明

部落格:https://blog.csdn.net/tjiyu/article/details/53983650

相關推薦

JVM自動記憶體管理機制--GO

之前看過JVM的相關知識,當時沒有留下任何學習成果物,有些遺憾。這次重新複習了下,並通過部落格來做下筆記(只能記錄一部分,因為寫部落格真的很花時間),也給其他同行一些知識分享。 Java自動記憶體管理機制包含兩部分:記憶體分配和記憶體回收,要想理解記憶體分配和回收的機制,則需要了解下Java記憶體區域(Ja

關於UDP-(疑難雜症和使用)

關於UDP-讀這篇就夠了(疑難雜症和使用) 本文為轉載文章 原文連結:https://www.qcloud.com/community/article/848077001486437077 版權歸原文所有     關於UDP  面向報文的傳輸

es6 -- 透徹掌握Promise的使用,

Promise的重要性我認為我沒有必要多講,概括起來說就是必須得掌握,而且還要掌握透徹。這篇文章的開頭,主要跟大家分析一下,為什麼會有Promise出現。 在實際的使用當中,有非常多的應用場景我們不能立即知道應該如何繼續往下執行。最重要也是最主要的一個場景就是ajax請求。通俗來說,由於網速的不同,可能你得

前端基礎進階(十三):透徹掌握Promise的使用,(轉)

https://www.jianshu.com/p/fe5f173276bd Promise的重要性我認為我沒有必要多講,概括起來說就是必須得掌握,而且還要掌握透徹。這篇文章的開頭,主要跟大家分析一下,為什麼會有Promise出現。 在實際的使用當中,有非常多的應用場景我們不能立即知道應該如

JVM介紹&自動記憶體管理機制

  1.介紹JVM(Java Virtual Machine,Java虛擬機器)      JVM是Java Virtual Machine的縮寫,通常成為java虛擬機器,作為Java可以進行一次編寫,到處執行(Write once, run anywhe

深入理解JVM(二)自動記憶體管理機制

2.1 C、C++記憶體管理是由開發人員管理,而Java則交給了JVM進行自動管理 2.2 JVM執行時資料區:方法區、堆(執行時執行緒共享),虛擬機器棧、本地方法棧、程式計數器(執行時執行緒隔離,私有)   2.2.1 程式計數器(Program Counter Register):每一個執行緒都獨有一

深入理解JVM虛擬機器-2自動記憶體管理機制

java虛擬機器所管理的記憶體將會包括一下幾個執行時資料區域。 程式計數器: 程式計數器是一塊較小的記憶體空間。位元組碼解析式工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來

JVM學習(1) 自動記憶體管理機制

Java記憶體區域與記憶體溢位異常 Java和C++之間有一堵由記憶體動態分配和垃圾手機技術所圍成的高牆,牆外面的人想進去,牆裡面的人卻想出來。 概述 對於從事C和C++程式開發的開發人員來說,在記憶體管理領域,他們即是擁有最高權力的皇帝,又是從事最基礎工作的勞動人民——既有用每一個物件的“所有權”,又

JVM自動記憶體管理機制

Java虛擬機器執行時資料區域:由所有執行緒共享的資料區有:方法區和堆,執行緒隔離的資料區有:虛擬機器棧、本地方法棧、程式計數器。程式計數器:一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要

三、Java虛擬機器自動記憶體管理機制、物件建立及記憶體分配

  1、物件是如何建立: 步驟:    (1)、虛擬機器遇到new <類名>的指令---->根據new的引數是否在常量池中定位一個類的符號引用    (2)、檢測該符號引用代表的類是否已經被載入、解析、和初始化。(如果沒有則

自動記憶體管理機制(5)- 虛擬機器效能監控

自動記憶體管理機制(5)- 虛擬機器效能監控 0. 概述 在我們日常開發的專案中,有時經常會碰到以下問題: OOM(OutOfMemoryError),記憶體不足 記憶體洩漏 執行緒死鎖 Lock Contention,鎖爭用 Java程序消耗CP

自動記憶體管理機制(4)- 記憶體分配和回收策略

自動記憶體管理機制(4)- 記憶體分配和回收策略 Java所承諾的自動記憶體管理主要是針對物件記憶體的回收和物件記憶體的分配。 在Java虛擬機器的五塊記憶體空間中,程式計數器、Java虛擬機器棧、本地方法棧記憶體的分配和回收都具有確定性,一般在編譯階段就能確定需要分配的記憶體大小,

自動記憶體管理機制(3)-HotSpot垃圾收集器

自動記憶體管理機制(3)-HotSpot垃圾收集器 如果說收集演算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實現。 這裡討論的收集器都是JDK1.7(包含JDK1.7)以後的HotSpot虛擬機器: 上半部屬於新生代收集器,下半部屬於老年代收集器。如果兩個收集器

自動記憶體管理機制(2)- 記憶體回收和垃圾收集演算法

自動記憶體管理機制(2)- 記憶體回收和垃圾收集演算法 1. 概述 首先思考三個問題: 哪些記憶體需要回收 什麼時候回收 如何回收 程式計數器、虛擬機器棧、本地方法棧是執行緒私有的,因此這幾個區域的記憶體分配和回收都具有確定性(執行緒結束時執行垃圾回

自動記憶體管理機制(1)- java記憶體區域與虛擬機器物件

自動記憶體管理機制(1)- java記憶體區域與虛擬機器物件 1. 執行時資料區域 Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。有的區域隨著虛擬機器進行的啟動而存在,有些區域則以來使用者執行緒的啟動和結束而建立和銷燬。 有以下幾個區域

深入理解java虛擬機器之自動記憶體管理機制(二)

垃圾收集演算法     java中的記憶體是交給虛擬機器管理的。要實現垃圾回收,必須考慮如下三個問題:     1. 哪些記憶體需要回收?     2. 什麼時候回收?     3. 怎麼回收?     對於第一點,往大了來說,是堆和方法區的記憶體需要回收。往具體了來說,是堆中哪些物件的記憶體可以回

深入理解java虛擬機器之自動記憶體管理機制(三)

  各類垃圾收集器與GC日誌 (一)垃圾收集器   一、Serial收集器     最基本、歷史最悠久的收集器。使用複製演算法,用在新生代,通常老年代用Serial old配合。GC過程需要stop the world。適用於client模式下的虛擬機器。   二、ParNew收集器  

深入理解java虛擬機器之自動記憶體管理機制(四)

記憶體分配與回收策略 (一)記憶體分配策略     給誰分配?分配到哪?是記憶體分配策略必須解答的問題。     java物件是分配的物件,往大方向來說,是分配到堆中,更細一點說,根據物件不同的特點分配到新生代和老年代區域。如果啟動了本地執行緒分配緩衝,就按執行緒優先在TLAB上分配。     一、新

Java虛擬機器筆記-1(Java技術體系&自動記憶體管理機制&記憶體區域與記憶體溢位&垃圾收集器與記憶體分配策略)

世界上沒有完美的程式,但寫程式是不斷追求完美的過程。 Devices(裝置、裝置)、GlassFish(商業相容應用伺服器) 目錄 1. Java技術體系包括: Java技術體系的4個平臺 虛擬機器分類 HotSpot VM 模組化、混合程式設計 多核並行

二、Java虛擬機器自動記憶體管理機制、執行時資料區域深入瞭解

執行時資料區域:     (1)、程式計數器         a、定義:是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。         b、執行緒私有:因為多執行緒是通過執行緒輪流切換並且分配處理器執行時間的方式來實現的,任何時刻,