JVM的內存模型與垃圾回收(整理)


一、JVM的內存模型:

從大的方面來講,JVM的內存模型分為兩大塊:

永久區內存( Permanent space )和堆內存(heap space)。

棧內存(stack space)一般都不歸在JVM內存模型中,因為棧內存屬於線程級別。

每個線程都有個獨立的棧內存空間。

Permanent space里存放加載的Class類級對象如class本身,method,field等等。

heap space主要存放對象實例和數組。

heap space由Old Generation和New Generation組成,Old Generation存放生命周期長久的實例對象,而新的對象實例一般放在New Generation。

New Generation還可以再分為Eden區(聖經中的伊甸園)、和Survivor區,新的對象實例總是首先放在Eden區,Survivor區作為Eden區和Old區的緩沖,可以向Old區轉移活動的對象實例。

 

下圖是JVM在內存空間(堆空間)中申請新對象過程的活動圖: 

沒錯,我們常見的OOM(out of memory)內存溢出異常,就是堆內存空間不足以存放新對象實例時導致。

永久區內存溢出相對少見,一般是由於需要加載海量的Class數據,超過了非堆內存的容量導致。通常出現在Web應用剛剛啟動時,因此Web應用推薦使用預加載機制,方便在部署時就發現並解決該問題。 

棧內存也會溢出,但是更加少見。

堆和棧:

-->Java棧是與每一個線程關聯的,JVM在創建每一個線程的時候,會分配一定的棧空間給線程。它主要用來存儲線程執行過程中的局部變量,方法的返回值,以及方法調用上下文。棧空間隨着線程的終止而釋放。StackOverflowError:如果在線程執行的過程中,棧空間不夠用,那么JVM就會拋出此異常,這種情況一般是死遞歸造成的。

-->Java堆是由所有的線程共享的一塊內存區域,堆用來保存各種JAVA對象,比如數組,線程對象等。

-->堆和棧分離的好處:面向對象的設計,當然除了面向對象的設計帶來的維護性,復用性和擴展性方面的好處外,我們看看面向對象如何巧妙的利用了堆棧分離。如果從JAVA內存模型的角度去理解面向對象的設計,我們就會發現對象它完美的表示了堆和棧,對象的數據放在堆中,而我們編寫的那些方法一般都是運行在棧中,因此面向對象的設計是一種非常完美的設計方式,它完美的統一了數據存儲和運行。

堆內存優化:

調整JVM啟動參數-Xms  -Xmx   -XX:newSize -XX:MaxNewSize,如調整初始堆內存和最大對內存 -Xms256M -Xmx512M。 或者調整初始New Generation的初始內存和最大內存 -XX:newSize=128M -XX:MaxNewSize=128M。 

永久區內存優化:

調整PermSize參數   如  -XX:PermSize=256M -XX:MaxPermSize=512M。

棧內存優化:

調整每個線程的棧內存容量  如  -Xss2048K 

最終,一個運行中的JVM所占的內存= 堆內存  +  永久區內存  +  所有線程所占的棧內存總和 。

 

二、垃圾回收

以下內容轉自http://blog.csdn.net/dc_726/article/details/7934101

垃圾回收包含的內容不少,但順着下面的順序捋清知識也並不難。首先要搞清垃圾回收的范圍(棧需要GC去回收嗎?),然后就是回收的前提條件;
如何判斷一個對象已經可以被回收(這里只重點學習根搜索算法就行了),之后便是建立在根搜索基礎上的三種回收策略,最后便是JVM中對這三種策略的具體實現。
 
1.范圍:要回收哪些區域?
 
Java方法棧、本地方法棧以及PC計數器隨方法或線程的結束而自然被回收,所以這些區域不需要考慮回收問題。Java堆和方法區是GC回收的重點區域,因為一個接口的多個實現類需要的內存不一樣,一個方法的多個分支需要的內存可能也不一樣,而這兩個區域又對立於棧可能隨時都會有對象不再被引用,因此這部分內存的分配和回收都是動態的。
 
2.前提:如何判斷對象已死?
 
    (1)引用計數法
 
引用計數法就是通過一個計數器記錄該對象被引用的次數,方法簡單高效,但是解決不了循環引用的問題。比如對象A包含指向對象B的引用,對象B也包含指向對象A的引用,但沒有引用指向A和B,這時當前回收如果采用的是引用計數法,那么對象A和B的被引用次數都為1,都不會被回收。
 
