天天看點

java垃圾回收機制 記憶體消耗_GC垃圾回收機制詳解

個人了解:

因為在使用JAVA建立一個類或者對象後,難免會存在以後不使用的情況,為了減少其繼續再占用記憶體,必須建立一套清理垃圾的機制,但是怎麼判斷什麼樣的才算是不使用的垃圾呢,這裡面進行了判斷并标記分類,然後根據不同的标記再進行不同的處理。不過世事無完美之說,其也是存在弊端的(開銷通常很大,而且它的運作具有不确定性),為了避免,我們還是在正常工作中,養成一個好的程式設計習慣。

一、JAVA記憶體區域:

在Java運作時的資料區裡,由JVM(JAVA虛拟機)管理的記憶體區域分為下圖幾個子產品:

java垃圾回收機制 記憶體消耗_GC垃圾回收機制詳解

其中:

1,程式計數器(Program Counter Register):程式計數器是一個比較小的記憶體區域,用于訓示目前線程所執行的位元組碼執行到了第幾行,可以了解為是目前線程的行号訓示器。位元組碼解釋器在工作時,會通過改變這個計數器的值來取下一條語句指令。

每個程式計數器隻用來記錄一個線程的行号,是以它是線程私有(一個線程就有一個程式計數器)的。

如果程式執行的是一個Java方法,則計數器記錄的是正在執行的虛拟機位元組碼指令位址;如果正在執行的是一個本地(native,由C語言編寫完成)方法,則計數器的值為Undefined,由于程式計數器隻是記錄目前指令位址,是以不存在記憶體溢出的情況,是以,程式計數器也是所有JVM記憶體區域中唯一一個沒有定義OutOfMemoryError的區域。

2,虛拟機棧(JVM Stack):一個線程的每個方法在執行的同時,都會建立一個棧幀(Statck Frame),棧幀中存儲的有局部變量表、操作站、動态連結、方法出口等,當方法被調用時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。

