一 JVM記憶體模型
1.1 Java棧
Java棧是與每一個線程關聯的,JVM在建立每一個線程的時候,會配置設定一定的棧空間給線程。它主要用來存儲線程執行過程中的局部變量,方法的傳回值,以及方法調用上下文。棧空間随着線程的終止而釋放。
StackOverflowError:如果線上程執行的過程中,棧空間不夠用,那麼JVM就會抛出此異常,這種情況一般是死遞歸造成的。
1.2 堆
Java中堆是由所有的線程共享的一塊記憶體區域,堆用來儲存各種JAVA對象,比如數組,線程對象等。
1.2.1 Generation
JVM堆一般又可以分為以下三部分:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZwpmLmJGMmBDOyUmM2MjMtUGZ0kTL3cjYz0SZ2cDZtAzNxMzY2QGOvwVM0UzNwIzLcRnbl1GajFGd0F2LcRWYvxGc19CXt92YuUWelRXauwGZvw1LcpDc0RHaiojIsJye.jpg)
Ø 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中預設采用的一種方式,我們平時建立的引用都屬于強引用。如果一個對象沒有強引用,那麼對象就會被回收。
public void testStrongReference(){
Object referent = new Object();
Object strongReference = referent;
referent = null;
System.gc();
assertNotNull(strongReference);
}
Ø Soft Reference
軟引用的對象在GC的時候不會被回收,隻有當記憶體不夠用的時候才會真正的回收,是以軟引用适合緩存的場合,這樣使得緩存中的對象可以盡量的再記憶體中待長久一點。
Public void testSoftReference(){
String str = "test";
SoftReference<String> softreference = new SoftReference<String>(str);
str=null;
System.gc();
assertNotNull(softreference.get());
}
Ø Weak reference
弱引用有利于對象更快的被回收,假如一個對象沒有強引用隻有弱引用,那麼在GC後,這個對象肯定會被回收。
Public void testWeakReference(){
String str = "test";
WeakReference<String> weakReference = new WeakReference<String>(str);
str=null;
System.gc();
assertNull(weakReference.get());
}
Ø Phantom reference
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
C oncurrent Collector通過并行的方式進行垃圾收集,這樣就減少了垃圾收集器收集一次的時間,這種GC在實時性要求高于吞吐量的時候比較有用。
此種GC可以用參數-XX:UseConcMarkSweepGC配置,此GC主要用于老生代和Perm代的收集。
參考資料
1 http://developers.sun.com/mobility/midp/articles/garbage/
2 http://developers.sun.com/mobility/midp/articles/garbagecollection2/
3 http://blogs.sun.com/watt/resource/jvm-options-list.html
4 http://java.sun.com/developer/technicalArticles/Programming/turbo/
5 http://www.ibm.com/developerworks/library/j-jtp10283/index.html?S_TACT=105AGX52&S_CMP=cn-a-j
6 http://www.ibm.com/developerworks/library/j-jtp11253/index.html?S_TACT=105AGX52&S_CMP=cn-a-j
7 http://java.sun.com/docs/hotspot/gc1.4.2/
三 垃圾收集政策配置
3.1 吞吐量優先
吞吐量是指GC的時間與運作總時間的比值,比如系統運作了100分鐘,而GC占用了一分鐘,那麼吞吐量就是99%,吞吐量優先一般運用于對響應性要求不高的場合,比如web應用,因為網絡傳輸本來就有延遲的問題,GC造成的短暫的暫停使得使用者以為是網絡阻塞所緻。
吞吐量優先可以通過-XX:GCTimeRatio來指定。
當通過-XX:GCTimeRatio不能滿足系統的要求以後,我們可以更加細緻的來對JVM進行調優。
首先因為要求高吞吐量,這樣就需要一個較大的Young generation,此時就需要引入“Parallel scavenging Collector”,可以通過參數:-XX:UseParallelGC來配置。
java -server -Xms3072m -Xmx3072m -XX:NewSize=2560m -XX:MaxNewSize=2560 XX:SurvivorRatio=2 -XX:+UseParallelGC
當年輕代使用了"Parallel scavenge collector"後,老生代就不能使用"CMS"GC了,在JDK1.6之前,此時老生代隻能采用串行收集,而JDK1.6引入了并行版本的老生代收集器,可以用參數-XX:UseParallelOldGC來配置。
3.1.1 控制并行的線程數
預設情況下,Parallel scavenging Collector 會開啟與cpu數量相同的線程進行并行的收集,但是也可以調節并行的線程數。假如你想用4個并行的線程去收集Young generation的話,那麼就可以配置-XX:ParallelGCThreads=4,此時JVM的配置參數如下:
java -server -Xms3072m -Xmx3072m -XX:NewSize=2560m -XX:MaxNewSize=2560 XX:SurvivorRatio=2 -XX:+UseParallelGC -XX:ParallelGCThreads=4
3.1.2 自動調節新生代
在采用了"Parallel scavenge collector"後,此GC會根據運作時的情況自動調節survivor ratio來使得性能最優,是以"Parallel scavenge collector"應該總是開啟此參數。
此時JVM的參數配置如下:
java -server -Xms3072m -Xmx3072m -XX:+UseParallelGC -XX:ParallelGCThreads=4 -XX:+UseAdaptiveSizePolicy
3.2 響應時間優先
響應時間優先是指GC每次運作的時間不能太久,這種情況一般使用與對及時性要求很高的系統,比如股票系統等。
響應時間優先可以通過參數-XX:MaxGCPauseMillis來配置,配置以後JVM将會自動調節年輕代,老生代的記憶體配置設定來滿足參數設定。
在一般情況下,JVM的預設配置就可以滿足要求,隻有預設配置不能滿足系統的要求時候,才會根據具體的情況來對JVM進行性能調優。如果采用預設的配置不能滿足系統的要求,那麼此時就可以自己動手來調節。
此時"Young generation"可以采用"Parallel copying collector",而"Old generation"則可以采用"Concurrent Collector",
舉個例子來說,以下參數設定了新生代用Parallel Copying Collector,老生代采用CMS收集器。
java -server -Xms512m -Xmx512m -XX:NewSize=64m -XX:MaxNewSize=64m-XX:SurvivorRatio=2 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
此時需要注意兩個問題:
1 如果沒有指定-XX:+UseParNewGC,則采用預設的非并行版本的copy collector.
2 如果在一個單CPU的系統上設定了-XX:+UseParNewGC ,則預設還是采用預設的copy collector.
3.2.1 控制并行的線程數
預設情況下,Parallel copy collector啟動和CPU數量一樣的線程,也可以通過參數-XX:ParallelGCThreads來指定,比如你想用3個線程去進行并發的複制收集,那麼可以改變上述參數如下:
java -server -Xms512m -Xmx512m -XX:NewSize=64m -XX:MaxNewSize=64m-XX:SurvivorRatio=2 -XX:ParallelGCThreads=4 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
3.2.2 控制并發收集的臨界值
預設情況下,CMS gc在"old generation"空間占用率高于68%的時候,就會進行垃圾收集,而如果想控制收集的臨界值,可以通過參數:-XX:CMSInitiatingOccupancyFraction來控制,比如改變上述的JVM配置如下:
java -server -Xms512m -Xmx512m -XX:NewSize=64m -XX:MaxNewSize=64m-XX:SurvivorRatio=2 -XX:ParallelGCThreads=4 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:CMSInitiatingOccupancyFraction=35
四 GC觸發以及常見的記憶體錯誤
4.1 Minor GC的觸發
Minor GC主要負責收集Young Generation,Minor GC一般在新生代不夠用的情況下觸發,比如我們一次性建立了很多對象等。
List<byte[]> buffer = new ArrayList<byte[]>();for(int i=0;i<8*1024;i++){
buffer.add(new byte[1024]);
}
以上代碼通過一個位元組數組的List模拟觸發Minor gc,設定JVM參數如下:
-verbose:gc -Xmn10M -Xms64M -Xmx64M -XX:+PrintGC
設定以上參數以後,因為-Xmn=10M,預設-XX:SurvivorRatio=8 ,則eden的空間大小為8M,當eden對象大小超過8M的時候就會觸發Minor gc.
運作的結果如下:
[GC 8192K->8030K(64512K), 0.0243391 secs]
從運作結果可以看出,gc前和gc後的eden區的占用情況,需要注意的是括号裡(64512)這個數值時63M,它不包括一塊Survivor 空間。
這裡需要注意的一點就是,如果建立的對象大于eden的大小,那麼将不會通過Survivor空間複制,直接轉移到old generation.
調整以上代碼如下:
List<byte[]> buffer = new ArrayList<byte[]>();
buffer.add(new byte[8*1024*1024]);
通過同樣的JVM參數運作,則發現不會觸發Minor gc,這是因為對象超過了eden的大小,進而直接配置設定到了Old generation.
4.2 Major GC的觸發
4.2.1 Old Generation空間滿或者接近某一個比例
Old generation 空間滿是因為Young generation提升到Old generation的對象+Old generation的本來的大小已經接近或者超過了Old generation的大小。對于CMS GC,當Old generation空間使用率接近某一個比例,可以通過參數-XX:CMS InitialingOccupancyFraction,此參數表示Old generation的使用率,預設為68%。
Young generation對象提升到Old generation對象有以下三種情況:
Ø 配置設定的對象大于eden空間的大小
Ø 在Young generation代中經過了-XX:MaxTenuringThreshold次複制任然存活的對象
Ø Minor gc的時候,放不進to survivor的對象
當Major GC以後,如果還沒有足夠的空間可以用的話,此時就會抛出java.lang.OutOfMemory:java heap space,當出現此錯誤的時候,說明可能存在記憶體洩露現象的,這時候就需要我們對程式進行檢檢視看什麼地方存在記憶體洩露的。
我們可以通過以下代碼來模拟一下java.lang.OutOfMemory:java heap space的發生:
List<byte[]> buffer = new ArrayList<byte[]>();
buffer.add(new byte[10*1024*1024]);
以上代碼配置設定了一個10M的位元組數組,我們通過以下的參數運作:
-verbose:gc -Xmn10M -Xms20M -Xmx20M -XX:+PrintGC
以上參數指定Young generation的空間大小為10M,Old generation空間大小為10M。
運作結果如下:
[GC 327K->134K(19456K), 0.0056516 secs]
[Full GC 134K->134K(19456K), 0.0178891 secs]
[Full GC 134K->131K(19456K), 0.0141412 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Test.main(Test.java:30)
從運作結果可以看出,JVM進行了一次Minor gc和兩次的Major gc,從Major gc的輸出可以看出,gc以後old區使用率為134K,而位元組數組為10M,加起來大于了old generation的空間,是以抛出了異常,如果調整-Xms21M,-Xmx21M,那麼就不會觸發gc操作也不會出現異常了。
4.2.2 Perm Generation 空間滿
Perm Generation空間主要存放Class對象,Field,Method對象,當一次性加載太多的類或者在熱部署以後不解除安裝類的情況(比如在Jboss伺服器中,如果經常熱部署一些應用就會出現Perm 空間溢出)就會造成Perm Generation被占滿,此時就會出現:
java.lang.OutOfMemory:PermGen space,在出現此異常的時候,如果是因為熱部署引起的,我們重新啟動AS就可以了,如果是因為加載的類太多,此時可以通過-XX:PermSize和-XX:MaxPermSize調整。
4.3 常見記憶體錯誤分析
4.3.1 StackOverflowError
java.lang.StackOverflowError錯誤表示JVM棧溢出,出現這個錯誤的原因一般都是遞歸的層次太深,或者無限的遞歸造成的。出現這種錯誤的時候首先要對應用程式進行檢查,看看是那些代碼造成了棧溢出,如果是遞歸造成的可以改為疊代方式實作。
JVM同樣也提供了一個參數來讓我們調節運作時棧空間的大小。-XX:Xss=256K表示棧空間最大為256K.我們也可以調大,但是建議不要對此參數進行調節。
4.3.2 OutOfMemoryError: Java heap space.
java.lang.OutOfMemoryError: Java heap space這個錯誤表示JVM的新生代和老生代的記憶體不足。出現這個錯誤說明應用程式出現了記憶體溢出或者程式所需要的記憶體大于JVM的記憶體設定了。
遇到這個問題的時候,首先我們可以調節JVM的Heap記憶體的大小,具體可以通過-Xmx -Xms來進行設定,如果設定大以後還是會出現記憶體溢出,那麼說明應用程式本身存在記憶體洩露,這個時候就需要我們對應用程式進行檢查,找出導緻記憶體洩露的地方,然後修正。
4.3.3 OutOfMemory:PermGen space
java.lang.OutOfMemory:PermGen space錯誤是由Perm space空間不足。一般出現這個錯誤是由加載了太多的類或者大量使用了動态代理造成的。如果出現了這個錯誤,我們可以将Perm空間調大一點。
-XX:PermSize=16M -XX:MaxPermSize=64M
參考資料
1 http://developers.sun.com/mobility/midp/articles/garbage/
2 http://developers.sun.com/mobility/midp/articles/garbagecollection2/
3 http://blogs.sun.com/watt/resource/jvm-options-list.html
4 http://java.sun.com/developer/technicalArticles/Programming/turbo/
5 http://www.ibm.com/developerworks/library/j-jtp10283/index.html?S_TACT=105AGX52&S_CMP=cn-a-j
6 http://www.ibm.com/developerworks/library/j-jtp11253/index.html?S_TACT=105AGX52&S_CMP=cn-a-j
7 http://java.sun.com/docs/hotspot/gc1.4.2/