下面是循環引用的例子,在Hotspot JVM下可以被正常回收,可以證實JVM采用的不是簡單的引用計數法。通過-XX:+PrintGCDetails輸出GC日志。
[java]   view plain copy
  1. package com.cdai.jvm.gc;  
  2.   
  3. public class ReferenceCount {  
  4.   
  5.     final static int MB = 1024 * 1024;  
  6.       
  7.     byte[] size = new byte[2 * MB];  
  8.       
  9.     Object ref;  
  10.       
  11.     public static void main(String[] args) {  
  12.         ReferenceCount objA = new ReferenceCount();  
  13.         ReferenceCount objB = new ReferenceCount();  
  14.         objA.ref = objB;  
  15.         objB.ref = objA;  
  16.           
  17.         objA = null;  
  18.         objB = null;  
  19.           
  20.         System.gc();  
  21.         System.gc();  
  22.     }  
  23.   
  24. }  
[Full GC (System) [Tenured: 2048K->366K(10944K), 0.0046272 secs] 4604K->366K(15872K), [Perm : 154K->154K(12288K)], 0.0046751 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
 
    (2)根搜索
 
通過選取一些根對象作為起始點,開始向下搜索,如果一個對象到根對象 不可達時,則說明此對象已經沒有被引用,是可以被回收的。可以作為根的 對象有:棧中變量引用的對象,類靜態屬性引用的對象,常量引用的對象等。 因為每個線程都有一個棧,所以我們需要選取多個根對象。
 
 
************附:對象復活 
在根搜索中得到的不可達對象並不是立即就被標記成可回收的,而是先進行一次 標記放入F-Queue等待執行對象的finalize()方法,執行后GC將進行二次標記,復活 的對象之后將不會被回收。因此,使對象復活的唯一辦法就是重寫finalize()方法, 並使對象重新被引用。
[java]   view plain copy
  1. package com.cdai.jvm.gc;  
  2.   
  3. public class DeadToRebirth {  
  4.   
  5.     private static DeadToRebirth hook;   
  6.       
  7.     @Override  
  8.     public void finalize() throws Throwable {  
  9.         super.finalize();  
  10.         DeadToRebirth.hook = this;  
  11.     }  
  12.       
  13.     public static void main(String[] args) throws Exception {  
  14.         DeadToRebirth.hook = new DeadToRebirth();  
  15.         DeadToRebirth.hook = null;  
  16.         System.gc();  
  17.         Thread.sleep(500);  
  18.         if (DeadToRebirth.hook != null)  
  19.             System.out.println("Rebirth!");  
  20.         else  
  21.             System.out.println("Dead!");  
  22.           
  23.         DeadToRebirth.hook = null;  
  24.         System.gc();  
  25.         Thread.sleep(500);  
  26.         if (DeadToRebirth.hook != null)  
  27.             System.out.println("Rebirth!");  
  28.         else  
  29.             System.out.println("Dead!");  
  30.     }  
  31.       
  32. }  
要注意的兩點是:
第一,finalize()方法只會被執行一次,所以對象只有一次復活的機會。
第二,執行GC后,要停頓半秒等待優先級很低的finalize()執行完畢。
 
 
3.策略:垃圾回收的算法
 
(1)標記-清除
 
沒錯,這里的標記指的就是之前我們介紹過的兩次標記過程。標記完成后就可以標記為垃圾的對象進行回收了。怎么樣,簡單吧。但是這種策略的缺點很明顯,回收后內存碎片很多,如果之后程序運行時申請大內存,可能會又導致一次GC。雖然缺點明顯,這種策略卻是后兩種策略的基礎。正因為它的缺點,所以促成了后兩種策略的產生。
 
 
(2)標記-復制
 
將內存分為兩塊,標記完成開始回收時,將一塊內存中保留的對象全部復制到另一塊空閑內存中。實現起來也很簡單,當大部分對象都被回收時這種策略也很高效。但這種策略也有缺點,可用內存變為一半了!
 
怎樣解決呢?聰明的程序員們總是辦法多過問題的。可以將堆不按1:1的比例分離,而是按8:1:1分成一塊Eden和兩小塊Survivor區,每次將Eden和Survivor中存活的對象復制到另一塊空閑的Survivor中。這三塊區域並不是堆的全部,而是構成了新生代。
 
從下圖可以看到這三塊區域如何配合完成GC的,具體的對象空間分配以及晉升請參加后面第6條補充。
 
 
為什么不是全部呢?如果回收時,空閑的那一小塊Survivor不夠用了怎么辦?這就是老年代的用處。當不夠用時,這些對象將直接通過分配擔保機制進入老年代。那么老年代也使用標記-復制策略吧?當然不行!老年代中的對象可不像新生代中的,每次回收都會清除掉大部分。如果貿然采用復制的策略,老年代的回收效率可想而知。
 
(3)標記-整理
 
根據老年代的特點,采用回收掉垃圾對象后對內存進行整理的策略再合適不過,將所有存活下來的對象都向一端移動。
 
 
 
4.實現:虛擬機中的收集器
 
(1)新生代上的GC實現
 
Serial:單線程的收集器,只使用一個線程進行收集,並且收集時會暫停其他所有工作線程(Stop the world)。它是Client模式下的默認新生代收集器。
 
ParNew:Serial收集器的多線程版本。在單CPU甚至兩個CPU的環境下,由於線程交互的開銷,無法保證性能超越Serial收集器。
 
Parallel Scavenge:也是多線程收集器,與ParNew的區別是,它是吞吐量優先收集器。吞吐量=運行用戶代碼時間/(運行用戶代碼+垃圾收集時間)。
另一點區別是配置-XX:+UseAdaptiveSizePolicy后,虛擬機會自動調整Eden/Survivor等參數來提供用戶所需的吞吐量。我們需要配置的就是內存大小-Xmx和吞吐量GCTimeRatio。
 
(2)老年代上的GC實現
 
Serial Old:Serial收集器的老年代版本。
 
Parallel Old:Parallel Scavenge的老年代版本。此前,如果新生代采用PS GC的話,老年代只有Serial Old能與之配合。現在有了Parallel Old與之配合,可以在注重吞吐量及CPU資源敏感的場合使用了。
 
CMS:采用的是標記-清除而非標記-整理,是一款並發低停頓的收集器。但是由於采用標記-清除,內存碎片問題不可避免。可以使用-XX:CMSFullGCsBeforeCompaction設置執行幾次CMS回收后,跟着來一次內存碎片整理。
 
 
5.觸發:何時開始GC?
 
Minor GC(新生代回收)的觸發條件比較簡單,Eden空間不足就開始進行Minor GC回收新生代。而Full GC(老年代回收,一般伴隨一次Minor GC)則有幾種觸發條件:
 
(1)老年代空間不足
 
(2)PermSpace空間不足
 
(3)統計得到的Minor GC晉升到老年代的平均大小大於老年代的剩余空間
 
這里注意一點:PermSpace並不等同於方法區,只不過是Hotspot JVM用PermSpace來實現方法區而已,有些虛擬機沒有PermSpace而用其他機制來實現方法區。
 
 
6.補充:對象的空間分配和晉升
 
(1)對象優先在Eden上分配
 
(2)大對象直接進入老年代
 
虛擬機提供了-XX:PretenureSizeThreshold參數,大於這個參數值的對象將直接分配到老年代中。因為新生代采用的是標記-復制策略,在Eden中分配大對象將會導致Eden區和兩個Survivor區之間大量的內存拷貝。
 
(3)長期存活的對象將進入老年代
 
對象在Survivor區中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲)時,就會晉升到老年代中。
 
 
轉載另一篇:

引用博文:

The Java Memory Architecture http://blog.codecentric.de/en/2010/01/the-java-memory-architecture-1-act/

JVM內存管理總結 http://blog.csdn.net/lengyuhong/article/details/5953544

JVM內存管理-深入垃圾收集器與內存分配策略 http://www.iteye.com/topic/802638

JVM內存管理-深入Java內存區域與OOM http://www.iteye.com/topic/802573

圖解JVM內存模型 http://longdick.iteye.com/blog/473866 

圖解JVM在內存中申請對象及垃圾回收流程 http://longdick.iteye.com/blog/468368 

一次Java垃圾收集調優實戰 http://www.iteye.com/topic/212967

JVM參數表 http://blogs.oracle.com/watt/resource/jvm-options-list.html



JVM的內部結構如下圖:



 

JVM主要包括兩個子系統和兩個組件:

 

 

1. 兩個子系統分別是Class loader子系統和Execution engine(執行引擎) 子系統;

 

1.1 Class loader子系統的作用:根據給定的全限定名類名(如 java.lang.Object)來裝載class文件的內容到 Runtime data area中的method area(方法區域)。Java程序員可以extends java.lang.ClassLoader類來寫自己的Class loader。

 

1.2 Execution engine子系統的作用:執行classes中的指令。任何JVM specification實現(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好壞主要就取決於他們各自實現的Execution engine的好壞。

 

2. 兩個組件分別是Runtime data area (運行時數據區域)組件和Native interface(本地接口)組件。

 

2.1 Native interface組件:與native libraries交互,是其它編程語言交互的接口。當調用native方法的時候,就進入了一個全新的並且不再受虛擬機限制的世界,所以也很容易出現JVM無法控制的native heap OutOfMemory。

 

2.2 Runtime Data Area組件:這就是我們常說的JVM的內存了。它主要分為五個部分——

1、Heap (堆):一個Java虛擬實例中只存在一個堆空間,Java堆是被所有線程共享的,在虛擬機啟動時創建。Java堆的唯一目的就是存放對象實例,絕大部分的對象實例都在這里分配。Java堆內還有更細致的划分:新生代、老年代,再細致一點的:eden、from survivor、to survivor,甚至更細粒度的本地線程分配緩沖(TLAB)等,無論對Java堆如何划分,目的都是為了更好的回收內存,或者更快的分配內存。

Java堆可以處於物理上不連續的內存空間,它邏輯上是連續的即可,就像我們的磁盤空間一樣。實現時可以選擇實現成固定大小的,也可以是可擴展的,不過當前所有商業的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制)。如果在堆中無法分配內存,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。

 

