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

一、深入理解JVM的內存結構及GC機制

1、JVM把內存分成如下區域:

(1)方法區(Method Area)

(2)堆區(Heap)

(3)虛擬機棧(VM Stack)

(4)本地方法棧(Native Method Stack)

(5)程序計數器(Program Counter Register)

其中的方法區和堆區,線程共享。

1.1 方法區(Method Area)

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

在Hotspot虛擬機中,這塊區域對應持久代(permanment Generation),一般來說,方法區上執行GC的情況很少,這是方法區被稱爲持久代的原因之一,但這不代表方法區上完全沒有GC,其上的GC主要針對常量池的回收和已加載類的卸載。在方法區上GC,條件相當苛刻而且困難。

運行時常量池(Runtime Constant Pool)是方法去的一部分,用於存儲編譯器生成的常量和引用。一般來說,常量的分配在編譯時就能確定,但也不完全是,也可以存儲在運行時期產生的常量。比如String類的intern()方法,作用是String類維護了一個常量池,如果調用的字符“hello”已經在常量池中,則直接返回常量池中的地址,否則新建一個常量加入池中,並返回地址。

1.2 堆區(Heap)

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

1.3 虛擬機棧(VM Stack)

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

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

虛擬機棧定義了兩種異常類型:StackOverFlowError(棧溢出)和OutOfMemoryError(內存溢出)。如果線程調用的棧深度大於虛擬機允許的最大深度,則拋出StackOverFlowError;不過大多數虛擬機都允許動態擴展虛擬機棧的大小,所以線程可以一直申請棧,直到內存不足時,拋出OutOfMemoryError。

1.4 本地方法棧(Native Method Stack)

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

1.5 程序計數器(Program Counter Register)

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

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

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

2、GC機制

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

2.1查找算法

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

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

 補充概念,在JDK1.2之後引入了四個概念:強引用、軟引用、弱引用、虛引用。

   強引用:new出來的對象都是強引用,GC無論如何都不會回收,即使拋出OOM異常。

   軟引用:只有當JVM內存不足時纔會被回收。

   弱引用:只要GC,就會立馬回收,不管內存是否充足。

   虛引用:可以忽略不計,JVM完全不會在乎虛引用,你可以理解爲它是來湊數的,湊夠”四大天王”。它唯一的作用就是做一些跟蹤記錄,輔助finalize函數的使用。

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

①.該類的所有實例都已經被回收;

②.加載該類的ClassLoad已經被回收;

③.該類對應的反射類java.lang.Class對象沒有被任何地方引用。

2.2 內存分區

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

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

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

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

2.3 GC算法

常見的GC算法:複製、標記-清除、標記-壓縮、分代收集算法

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

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

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

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

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

標記-清除動作不需要移動對象,且僅對不存活的對象進行清理,在空間中存活對象較多的時候,效率較高,但由於只是清除,沒有重新整理,因此會造成內存碎片。

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

由於進行了移動規整動作,該算法避免了標記-清除的碎片問題,但由於需要進行移動,因此成本也增加了。(該算法適用於舊生代)

分代收集算法:當前商業虛擬機都採用這種算法。首先根據對象存活週期的不同將內存分爲幾塊即新生代、老年代,然後根據不同年代的特點,採用不同的收集算法。在新生代中,每次垃圾收集時都有大量對象死去,只有少量存活,所以選擇了複製算法。而老年代中因爲對象存活率比較高,所以採用標記-整理算法(或者標記-清除算法)。

Minor GC

  一般情況下,當新對象生成,並且在Eden申請空間失敗時,就會觸發Minor GC,對Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因爲大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裏需要使用速度快、效率高的算法,使Eden去能儘快空閒出來。

Full GC

  對整個堆進行整理,包括Young、Tenured和Perm。Full GC因爲需要對整個堆進行回收,所以比Minor GC要慢,因此應該儘可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於FullGC的調節。有如下原因可能導致Full GC:

  1.年老代(Tenured)被寫滿;

  2.持久代(Perm)被寫滿;

  3.System.gc()被顯示調用;

       4.上一次GC之後Heap的各域分配策略動態變化。

Java常見的內存泄漏

  1. 數據庫連接,網絡連接,IO連接等沒有顯示調用close關閉,會導致內存泄露;
  2. 監聽器的使用,在釋放對象的同時沒有相應刪除監聽器的時候也可能導致內存泄露。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章