JAVA內存模型和GC原理

一個優秀Java程序員,必須瞭解Java內存模型、GC工做原理,以及如何優化GC的性能、與GC進行有限的交互,有一些應用程序對性能要求較高,例如嵌入式系統、實時系統等,只有全面提高內存的管理效率,才能提升整個應用程序的性能。html

本文將從JVM內存模型、GC工做原理,以及GC的幾個關鍵問題進行探討,從GC角度提升Java程序的性能。java

 

1、Java內存模型程序員

 

按照官方的說法:Java 虛擬機具備一個堆,堆是運行時數據區域,全部類實例和數組的內存均今後處分配。算法

JVM主要管理兩種類型內存:堆和非堆,堆內存(Heap Memory)是在 Java 虛擬機啓動時建立,非堆內存(Non-heap Memory)是在JVM堆以外的內存。編程

簡單來講,堆是Java代碼可及的內存,留給開發人員使用的;非堆是JVM留給本身用的,包含方法區、JVM內部處理或優化所需的內存(如 JITCompiler,Just-in-time Compiler,即時編譯後的代碼緩存)、每一個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼。數組

JVM 內存包含以下幾個部分:緩存

  • 堆內存(Heap Memory): 存放Java對象
  • 非堆內存(Non-Heap Memory): 存放類加載信息和其它meta-data
  • 其它(Other): 存放JVM 自身代碼等

在JVM啓動時,就已經保留了固定的內存空間給Heap內存,這部份內存並不必定都會被JVM使用,可是能夠肯定的是這部分保留的內存不會被其餘進程使用,這部份內存大小由-Xmx 參數指定。而另外一部份內存在JVM啓動時就分配給JVM,做爲JVM的初始Heap內存使用,這部份內存是由 -Xms 參數指定。網絡

 

詳細配置文件目錄:eclipse/eclipse.ini數據結構

 

默認空餘堆內存小於40%時,JVM 就會增大堆直到-Xmx 的最大限制,能夠由 -XX:MinHeapFreeRatio 指定。 eclipse

默認空餘堆內存大於70%時,JVM 會減小堆直到-Xms的最小限制,能夠由 -XX:MaxHeapFreeRatio 指定,詳見

能夠經過 -XX:MaxPermSize 設置Non-Heap大小,詳細參見個人百度博客

 

2、Java內存分配

 

Java的內存管理實際上就是變量和對象的管理,其中包括對象的分配和釋放。

       JVM堆內存分爲2塊:Permanent Space 和 Heap Space.

  Permanent 即 持久代(Permanent Generation),主要存放的是Java類定義信息,與垃圾收集器要收集的Java對象關係不大。

  Heap = { Old + NEW = {Eden, from, to} },Old 即 年老代(Old Generation),New 即 年輕代(Young Generation)。年老代和年輕代的劃分對垃圾收集影響比較大。

  年輕代

  全部新生成的對象首先都是放在年輕代。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象。年輕代通常分3個區,1個Eden區,2個Survivor區(from 和 to)。

  大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當一個Survivor區滿時,此區的存活對象將被複制到另一個Survivor區,當另外一個Survivor區也滿了的時候,從前一個Survivor區複製過來的而且此時還存活的對象,將可能被複制到年老代。

  2個Survivor區是對稱的,沒有前後關係,因此同一個Survivor區中可能同時存在從Eden區複製過來對象,和從另外一個Survivor區複製過來的對象;而複製到年老區的只有從另外一個Survivor區過來的對象。並且,由於須要交換的緣由,Survivor區至少有一個是空的。特殊的狀況下,根據程序須要,Survivor區是能夠配置爲多個的(多於2個),這樣能夠增長對象在年輕代中的存在時間,減小被放到年老代的可能

       針對年輕代的垃圾回收即 Young GC.

  年老代

  在年輕代中經歷了N次(可配置)垃圾回收後仍然存活的對象,就會被複制到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。

  針對年老代的垃圾回收即 Full GC.

  持久代

  用於存放靜態類型數據,如 Java Class, Method 等。持久代對垃圾回收沒有顯着影響。可是有些應用可能動態生成或調用一些Class,例如 Hibernate CGLib 等,在這種時候每每須要設置一個比較大的持久代空間來存放這些運行過程當中動態增長的類型