2、Method Area(方法區域):被裝載的class的信息存儲在Method area的內存中。當虛擬機裝載某個類型時,它使用類裝載器定位相應的class文件,然后讀入這個class文件內容並把它傳輸到虛擬機中。叫“方法區”可能認識它的人還不太多,如果叫永久代(Permanent Generation)它的粉絲也許就多了。它還有個別名叫 做Non-Heap(非堆)。

方法區中存放了每個Class的結構信息,包括常量池、字段描述、方法描述等等。Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量表(constant_pool table),用於存放編譯期已可知的常量,這部分內容將在類加載后進入方法區(永久代)存放。但是Java語言並不要求常量一定只有編譯期預置入Class的常量表的內容才能進入方法區常量池,運行期間也可將新內容放入常量池(最典型的String.intern()方法)。

運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法在申請到內存時會拋出OutOfMemoryError異常。

 

3、Java Stack(java的棧):虛擬機只會直接對Java stack執行兩種操作:以幀為單位的壓棧或出棧。棧描述的是Java方法調用的內存模型:每個方法被執行的時候,都會同時創建一個幀(Frame)用於存儲本地變量表、操作棧、動態鏈接、方法出入口等信息。每一個方法的調用至完成,就意味着一個幀在VM棧中的入棧至出棧的過程。


4、Program Counter(程序計數器):每一個線程都有它自己的PC寄存器,也是該線程啟動時創建的。PC寄存器的內容總是指向下一條將被執行指令的餓地址,這里的地址可以是一個本地指針,也可以是在方法區中相對應於該方法起始指令的偏移量。

  
5、Native method stack(本地方法棧):保存native方法進入區域的地址.
以上五部分只有Heap 和Method Area是被所有線程的共享使用的;而Java stack, Program counter 和Native method stack是以線程為粒度的,每個線程獨自擁有自己的部分。

 

此外還有本機直接內存的管理(Direct Memory) -- 直接內存並不是虛擬機運行時數據區的一部分,它根本就是本機內存而不是VM直接管理的區域。

顯然本機直接內存的分配不會受到Java堆大小的限制,但是即然是內存那肯定還是要受到本機物理內存(包括SWAP區或者Windows虛擬內存)的限制的,一般服務器管理員配置JVM參數時,會根據實際內存設置-Xmx等參數信息,但經常忽略掉直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操作系統級的限制),而導致動態擴展時出現OutOfMemoryError異常。

 

 

 

JVM內存模型實例以及參數對應:

 

 

  • Model1
 
 
 
  • Model2
 
 
  • 對照表:

Model-1

Model-2

Exception

JVM Options

Method Area

Perm

java.lang.OutOfMemoryError: PermGen space

-XX:PermSize=<value>

-XX:MaxPermSize=<value>

Heap

Young Tenured

java.lang.OutOfMemoryError: Java heap space

-Xms<size>

-Xmx<size>

-Xmn<size>

-XX:newSize

-XX:MaxNewSize

-XX:NewRatio=<value>

-XX:SurvivorRatio=<value>

Thread-1…N

NULL

java.lang.StackOverflowError

-Xss<size>

*Memory Size of Runtime JVM = Heap + Perm + Sum(Thread-1...N)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM