深刻理解JVM的內存結構及GC機制

1、前言

       JAVA GC(Garbage Collection,垃圾回收)機制是區別C++的一個重要特徵,C++須要開發者本身實現垃圾回收的邏輯,而JAVA開發者則只須要專一於業務開發,由於垃圾回收這件繁瑣的事情JVM已經爲咱們代勞了,從這一點上來講,JAVA仍是要作的比較完善一些。但這並不意味着咱們不用去理解GC機制的原理,由於若是不瞭解其原理,可能會引起內存泄漏、頻繁GC致使應用卡頓,甚至出現OOM等問題,所以咱們須要深刻理解其原理,才能編寫出高性能的應用程序,解決性能瓶頸。java

       想要理解GC的原理,咱們必須先理解JVM內存管理機制,由於這樣咱們才能知道回收哪些對象、何時回收以及怎麼回收。算法

2、JVM內存管理

       根據JVM規範,JVM把內存劃分紅了以下幾個區域:編程

1.方法區(Method Area)
2.堆區(Heap)
3.虛擬機棧(VM Stack)
4.本地方法棧(Native Method Stack)
5.程序計數器(Program Counter Register)複製代碼

image.png
image.png

       其中,方法區和堆全部線程共享。數組

2.1 方法區(Method Area)

       方法區存放了要加載的類的信息(如類名、修飾符等)、靜態變量、構造函數、final定義的常量、類中的字段和方法等信息。方法區是全局共享的,在必定條件下也會被GC。當方法區超過它容許的大小時,就會拋出OutOfMemory:PermGen Space異常。緩存

       在Hotspot虛擬機中,這塊區域對應持久代(Permanent Generation),通常來講,方法區上執行GC的狀況不多,所以方法區被稱爲持久代的緣由之一,但這並不表明方法區上徹底沒有GC,其上的GC主要針對常量池的回收和已加載類的卸載。在方法區上進行GC,條件至關苛刻並且困難。bash

       運行時常量池(Runtime Constant Pool)是方法區的一部分,用於存儲編譯器生成的常量和引用。通常來講,常量的分配在編譯時就能肯定,但也不全是,也能夠存儲在運行時期產生的常量。好比String類的intern()方法,做用是String類維護了一個常量池,若是調用的字符"hello"已經在常量池中,則直接返回常量池中的地址,不然新建一個常量加入池中,並返回地址。多線程

2.2 堆區(Heap)

       堆區是GC最頻繁的,也是理解GC機制最重要的區域。堆區由全部線程共享,在虛擬機啓動時建立。堆區主要用於存放對象實例及數組,全部new出來的對象都存儲在該區域。架構

2.3 虛擬機棧(VM Stack)

       虛擬機棧佔用的是操做系統內存,每一個線程對應一個虛擬機棧,它是線程私有的,生命週期和線程同樣,每一個方法被執行時產生一個棧幀(Statck Frame),棧幀用於存儲局部變量表、動態連接、操做數和方法出口等信息,當方法被調用時,棧幀入棧,當方法調用結束時,棧幀出棧。併發

       局部變量表中存儲着方法相關的局部變量,包括各類基本數據類型及對象的引用地址等,所以他有個特色:內存空間能夠在編譯期間就肯定,運行時再也不改變。函數

       虛擬機棧定義了兩種異常類型StackOverFlowError(棧溢出)和OutOfMemoryError(內存溢出)。若是線程調用的棧深度大於虛擬機容許的最大深度,則拋出StackOverFlowError;不過大多數虛擬機都容許動態擴展虛擬機棧的大小,因此線程能夠一直申請棧,直到內存不足時,拋出OutOfMemoryError。

2.4 本地方法棧(Native Method Stack)

       本地方法棧用於支持native方法的執行,存儲了每一個native方法的執行狀態。本地方法棧和虛擬機棧他們的運行機制一致,惟一的區別是,虛擬機棧執行Java方法,本地方法棧執行native方法。在不少虛擬機中(如Sun的JDK默認的HotSpot虛擬機),會將虛擬機棧和本地方法棧一塊兒使用。