JVM內存申請過程以下:

  1. JVM 會試圖爲相關Java對象在Eden中初始化一塊內存區域
  2. 當Eden空間足夠時,內存申請結束;不然到下一步
  3. JVM 試圖釋放在Eden中全部不活躍的對象(這屬於1或更高級的垃圾回收),釋放後若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區
  4. Survivor區被用來做爲Eden及OLD的中間交換區域,當OLD區空間足夠時,Survivor區的對象會被移到Old區,不然會被保留在Survivor區
  5. 當OLD區空間不夠時,JVM 會在OLD區進行徹底的垃圾收集(0級)
  6. 徹底垃圾收集後,若Survivor及OLD區仍然沒法存放從Eden複製過來的部分對象,致使JVM沒法在Eden區爲新對象建立內存區域,則出現」out of memory」錯誤

補充一張全圖:

 

3、GC基本原理

GC(Garbage Collection),是JAVA/.NET中的垃圾收集器。

Java是由C++發展來的,它擯棄了C++中一些繁瑣容易出錯的東西,引入了計數器的概念,其中有一條就是這個GC機制(C#借鑑了JAVA)

編程人員容易出現問題的地方,忘記或者錯誤的內存回收會致使程序或系統的不穩定甚至崩潰,Java提供的GC功能能夠自動監測對象是否超過做用域從而達到自動回收內存的目的,Java語言沒有提供釋放已分配內存的顯示操做方法。因此,Java的內存管理實際上就是對象的管理,其中包括對象的分配和釋放。

對於程序員來講,分配對象使用new關鍵字;釋放對象時,只要將對象全部引用賦值爲null,讓程序不可以再訪問到這個對象,咱們稱該對象爲"不可達的".GC將負責回收全部"不可達"對象的內存空間。

對於GC來講,當程序員建立對象時,GC就開始監控這個對象的地址、大小以及使用狀況。一般,GC採用有向圖的方式記錄和管理堆(heap)中的全部對象。經過這種方式肯定哪些對象是"可達的",哪些對象是"不可達的".當GC肯定一些對象爲"不可達"時,GC就有責任回收這些內存空間。可是,爲了保證 GC可以在不一樣平臺實現的問題,Java規範對GC的不少行爲都沒有進行嚴格的規定。例如,對於採用什麼類型的回收算法、何時進行回收等重要問題都沒有明確的規定。所以,不一樣的JVM的實現者每每有不一樣的實現算法。這也給Java程序員的開發帶來行多不肯定性。本文研究了幾個與GC工做相關的問題,努力減小這種不肯定性給Java程序帶來的負面影響。

 

4、GC分代劃分

JVM內存模型中Heap區分兩大塊,一塊是 Young Generation,另外一塊是Old Generation

1) 在Young Generation中,有一個叫Eden Space的空間,主要是用來存放新生的對象,還有兩個Survivor Spaces(from、to),它們的大小老是同樣,它們用來存放每次垃圾回收後存活下來的對象。

2) 在Old Generation中,主要存放應用程序中生命週期長的內存對象。

3) 在Young Generation塊中,垃圾回收通常用Copying的算法,速度快。每次GC的時候,存活下來的對象首先由Eden拷貝到某個SurvivorSpace,當Survivor Space空間滿了後,剩下的live對象就被直接拷貝到OldGeneration中去。所以,每次GC後,Eden內存塊會被清空。

4) 在Old Generation塊中,垃圾回收通常用mark-compact的算法,速度慢些,但減小內存要求。

5) 垃圾回收分多級,0級爲所有(Full)的垃圾回收,會回收OLD段中的垃圾;1級或以上爲部分垃圾回收,只會回收Young中的垃圾,內存溢出一般發生於OLD段或Perm段垃圾回收後,仍然無內存空間容納新的Java對象的狀況。

 

5、增量式GC

增量式GC(Incremental GC),是GC在JVM中一般是由一個或一組進程來實現的,它自己也和用戶程序同樣佔用heap空間,運行時也佔用CPU。

