Java內存管理

Java程序實際上是把內存控制的權力交給了Java虛擬機,一旦出現內存泄漏和溢出方面的問題,如果不瞭解虛擬機是怎樣使用內存的,那排查錯誤將會成爲一項異常艱難的工作。而且瞭解了Java的內存管理,有助於優化JVM,從而使得自己的應用獲得最佳的性能體驗。所以還等什麼,趕緊跟着我來一起學習這方面的知識吧~
Java內存管理分爲兩個方面:內存分配和垃圾回收,下面我們一一的來看一下。

Jvm定義了5個區域用於存儲運行時的數據,如下:


第一、程序計數器(PC、Program Counter Register)
程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看做當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時就是通過改變這個計數器的值來取下一條需要執行的字節碼指令,分支、跳轉、循環、異常處理、線程恢復等基礎功能都需要這個計數器來完成。
當線程正在執行的是一個Java方法,這個計數器記錄的是在正在執行的虛擬機字節碼指令的地址;當執行的是Native方法,這個計數器值爲空。
注:每條線程都會有一個獨立的程序計數器,唯一不會出現OOM的。
第二、Java棧/虛擬機棧(VM Stack)

Java棧就是Java中的方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀,這個棧幀用於存儲局部變量表、操作數棧、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區部分會談到)的引用、方法返回地址等信息,每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

局部變量表,顧名思義,想必不用解釋大家應該明白它的作用了吧。就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參)。對於基本數據類型的變量,則直接存儲它的值,對於引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就可以確定其大小了,因此在程序執行期間局部變量表的大小是不會改變的。

操作數棧,想必學過數據結構中的棧的朋友想必對表達式求值問題不會陌生,棧最典型的一個應用就是用來對表達式求值。想想一個線程執行方法的過程中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。因此可以這麼說,程序中的所有計算過程都是在藉助於操作數棧來完成的。

指向運行時常量池的引用,因爲在方法執行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向運行時常量。

方法返回地址,當一個方法執行完畢之後,要返回之前調用它的地方,因此在棧幀中必須保存一個方法返回地址。

異常可能性:對於棧有兩種異常情況,如果線程請求的棧深度大於棧所允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態拓展,在拓展的時無法申請到足夠的內存,將會拋出OutOfMemoryError異常
第三、本地方法棧(Native Method Stack)
本地方法棧與Java棧所發揮的作用是非常相似的,它們之間的區別不過是Java棧執行Java方法,本地方法棧執行的是本地方法。有的虛擬機直接把本地方法棧和虛擬機棧合二爲一。
異常可能性:和Java棧一樣,可能拋出StackOverflowError和OutOfMemeryError異常
第四、Java堆(Heap)
對於大多數應用來說,Java堆是Java虛擬機所管理的內存中最大的一塊,在虛擬機啓動時創建。此內存區域的目的就是存放對象實例以及數組(當然,數組引用是存放在Java棧中的),幾乎所有的對象實例都在這裏分配內存,當然我們後面說到的垃圾回收器的內容的時候,其實Java堆就是垃圾回收器管理的主要區域。
從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(TLAB)。Java堆可以處於物理上不連續的內存空間,只要邏輯上連續的即可。在實現上,既可以實現固定大小的,也可以是擴展的。
異常可能性:如果堆中沒有內存完成實例分配,並且堆也無法再拓展時,將會拋出OutOfMemeryError異常
第五、方法區(Method Area)
方法區它用於存儲已被虛擬機加載的類信息(包括類的名稱、方法信息、字段信息)、常量、靜態變量、以及編譯器編譯後的代碼等數據。
1)運行時常量池
運行時常量池是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類加載器進入方法區後的運行時異常常量池存放。它是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM後,對應的運行時常量池就被創建出來。當然並非Class文件常量池中的內容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中,比如String的intern方法(將字符串的值放到常量池)。
相對而言,垃圾收集行爲在這個區域比較少出現,但並非數據進了方法區就永久的存在了,這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。 
異常可能性:當方法區無法滿足內存分配需求時,將拋出OutOfMemeryError異常
直接內存
直接內存不是虛擬機運行時數據區的一部分,在NIO類中引入一種基於通道與緩衝區的IO方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作,或直接使用Unsafe.allocateMemory,但不推薦這種方式。
直接內存的分配不會受到Java堆大小的限制,但是會受到本機內存大小的限制,所有也可能會拋OutOfMemoryError異常。
說明:
線程私有:程序計數器,Java棧,本地方法棧
線程共享:Java堆,方法區