2.5 程序計數器(Program Counter Register)

       程序計數器是一個很小的內存區域,不在RAM上,而是直接劃分在CPU上,程序猿沒法操做它,它的做用是:JVM在解釋字節碼(.class)文件時,存儲當前線程執行的字節碼行號,只是一種概念模型,各類JVM所採用的方式不同。字節碼解釋器工做時,就是經過改變程序計數器的值來取下一條要執行的指令,分支、循環、跳轉等基礎功能都是依賴此技術區完成的。

       每一個程序計數器只能記錄一個線程的行號,所以它是線程私有的。

       若是程序當前正在執行的是一個java方法,則程序計數器記錄的是正在執行的虛擬機字節碼指令地址,若是執行的是native方法,則計數器的值爲空,此內存區是惟一不會拋出OutOfMemoryError的區域。

3、GC機制

       隨着程序的運行,內存中的實例對象、變量等佔據的內存愈來愈多,若是不及時進行回收,會下降程序運行效率,甚至引起系統異常。

       在上面介紹的五個內存區域中,有3個是不須要進行垃圾回收的:本地方法棧、程序計數器、虛擬機棧。由於他們的生命週期是和線程同步的,隨着線程的銷燬,他們佔用的內存會自動釋放。因此,只有方法區和堆區須要進行垃圾回收,回收的對象就是那些不存在任何引用的對象。

3.1 查找算法

        經典的引用計數算法,每一個對象添加到引用計數器,每被引用一次,計數器+1,失去引用,計數器-1,當計數器在一段時間內爲0時,即認爲該對象能夠被回收了。可是這個算法有個明顯的缺陷:當兩個對象相互引用,可是兩者都已經沒有做用時,理應把它們都回收,可是因爲它們相互引用,不符合垃圾回收的條件,因此就致使沒法處理掉這一塊內存區域。所以,Sun的JVM並無採用這種算法,而是採用一個叫——根搜索算法,如圖:

image.png
image.png

       基本思想是:從一個叫GC Roots的根節點出發,向下搜索,若是一個對象不能達到GC Roots的時候,說明該對象再也不被引用,能夠被回收。如上圖中的Object五、Object六、Object7,雖然它們三個依然相互引用,可是它們其實已經沒有做用了,這樣就解決了引用計數算法的缺陷。

       補充概念,在JDK1.2以後引入了四個概念:強引用、軟引用、弱引用、虛引用
       強引用:new出來的對象都是強引用,GC不管如何都不會回收,即便拋出OOM異常。
       軟引用:只有當JVM內存不足時纔會被回收。
       弱引用:只要GC,就會立馬回收,無論內存是否充足。
       虛引用:能夠忽略不計,JVM徹底不會在意虛引用,你能夠理解爲它是來湊數的,湊夠"四大天王"。它惟一的做用就是作一些跟蹤記錄,輔助finalize函數的使用。

       最後總結,什麼樣的類須要被回收:

a.該類的全部實例都已經被回收;
b.加載該類的ClassLoad已經被回收;
c.該類對應的反射類java.lang.Class對象沒有被任何地方引用。複製代碼

3.2 內存分區

       內存主要被分爲三塊:新生代(Youn Generation)、舊生代(Old Generation)、持久代(Permanent Generation)。三代的特色不一樣,造就了他們使用的GC算法不一樣,新生代適合生命週期較短,快速建立和銷燬的對象,舊生代適合生命週期較長的對象,持久代在Sun Hotpot虛擬機中就是指方法區(有些JVM根本就沒有持久代這一說法)。

image.png
image.png

       新生代(Youn Generation):大體分爲Eden區和Survivor區,Survivor區又分爲大小相同的兩部分:FromSpace和ToSpace。新建的對象都是重新生代分配內存,Eden區不足的時候,會把存活的對象轉移到Survivor區。當新生代進行垃圾回收時會出發Minor GC(也稱做Youn GC)。

       舊生代(Old Generation):舊生代用於存放新生代屢次回收依然存活的對象,如緩存對象。當舊生代滿了的時候就須要對舊生代進行回收,舊生代的垃圾回收稱做Major GC(也稱做Full GC)。

       持久代(Permanent Generation):在Sun 的JVM中就是方法區的意思,儘管大多數JVM沒有這一代。

3.3 GC算法

       常見的GC算法複製、標記-清除和標記-壓縮

       複製:複製算法採用的方式爲從根集合進行掃描,將存活的對象移動到一塊空閒的區域,如圖所示:

image.png
image.png

當存活的對象較少時,複製算法會比較高效(新生代的Eden區就是採用這種算法),其帶來的成本是須要一塊額外的空閒空間和對象的移動。

       標記-清除:該算法採用的方式是從跟集合開始掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未被標記的對象,並進行清除。標記和清除的過程以下:

image.png
image.png

上圖中藍色部分是有被引用的對象,褐色部分是沒有被引用的對象。在Marking階段,須要進行全盤掃描,這個過程是比較耗時的。

image.png
image.png

清除階段清理的是沒有被引用的對象,存活的對象被保留。

標記-清除動做不須要移動對象,且僅對不存活的對象進行清理,在空間中存活對象較多的時候,效率較高,但因爲只是清除,沒有從新整理,所以會形成內存碎片。

       標記-壓縮:該算法與標記-清除算法相似,都是先對存活的對象進行標記,可是在清除後會把活的對象向左端空閒空間移動,而後再更新其引用對象的指針,以下圖所示

image.png
image.png

因爲進行了移動規整動做,該算法避免了標記-清除的碎片問題,但因爲須要進行移動,所以成本也增長了。(該算法適用於舊生代)

4、垃圾收集器

       在JVM中,GC是由垃圾回收器來執行,因此,在實際應用場景中,咱們須要選擇合適的垃圾收集器,下面咱們介紹一下垃圾收集器。

4.1 串行收集器(Serial GC)

       Serial GC是最古老也是最基本的收集器,可是如今依然普遍使用,JAVA SE5和JAVA SE6中客戶端虛擬機採用的默認配置。比較適合於只有一個處理器的系統。在串行處理器中minor和major GC過程都是用一個線程進行回收的。它的最大特色是在進行垃圾回收時,須要對全部正在執行的線程暫停(stop the world),對於有些應用是難以接受的,可是若是應用的實時性要求不是那麼高,只要停頓的時間控制在N毫秒以內,大多數應用仍是能夠接受的,並且事實上,它並無讓咱們失望,幾十毫秒的停頓,對於咱們客戶機是徹底能夠接受的,該收集器適用於單CPU、新生代空間較小且對暫停時間要求不是特別高的應用上,是client級別的默認GC方式。

4.2 ParNew GC

       基本和Serial GC同樣,但本質區別是加入了多線程機制,提升了效率,這樣它就能夠被用於服務端上(server),同時它能夠與CMS GC配合,因此,更加有理由將他用於server端。

4.3 Parallel Scavenge GC

       在整個掃描和複製過程採用多線程的方式進行,適用於多CPU、對暫停時間要求較短的應用,是server級別的默認GC方式。

4.4 CMS (Concurrent Mark Sweep)收集器

       該收集器的目標是解決Serial GC停頓的問題,以達到最短回收時間。常見的B/S架構的應用就適合這種收集器,由於其高併發、高響應的特色,CMS是基於標記-清楚算法實現的。

CMS收集器的優勢:併發收集、低停頓,但遠沒有達到完美;

CMS收集器的缺點:

a.CMS收集器對CPU資源很是敏感,在併發階段雖然不會致使用戶停頓,可是會佔用CPU資源而致使應用程序變慢,總吞吐量降低。
b.CMS收集器沒法處理浮動垃圾,可能出現「Concurrnet Mode Failure」,失敗而致使另外一次的Full GC。
c.CMS收集器是基於標記-清除算法的實現,所以也會產生碎片。複製代碼

4.5 G1收集器

       相比CMS收集器有很多改進,首先,基於標記-壓縮算法,不會產生內存碎片,其次能夠比較精確的控制停頓。

4.6 Serial Old收集器

       Serial Old是Serial收集器的老年代版本,它一樣使用一個單線程執行收集,使用「標記-整理」算法。主要使用在Client模式下的虛擬機。

4.7 Parallel Old收集器

       Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。

4.8 RTSJ垃圾收集器

       RTSJ垃圾收集器,用於Java實時編程。

5、總結

       深刻理解JVM的內存模型和GC機制有助於幫助咱們編寫高性能代碼和提供代碼優化的思路與方向。

相關文章
相關標籤/搜索