局部變量表中存儲着方法的相關局部變量,包括各種基本資料類型,對象的引用,傳回位址等。在局部變量表中,隻有long和double類型會占用2個局部變量空間(Slot,對于32位機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,局部變量表是在編譯時就已經确定好的,方法運作所需要配置設定的空間在棧幀中是完全确定的,在方法的生命周期内都不會改變。

虛拟機棧中定義了兩種異常,如果線程調用的棧深度大于虛拟機允許的最大深度,則抛出StatckOverFlowError(棧溢出);不過多數Java虛拟機都允許動态擴充虛拟機棧的大小(有少部分是固定長度的),是以線程可以一直申請棧,直到記憶體不足,此時,會抛出OutOfMemoryError(記憶體溢出)。

每個線程對應着一個虛拟機棧,是以虛拟機棧也是線程私有的。

3,本地方法棧(Native Method Statck):本地方法棧在作用,運作機制,異常類型等方面都與虛拟機棧相同,唯一的差別是:虛拟機棧是執行Java方法的,而本地方法棧是用來執行native方法的,在很多虛拟機中(如Sun的JDK預設的HotSpot虛拟機),會将本地方法棧與虛拟機棧放在一起使用。

本地方法棧也是線程私有的。

4,堆區(Heap):堆區是了解Java GC機制最重要的區域,沒有之一。在JVM所管理的記憶體中,堆區是最大的一塊,堆區也是Java GC機制所管理的主要記憶體區域,堆區由所有線程共享,在虛拟機啟動時建立。堆區的存在是為了存儲對象執行個體,原則上講,所有的對象都在堆區上配置設定記憶體(不過現代技術裡,也不是這麼絕對的,也有棧上直接配置設定的)。

一般的,根據Java虛拟機規範規定,堆記憶體需要在邏輯上是連續的(在實體上不需要),在實作時,可以是固定大小的,也可以是可擴充的,目前主流的虛拟機都是可擴充的。如果在執行垃圾回收之後,仍沒有足夠的記憶體配置設定,也不能再擴充,将會抛出OutOfMemoryError:Java heap space異常。

關于堆區的内容還有很多,将在下節“Java記憶體配置設定機制”中詳細介紹。

5,方法區(Method Area):在Java虛拟機規範中,将方法區作為堆的一個邏輯部分來對待,但事實上,方法區并不是堆(Non-Heap);另外,不少人的部落格中,将Java GC的分代收集機制分為3個代:青年代,老年代,永久代,這些作者将方法區定義為“永久代”,這是因為,對于之前的HotSpot Java虛拟機的實作方式中,将分代收集的思想擴充到了方法區,并将方法區設計成了永久代。不過,除HotSpot之外的多數虛拟機,并不将方法區當做永久代,HotSpot本身,也計劃取消永久代。本文中,由于筆者主要使用Oracle JDK6.0,是以仍将使用永久代一詞。

方法區是各個線程共享的區域,用于存儲已經被虛拟機加載的類資訊(即加載類時需要加載的資訊,包括版本、field、方法、接口等資訊)、final常量、靜态變量、編譯器即時編譯的代碼等。

方法區在實體上也不需要是連續的,可以選擇固定大小或可擴充大小,并且方法區比堆還多了一個限制:可以選擇是否執行垃圾收集。一般的,方法區上執行的垃圾收集是很少的,這也是方法區被稱為永久代的原因之一(HotSpot),但這也不代表着在方法區上完全沒有垃圾收集,其上的垃圾收集主要是針對常量池的記憶體回收和對已加載類的解除安裝。

在方法區上進行垃圾收集,條件苛刻而且相當困難,效果也不令人滿意,是以一般不做太多考慮,可以留作以後進一步深入研究時使用。

在方法區上定義了OutOfMemoryError:PermGen space異常,在記憶體不足時抛出。

運作時常量池(Runtime Constant Pool)是方法區的一部分,用于存儲編譯期就生成的字面常量、符号引用、翻譯出來的直接引用(符号引用就是編碼是用字元串表示某個變量、接口的位置,直接引用就是根據符号引用翻譯出來的位址,将在類連結階段完成翻譯);運作時常量池除了存儲編譯期常量外,也可以存儲在運作時間産生的常量(比如String類的intern()方法,作用是String維護了一個常量池,如果調用的字元“abc”已經在常量池中,則傳回池中的字元串位址,否則,建立一個常量加入池中,并傳回位址)。

6,直接記憶體(Direct Memory):直接記憶體并不是JVM管理的記憶體,可以這樣了解,直接記憶體,就是JVM以外的機器記憶體,比如,你有4G的記憶體,JVM占用了1G,則其餘的3G就是直接記憶體,JDK中有一種基于通道(Channel)和緩沖區(Buffer)的記憶體配置設定方式,将由C語言實作的native函數庫配置設定在直接記憶體中,用存儲在JVM堆中的DirectByteBuffer來引用。由于直接記憶體收到本機器記憶體的限制,是以也可能出現OutOfMemoryError的異常。

Java對象的通路方式

一般來說,一個Java的引用通路涉及到3個記憶體區域:JVM棧,堆,方法區。

以最簡單的本地變量引用:Object obj = new Object()為例:

①、Object obj表示一個本地引用,存儲在JVM棧的本地變量表中,表示一個reference類型資料;

②、new Object()作為執行個體對象資料存儲在堆中;

③、堆中還記錄了Object類的類型資訊(接口、方法、field、對象類型等)的位址,這些位址所執行的資料存儲在方法區中;

二、需要GC的原因:

應用程式對資源操作,通常簡單分為以下幾個步驟:

1、為對應的資源配置設定記憶體

2、初始化記憶體

3、使用資源

4、清理資源

5、釋放記憶體

應用程式對資源(記憶體使用)管理的方式,常見的一般有如下幾種:

1、手動管理:C,C++

2、計數管理:COM

3、自動管理:.NET,Java,PHP,GO…

但是,手動管理和計數管理的複雜性很容易産生以下典型問題:

1.程式員忘記去釋放記憶體

2.應用程式通路已經釋放的記憶體

産生的後果很嚴重,常見的如記憶體洩露、資料内容亂碼,而且大部分時候,程式的行為會變得怪異而不可預測,還有Access Violation等。

.NET、Java等給出的解決方案,就是通過自動垃圾回收機制GC進行記憶體管理。這樣,問題1自然得到解決,問題2也沒有存在的基礎。

總結:無法自動化的記憶體管理方式極容易産生bug,影響系統穩定性,尤其是線上多伺服器的叢集環境,程式出現執行時bug必須定位到某台伺服器然後dump記憶體再分析bug所在,極其打擊開發人員程式設計積極性,而且源源不斷的類似bug讓人厭惡。

三、GC如何工作:

GC的工作流程主要分為如下幾個步驟:

1、标記(Mark)---GC的根節點也即GC Root

2、計劃(Plan)

3、清理(Sweep)

4、引用更新(Relocate)

5、壓縮(Compact)

在Java中,可以當做GC Root的對象有以下幾種:

1、虛拟機(JVM)棧中的引用的對象

2、方法區中的類靜态屬性引用的對象

3、方法區中的常量引用的對象(主要指聲明為final的常量值)

4、本地方法棧中JNI的引用的對象

四、什麼時候發生GC;

1、當應用程式配置設定新的對象,GC的代的預算大小已經達到門檻值,比如GC的第0代已滿

2、代碼主動顯式調用System.GC.Collect()

3、其他特殊情況,比如,windows報告記憶體不足、CLR解除安裝AppDomain、CLR關閉,甚至某些極端情況下系統參數設定改變也可能導緻GC回收

五、建議:

由于GC的代價很大,平時開發中注意一些良好的程式設計習慣有可能對GC有積極正面的影響,否則有可能産生不良效果。

1、盡量不要new很大的object,大對象(>=85000Byte)直接歸為G2代,GC回收算法從來不對大對象堆(LOH)進行記憶體壓縮整理,因為在堆中下移85000位元組或更大的記憶體塊會浪費太多CPU時間。

2、不要頻繁的new生命周期很短object,這樣頻繁垃圾回收頻繁壓縮有可能會導緻很多記憶體碎片,可以使用設計良好穩定運作的對象池(ObjectPool)技術來規避這種問題。

3、使用更好的程式設計技巧,比如更好的算法、更優的資料結構、更佳的解決政策等等。

繼續閱讀