垃圾回收(garbage collection,簡稱GC)可以自動清空堆中不再使用的對象,由於不需要手動釋放內存,程序員在編程中也可以減少犯錯的機會。利用垃圾回收,程序員可以避免一些指針和內存泄露相關的bug(這一類bug通常很隱蔽),但另一方面,垃圾回收需要耗費更多的計算時間,垃圾回收實際上是將原本屬於程序員的責任轉移給了計算機。

在Java中,對象的是通過引用使用的,如果不再有引用指向對象,那麼我們就再也無從調用或者處理該對象。這樣的對象將不可到達(unreachable)。垃圾回收用於釋放不可到達對象所佔據的內存。這是垃圾回收的基本原則。

Java中的引用類型有4種,由強到弱依次如下:
1) 強引用(StrongReference)是使用最普遍的引用,類似:“Object obj = new Object()” 。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。
2) 軟引用(Soft Reference)是用來描述一些有用但並不是必需的對象,如果內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。
3) 弱引用(WeakReference)也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前,當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。不過,由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。
4) 虛引用(PhantomReference)也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係,一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。

GC在進行回收時,需要通過算法檢查是否回收Soft引用對象,而對於Weak引用對象,GC總是進行回收。Weak引用對象更容易、更快被GC回收。雖然GC在運行時一定回收Weak對象,但是複雜關係的Weak對象羣常常需要好幾次 GC的運行才能完成。Weak引用對象常常用於Map結構中,引用數據量較大的對象,一旦該對象的強引用爲null時,GC能夠快速地回收該對象空間。 

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);  
 }  

虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃 圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是 否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前採取必要的行動。 

下面的例子是將虛引用關聯到引用隊列:

ReferenceQueue refQueue = new ReferenceQueue(); //reference will be stored in this queue for cleanup
DigitalCounter digit = new DigitalCounter();
PhantomReference<DigitalCounter> phantom = new PhantomReference<DigitalCounter>(digit, refQueue);


GC算法

“標記-清除”(Mark-Sweep)算法,分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。


它的主要缺點是:
1、效率問題,標記和清除過程的效率都不高;
2、空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致,當程序在以後的運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作;

“複製”(Copying)算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。


它的主要缺點是:
1、只是這種算法是將內存縮小爲原來的一半,有點過於浪費;
2、對象存活率較高時就要執行較多的複製操作,效率將會變低;

“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存,這樣話連續的內存空間就比較多了。


上面幾種算法是通過分代回收(generational collection)混合在一起的,一般是把Java堆分爲Young Generation(新生代),Old Generation(老年代)和Permanent Generation(持久代),這樣就可以根據各個年代的特點採用最適當的回收算法。


1) 在Young Generation中,有一個叫Eden Space的空間,主要是用來存放新生的對象,還有兩個Survivor Spaces(from、to),它們的大小總是一樣,它們用來存放每次垃圾回收後存活下來的對象。
3) 在Young Generation塊中,垃圾回收一般用Copying的算法,速度快。每次GC的時候,存活下來的對象首先由Eden拷貝到某個SurvivorSpace,當Survivor Space空間滿了後,剩下的live對象就被直接拷貝到OldGeneration中去。因此,每次GC後,Eden內存塊會被清空。
4) 在Old Generation塊中主要存放應用程序中生命週期長的內存對象,垃圾回收一般用mark-compact的算法,速度慢些,但減少內存要求。
5)在Permanent Generation中,主要用來放JVM自己的反射對象,比如類對象和方法對象等。
6) 垃圾回收分多級,0級爲全部(Full)的垃圾回收,會回收Old段中的垃圾;1級或以上爲部分垃圾回收,只會回收Young中的垃圾,內存溢出通常發生於Old段或Perm段垃圾回收後,仍然無內存空間容納新的Java對象的情況。

說明:
from, to: 這兩個區域大小相等,相當於copying算法中的兩個區域,當新建對象無法放入eden區時,將出發minor collection。JVM採用copying算法,將eden區與from區的可到達對象複製到to區。經過一次垃圾回收,eden區和from區清空,to區中則緊密的存放着存活對象。隨後from區成爲新的to區, to區成爲新的from區。如果進行minor collection的時候,發現to區放不下,則將部分對象放入成熟世代。另一方面,即使to區沒有滿,JVM依然會移動世代足夠久遠的對象到成熟世代。如果成熟世代放滿對象,無法移入新的對象,那麼將觸發major collection(Full回收)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章