當GC進程運行時,應用程序中止運行。所以,當GC運行時間較長時,用戶可以感到Java程序的停頓,另一方面,若是GC運行時間過短,則可能對象回收率過低,這意味着還有不少應該回收的對象沒有被回收,仍然佔用大量內存。所以,在設計GC的時候,就必須在停頓時間和回收率之間進行權衡。一個好的GC實現容許用戶定義本身所須要的設置,例若有些內存有限的設備,對內存的使用量很是敏感,但願GC可以準確的回收內存,它並不在乎程序速度的快慢。另一些實時網絡遊戲,就不可以容許程序有長時間的中斷。

增量式GC就是經過必定的回收算法,把一個長時間的中斷,劃分爲不少個小的中斷,經過這種方式減小GC對用戶程序的影響。雖然,增量式GC在總體性能上可能不如普通GC的效率高,可是它可以減小程序的最長停頓時間。

Sun JDK提供的HotSpot JVM就能支持增量式GC。HotSpot JVM缺省GC方式爲不使用增量GC,爲了啓動增量GC,咱們必須在運行Java程序時增長-Xincgc的參數。

HotSpot JVM增量式GC的實現是採用Train GC算法,它的基本想法就是:將堆中的全部對象按照建立和使用狀況進行分組(分層),將使用頻繁高和具備相關性的對象放在一隊中,隨着程序的運行,不斷對組進行調整。當GC運行時,它老是先回收最老的(最近不多訪問的)的對象,若是整組都爲可回收對象,GC將整組回收。這樣,每次GC運行只回收必定比例的不可達對象,保證程序的順暢運行。

 

6、詳解函數finalize

finalize 是位於Object類的一個方法,詳見個人開源項目:src-jdk1.7.0_02

protected void finalize() throws Throwable { }

該方法的訪問修飾符爲protected,因爲全部類爲Object的子類,所以用戶類很容易訪問到這個方法。

因爲,finalize函數沒有自動實現鏈式調用,咱們必須手動的實現,所以finalize函數的最後一個語句一般是 super.finalize()。經過這種方式,咱們能夠實現從下到上實現finalize的調用,即先釋放本身的資源,而後再釋放父類的資源。根據Java語言規範,JVM保證調用finalize函數以前,這個對象是不可達的,可是JVM不保證這個函數必定會被調用。另外,規範還保證finalize函數最多運行一次。

不少Java初學者會認爲這個方法相似與C++中的析構函數,將不少對象、資源的釋放都放在這一函數裏面。其實,這不是一種很好的方式,緣由有三:

其1、GC爲了可以支持finalize函數,要對覆蓋這個函數的對象做不少附加的工做。

其2、在finalize運行完成以後,該對象可能變成可達的,GC還要再檢查一次該對象是不是可達的。所以,使用 finalize會下降GC的運行性能。

其3、因爲GC調用finalize的時間是不肯定的,所以經過這種方式釋放資源也是不肯定的。

 

一般,finalize用於一些不容易控制、而且很是重要資源的釋放,例如一些I/O的操做,數據的鏈接。這些資源的釋放對整個應用程序是很是關鍵的。在這種狀況下,程序員應該以經過程序自己管理(包括釋放)這些資源爲主,以finalize函數釋放資源方式爲輔,造成一種雙保險的管理機制,而不該該僅僅依靠finalize來釋放資源。

下面給出一個例子說明,finalize函數被調用之後,仍然多是可達的,同時也可說明一個對象的finalize只可能運行一次。

 

class MyObject {
		Test main; 		// 記錄Test對象,在finalize中時用於恢復可達性

		public MyObject(Test t) {
			main = t; 	// 保存Test 對象
		}

		protected void finalize() {
			main.ref = this;	// 恢復本對象,讓本對象可達
			System.out.println("This is finalize");		// 用於測試finalize只運行一次
		}
	}

	class Test {
		MyObject ref;

		public static void main(String[] args) {
			Test test = new Test();
			test.ref = new MyObject(test);
			test.ref = null; 	// MyObject對象爲不可達對象,finalize將被調用
			System.gc();
			if (test.ref != null)
				System.out.println("My Object還活着");
		}
	}

 

運行結果:

  This is finalize

  My Object還活着

此例子中須要注意,雖然MyObject對象在finalize中變成可達對象,可是下次回收時候,finalize卻再也不被調用,由於finalize函數最多隻調用一次。

 

7、GC程序交互

