JVM内存模型及垃圾收集策略解析(一)
http://www.cnblogs.com/laoyangHJ/archive/2011/08/17/jvm_model.html
JVM內存模型是Java的核心技術之一,之前51CTO曾為大家介紹過JVM分代垃圾回收策略的基礎概念,現在很多編程語言都引入了類似Java JVM的內存模型和垃圾收集器的機制,下面我們將主要針對Java中的JVM內存模型及垃圾收集的具體策略進行綜合的分析。
一 JVM內存模型
1.1 Java棧
Java棧是與每一個線程關聯的,JVM在創建每一個線程的時候,會分配一定的棧空間給線程。它主要用來存儲線程執行過程中的局部變量,方法的返回值,以及方法調用上下文。棧空間隨著線程的終止而釋放。StackOverflowError:如果在線程執行的過程中,棧空間不夠用,那么JVM就會拋出此異常,這種情況一般是死遞歸造成的。
1.2 堆
Java中堆是由所有的線程共享的一塊內存區域,堆用來保存各種JAVA對象,比如數組,線程對象等。
1.2.1 Generation
JVM堆一般又可以分為以下三部分:
◆ Perm
Perm代主要保存class,method,filed對象,這部門的空間一般不會溢出,除非一次性加載了很多的類,不過在涉及到熱部署的應用服務器的時候,有時候會遇到java.lang.OutOfMemoryError : PermGen space 的錯誤,造成這個錯誤的很大原因就有可能是每次都重新部署,但是重新部署后,類的class沒有被卸載掉,這樣就造成了大量的class對象保存在了perm中,這種情況下,一般重新啟動應用服務器可以解決問題。
◆ Tenured
Tenured區主要保存生命周期長的對象,一般是一些老的對象,當一些對象在Young復制轉移一定的次數以后,對象就會被轉移到Tenured區,一般如果系統中用了application級別的緩存,緩存中的對象往往會被轉移到這一區間。
◆ Young
Young區被劃分為三部分,Eden區和兩個大小嚴格相同的Survivor區,其中Survivor區間中,某一時刻只有其中一個是被使用的,另外一個留做垃圾收集時復制對象用,在Young區間變滿的時候,minor GC就會將存活的對象移到空閑的Survivor區間中,根據JVM的策略,在經過幾次垃圾收集后,任然存活于Survivor的對象將被移動到Tenured區間。
1.2.2 Sizing the Generations
JVM提供了相應的參數來對內存大小進行配置。正如上面描述,JVM中堆被分為了3個大的區間,同時JVM也提供了一些選項對Young,Tenured的大小進行控制。
◆ Total Heap
-Xms :指定了JVM初始啟動以后初始化內存
-Xmx:指定JVM堆得最大內存,在JVM啟動以后,會分配-Xmx參數指定大小的內存給JVM,但是不一定全部使用,JVM會根據-Xms參數來調節真正用于JVM的內存
-Xmx -Xms之差就是三個Virtual空間的大小
◆ Young Generation
-XX:NewRatio=8意味著tenured 和 young的比值8:1,這樣eden+2*survivor=1/9
堆內存
-XX:SurvivorRatio=32意味著eden和一個survivor的比值是32:1,這樣一個Survivor就占Young區的1/34.
-Xmn 參數設置了年輕代的大小
◆ Perm Generation
-XX:PermSize=16M -XX:MaxPermSize=64M
Thread Stack
-XX:Xss=128K
1.3 堆棧分離的好處
呵呵,其它的先不說了,就來說說面向對象的設計吧,當然除了面向對象的設計帶來的維護性,復用性和擴展性方面的好處外,我們看看面向對象如何巧妙的利用了堆棧分離。如果從JAVA內存模型的角度去理解面向對象的設計,我們就會發現對象它完美的表示了堆和棧,對象的數據放在堆中,而我們編寫的那些方法一般都是運行在棧中,因此面向對象的設計是一種非常完美的設計方式,它完美的統一了數據存儲和運行。
二 JAVA垃圾收集器
2.1 垃圾收集簡史
垃圾收集提供了內存管理的機制,使得應用程序不需要在關注內存如何釋放,內存用完后,垃圾收集會進行收集,這樣就減輕了因為人為的管理內存而造成的錯誤,比如在C++語言里,出現內存泄露時很常見的。Java語言是目前使用最多的依賴于垃圾收集器的語言,但是垃圾收集器策略從20世紀60年代就已經流行起來了,比如Smalltalk,Eiffel等編程語言也集成了垃圾收集器的機制。
2.2 常見的垃圾收集策略
所有的垃圾收集算法都面臨同一個問題,那就是找出應用程序不可到達的內存塊,將其釋放,這里面得不可到達主要是指應用程序已經沒有內存塊的引用了,而在JAVA中,某個對象對應用程序是可到達的是指:這個對象被根(根主要是指類的靜態變量,或者活躍在所有線程棧的對象的引用)引用或者對象被另一個可到達的對象引用。
2.2.1 Reference Counting(引用計數)
?
引用計數是最簡單直接的一種方式,這種方式在每一個對象中增加一個引用的計數,這個計數代表當前程序有多少個引用引用了此對象,如果此對象的引用計數變為0,那么此對象就可以作為垃圾收集器的目標對象來收集。
優點:
簡單,直接,不需要暫停整個應用
缺點:
???? 1.需要編譯器的配合,編譯器要生成特殊的指令來進行引用計數的操作,比如每次將對象賦值給新的引用,或者者對象的引用超出了作用域等。
???? 2.不能處理循環引用的問題
2.2.2 跟蹤收集器
跟蹤收集器首先要暫停整個應用程序,然后開始從根對象掃描整個堆,判斷掃描的對象是否有對象引用,這里面有三個問題需要搞清楚:
1.如果每次掃描整個堆,那么勢必讓GC的時間變長,從而影響了應用本身的執行。因此在JVM里面采用了分代收集,在新生代收集的時候minor gc只需要掃描新生代,而不需要掃描老生代。2.JVM采用了分代收集以后,minor gc只掃描新生代,但是minor gc怎么判斷是否有老生代的對象引用了新生代的對象,JVM采用了卡片標記的策略,卡片標記將老生代分成了一塊一塊的,劃分以后的每一個塊就叫做一個卡片,JVM采用卡表維護了每一個塊的狀態,當JAVA程序運行的時候,如果發現老生代對象引用或者釋放了新生代對象的引用,那么就JVM就將卡表的狀態設置為臟狀態,這樣每次minor gc的時候就會只掃描被標記為臟狀態的卡片,而不需要掃描整個堆。具體如下圖:
3.GC在收集一個對象的時候會判斷是否有引用指向對象,在JAVA中的引用主要有四種:Strong reference,Soft reference,Weak reference,Phantom reference.
◆ Strong Reference
強引用是JAVA中默認采用的一種方式,我們平時創建的引用都屬于強引用。如果一個對象沒有強引用,那么對象就會被回收。
◆ Soft Reference
軟引用的對象在GC的時候不會被回收,只有當內存不夠用的時候才會真正的回收,因此軟引用適合緩存的場合,這樣使得緩存中的對象可以盡量的再內存中待長久一點。
Weak reference
弱引用有利于對象更快的被回收,假如一個對象沒有強引用只有弱引用,那么在GC后,這個對象肯定會被回收。
Phantom reference
?Phantom Reference(幽靈引用) 與 WeakReference 和 SoftReference 有很大的不同,??因為它的 get() 方法永遠返回 null, 這也正是它名字的由來.PhantomReference 唯一的用處就是跟蹤 referent??何時被 enqueue 到 ReferenceQueue 中.
<插入部分>
RererenceQueue?
當一個 WeakReference 開始返回 null 時, 它所指向的對象已經準備被回收, 這時可以做一些合適的清理工作.? ?將一個 ReferenceQueue 傳給一個 Reference 的構造函數, 當對象被回收時, 虛擬機會自動將這個對象插入到 ReferenceQueue 中, WeakHashMap 就是利用 ReferenceQueue 來清除 key 已經沒有強引用的 entries.
Java代碼
6.??PhantomReference??vs WeakReference?
PhantomReference??有兩個好處, 其一, 它可以讓我們準確地知道對象何時被從內存中刪除, 這個特性可以被用于一些特殊的需求中(例如 Distributed GC,??XWork 和 google-guice 中也使用 PhantomReference 做了一些清理性工作).
其二, 它可以避免 finalization 帶來的一些根本性問題, 上文提到 PhantomReference 的唯一作用就是跟蹤 referent 何時被 enqueue 到 ReferenceQueue 中,??但是 WeakReference 也有對應的功能, 兩者的區別到底在哪呢 ?
這就要說到 Object 的 finalize 方法, 此方法將在 gc 執行前被調用, 如果某個對象重載了 finalize 方法并故意在方法內創建本身的強引用,??這將導致這一輪的 GC 無法回收這個對象并有可能
引起任意次 GC, 最后的結果就是明明 JVM 內有很多 Garbage 卻 OutOfMemory, 使用 PhantomReference 就可以避免這個問題, 因為 PhantomReference 是在 finalize 方法執行后回收的,也就意味著此時已經不可能拿到原來的引用,??也就不會出現上述問題,??當然這是一個很極端的例子, 一般不會出現.
<插入部分/>
2.2.2.1 Mark-Sweep Collector(標記-清除收集器)
標記清除收集器最早由Lisp的發明人于1960年提出,標記清除收集器停止所有的工作,從根掃描每個活躍的對象,然后標記掃描過的對象,標記完成以后,清除那些沒有被標記的對象。
優點:
1 解決循環引用的問題
2 不需要編譯器的配合,從而就不執行額外的指令
缺點:
1.每個活躍的對象都要進行掃描,收集暫停的時間比較長。
2.2.2.2 Copying Collector(復制收集器)復制收集器將內存分為兩塊一樣大小空間,某一個時刻,只有一個空間處于活躍的狀態,當活躍的空間滿的時候,GC就會將活躍的對象復制到未使用的空間中去,原來不活躍的空間就變為了活躍的空間。復制收集器具體過程可以參考下圖:
優點:
1 只掃描可以到達的對象,不需要掃描所有的對象,從而減少了應用暫停的時間
缺點:
1.需要額外的空間消耗,某一個時刻,總是有一塊內存處于未使用狀態
2.復制對象需要一定的開銷
2.2.2.3 Mark-Compact Collector(標記-整理收集器)標記整理收集器汲取了標記清除和復制收集器的優點,它分兩個階段執行,在第一個階段,首先掃描所有活躍的對象,并標記所有活躍的對象,第二個階段首先清除未標記的對象,然后將活躍的的對象復制到堆得底部。標記整理收集器的過程示意圖請參考下圖:Mark-compact策略極大的減少了內存碎片,并且不需要像Copy Collector一樣需要兩倍的空間。
2.3 JVM的垃圾收集策略
?
GC的執行時要耗費一定的CPU資源和時間的,因此在JDK1.2以后,JVM引入了分代收集的策略,其中對新生代采用"Mark-Compact"策略,而對老生代采用了“Mark-Sweep"的策略。其中新生代的垃圾收集器命名為“minor gc”,老生代的GC命名為"Full Gc 或者Major GC".其中用System.gc()強制執行的是Full Gc.
2.3.1 Serial Collector
Serial Collector是指任何時刻都只有一個線程進行垃圾收集,這種策略有一個名字“stop the whole world",它需要停止整個應用的執行。這種類型的收集器適合于單CPU的機器。
Serial Copying Collector
此種GC用-XX:UseSerialGC選項配置,它只用于新生代對象的收集。1.5.0以后。-XX:MaxTenuringThreshold來設置對象復制的次數。當eden空間不夠的時候,GC會將eden的活躍對象和一個名叫From survivor空間中尚不夠資格放入Old代的對象復制到另外一個名字叫To Survivor的空間。而此參數就是用來說明到底From survivor中的哪些對象不夠資格,假如這個參數設置為31,那么也就是說只有對象復制31次以后才算是有資格的對象。這里需要注意幾個個問題:
◆? From Survivor和To survivor的角色是不斷的變化的,同一時間只有一塊空間處于使用狀態,這個空間就叫做From Survivor區,當復制一次后角色就發生了變化。
◆? 如果復制的過程中發現To survivor空間已經滿了,那么就直接復制到old generation.
◆? 比較大的對象也會直接復制到Old generation,在開發中,我們應該盡量避免這種情況的發生。
Serial? Mark-Compact Collector
串行的標記-整理收集器是JDK5 update6之前默認的老生代的垃圾收集器,此收集使得內存碎片最少化,但是它需要暫停的時間比較長。
2.3.2 Parallel Collector?
Parallel Collector主要是為了應對多CPU,大數據量的環境。Parallel Collector又可以分為以下兩種:
Parallel Copying Collector
此種GC用-XX:UseParNewGC參數配置,它主要用于新生代的收集,此GC可以配合CMS一起使用。1.4.1以后Parallel Mark-Compact Collector,此種GC用-XX:UseParallelOldGC參數配置,此GC主要用于老生代對象的收集。1.6.0
Parallel scavenging Collector
此種GC用-XX:UseParallelGC參數配置,它是對新生代對象的垃圾收集器,但是它不能和CMS配合使用,它適合于比較大新生代的情況,此收集器起始于jdk 1.4.0。它比較適合于對吞吐量高于暫停時間的場合,Serial gc和Parallel gc可以用如下的圖來表示:
2.3.3 Concurrent Collector
Concurrent Collector通過并行的方式進行垃圾收集,這樣就減少了垃圾收集器收集一次的時間,這種GC在實時性要求高于吞吐量的時候比較有用。此種GC可以用參數-XX:UseConcMarkSweepGC配置,此GC主要用于老生代和Perm代的收集。
總結
以上是生活随笔為你收集整理的JVM内存模型及垃圾收集策略解析(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据库水平切分的实现原理解析---分库,
- 下一篇: 深入Java虚拟机:JVM中的Stack