程序如何與GC進行交互呢? Java2加強了內存管理功能,增長了一個java.lang.ref包,詳見個人開源項目:src-jdk1.7.0_02

其中定義了三種引用類。這三種引用類分別爲:SoftReference、 WeakReference、 PhantomReference

經過使用這些引用類,程序員能夠在必定程度與GC進行交互,以便改善GC的工做效率,這些引用類的引用強度介於可達對象和不可達對象之間。

建立一個引用對象也很是容易,例如:若是你須要建立一個Soft Reference對象,那麼首先建立一個對象,並採用普通引用方式(可達對象);而後再建立一個SoftReference引用該對象;最後將普通引用設置爲null。經過這種方式,這個對象就只有一個Soft Reference引用。同時,咱們稱這個對象爲Soft Reference 對象。

Soft Reference的主要特色是據有較強的引用功能。只有當內存不夠的時候,才進行回收這類內存,所以在內存足夠的時候,它們一般不被回收。另外,這些引用對象還能保證在Java拋出OutOfMemory 異常以前,被設置爲null。它能夠用於實現一些經常使用圖片的緩存,實現Cache的功能,保證最大限度的使用內存而不引發OutOfMemory。如下給出這種引用類型的使用僞代碼:

 

// 申請一個圖像對象
	  Image image=new Image();		// 建立Image對象
	  …
	  // 使用 image
	  …
	  // 使用完了image,將它設置爲soft 引用類型,而且釋放強引用;
	  SoftReference sr=new SoftReference(image);
	  image=null;
	  …
	  // 下次使用時
	  if (sr!=null) 
			image=sr.get();
	  else{
	  		image=new Image();	//因爲GC因爲低內存,已釋放image,所以須要從新裝載;
	  		sr=new SoftReference(image);
	  }

 

Weak引用對象與Soft引用對象的最大不一樣就在於:GC在進行回收時,須要經過算法檢查是否回收Soft引用對象,而對於Weak引用對象,GC老是進行回收。Weak引用對象更容易、更快被GC回收。雖然,GC在運行時必定回收Weak對象,可是複雜關係的Weak對象羣經常須要好幾回GC的運行才能完成。Weak引用對象經常用於Map結構中,引用數據量較大的對象,一旦該對象的強引用爲null時,GC可以快速地回收該對象空間。

Phantom引用的用途較少,主要用於輔助finalize函數的使用。Phantom對象指一些對象,它們執行完了finalize函數,併爲不可達對象,可是它們尚未被GC回收。這種對象能夠輔助finalize進行一些後期的回收工做,咱們經過覆蓋Reference的clear()方法,加強資源回收機制的靈活性。

 

8、Java編程建議

根據GC的工做原理,咱們能夠經過一些技巧和方式,讓GC運行更加有效率,更加符合應用程序的要求。一些關於程序設計的幾點建議:

1)最基本的建議就是儘早釋放無用對象的引用。大多數程序員在使用臨時變量的時候,都是讓引用變量在退出活動域(scope)後,自動設置爲 null.咱們在使用這種方式時候,必須特別注意一些複雜的對象圖,例如數組,隊列,樹,圖等,這些對象之間有相互引用關係較爲複雜。對於這類對象,GC 回收它們通常效率較低。若是程序容許,儘早將不用的引用對象賦爲null,這樣能夠加速GC的工做。

2)儘可能少用finalize函數。finalize函數是Java提供給程序員一個釋放對象或資源的機會。可是,它會加大GC的工做量,所以儘可能少採用finalize方式回收資源。

3)若是須要使用常用的圖片,可使用soft應用類型。它能夠儘量將圖片保存在內存中,供程序調用,而不引發OutOfMemory.

4)注意集合數據類型,包括數組,樹,圖,鏈表等數據結構,這些數據結構對GC來講,回收更爲複雜。另外,注意一些全局的變量,以及一些靜態變量。這些變量每每容易引發懸掛對象(dangling reference),形成內存浪費。

5)當程序有必定的等待時間,程序員能夠手動執行System.gc(),通知GC運行,可是Java語言規範並不保證GC必定會執行。使用增量式GC能夠縮短Java程序的暫停時間。

 

引用:

Java內存模型及GC原理

Permanent Space 和 Heap Space

相關文章
相關標籤/搜索