近期換裝了64位系統,不知道怎麼的Tomcat老是記憶體溢出,經查資料得出以下經驗:
本文介紹了Java記憶體溢出的詳細解決方案。本文總結記憶體溢出主要有兩種情況,而JVM經常調用垃圾回收器解決記憶體堆不足的問題,但是有時仍會有記憶體不足的錯誤。作者分析了JVM記憶體區域組成及JVM設定虛拟記憶體的方式,進而給出了一系列解決方案。
一、記憶體溢出類型
1、java.lang.OutOfMemoryError: PermGen space
JVM 管理兩種類型的記憶體,堆和非堆。堆是給開發人員用的上面說的就是,是在JVM啟動時建立;非堆是留給JVM自己用的,用來存放類的資訊的。它和堆不同,運 行期内GC不會釋放空間。如果web app用了大量的第三方jar或者應用有太多的class檔案而恰好MaxPermSize設定較小,超出了也會導緻這塊記憶體的占用過多造成溢出,或者 tomcat熱部署時侯不會清理前面加載的環境,隻會将context更改為新部署的,非堆存的内容就會越來越多。
PermGen space的全稱是Permanent Generation space,是指記憶體的永久儲存區域,這塊記憶體主要是被JVM存放Class和Meta資訊的,Class在被Loader時就會被放到PermGen space中,它和存放類執行個體(Instance)的Heap區域不同,GC(Garbage Collection)不會在主程式運作期對PermGen space進行清理,是以如果你的應用中有很CLASS的話,就很可能出現PermGen space錯誤,這種錯誤常見在web伺服器對JSP進行pre compile的時候。如果你的WEB APP下都用了大量的第三方jar, 其大小超過了jvm預設的大小(4M)那麼就會産生此錯誤資訊了。
一個最佳的配置例子:(經過本人驗證,自從用此配置之後,再未出現過tomcat死掉的情況)
set JAVA_OPTS=-Xms800m -Xmx800m -XX:PermSize=128M -XX:MaxNewSize=256m -XX:MaxPermSize=256m
2、java.lang.OutOfMemoryError: Java heap space
第 一種情況是個補充,主要存在問題就是出現在這個情況中。其預設空間(即-Xms)是實體記憶體的1/64,最大空間(-Xmx)是實體記憶體的1/4。如果内 存剩餘不到40%,JVM就會增大堆到Xmx設定的值,記憶體剩餘超過70%,JVM就會減小堆到Xms設定的值。是以伺服器的Xmx和Xms設定一般應該 設定相同避免每次GC後都要調整虛拟機堆的大小。假設實體記憶體無限大,那麼JVM記憶體的最大值跟作業系統有關,一般32位機是1.5g到3g之間,而64 位的就不會有限制了。
注意:如果Xms超過了Xmx值,或者堆最大值和非堆最大值的總和超過了實體記憶體或者作業系統的最大限制都會引起伺服器啟動不起來。
垃圾回收GC的角色
JVM調用GC的頻度還是很高的,主要兩種情況下進行垃圾回收:
當應用程式線程空閑;另一個是java記憶體堆不足時,會不斷調用GC,若連續回收都解決不了記憶體堆不足的問題時,就會報out of memory錯誤。因為這個異常根據系統運作環境決定,是以無法預期它何時出現。
根據GC的機制,程式的運作會引起系統運作環境的變化,增加GC的觸發機會。
為了避免這些問題,程式的設計和編寫就應避免垃圾對象的記憶體占用和GC的開銷。顯示調用System.GC()隻能建議JVM需要在記憶體中對垃圾對象進行回收,但不是必須馬上回收,
一個是并不能解決記憶體資源耗空的局面,另外也會增加GC的消耗。
二、JVM記憶體區域組成
簡單的說java中的堆和棧
java把記憶體分兩種:一種是棧記憶體,另一種是堆記憶體
1。在函數中定義的基本類型變量和對象的引用變量都在函數的棧記憶體中配置設定;
2。堆記憶體用來存放由new建立的對象和數組
在函數(代碼塊)中定義一個變量時,java就在棧中為這個變量配置設定記憶體空間,當超過變量的作用域後,java會自動釋放掉為該變量所配置設定的記憶體空間;在堆中配置設定的記憶體由java虛拟機的自動垃圾回收器來管理
堆的優勢是可以動态配置設定記憶體大小,生存期也不必事先告訴編譯器,因為它是在運作時動态配置設定記憶體的。缺點就是要在運作時動态配置設定記憶體,存取速度較慢;
棧的優勢是存取速度比堆要快,缺點是存在棧中的資料大小與生存期必須是确定的無靈活性。
java堆分為三個區:New、Old和Permanent
GC有兩個線程:
新建立的對象被配置設定到New區,當該區被填滿時會被GC輔助線程移到Old區,當Old區也填滿了會觸發GC主線程周遊堆記憶體裡的所有對象。Old區的大小等于Xmx減去-Xmn
java棧存放
棧調整:參數有+UseDefaultStackSize -Xss256K,表示每個線程可申請256k的棧空間
每個線程都有他自己的Stack
三、JVM如何設定虛拟記憶體
提示:在JVM中如果98%的時間是用于GC且可用的Heap size 不足2%的時候将抛出此異常資訊。
提示:Heap Size 最大不要超過可用實體記憶體的80%,一般的要将-Xms和-Xmx選項設定為相同,而-Xmn為1/4的-Xmx值。
提示:JVM初始配置設定的記憶體由-Xms指定,預設是實體記憶體的1/64;JVM最大配置設定的記憶體由-Xmx指定,預設是實體記憶體的1/4。
預設空餘堆記憶體小于40%時,JVM就會增大堆直到-Xmx的最大限制;空餘堆記憶體大于70%時,JVM會減少堆直到-Xms的最小限制。是以伺服器一般設定-Xms、-Xmx相等以避免在每次GC 後調整堆的大小。
提示:假設實體記憶體無限大的話,JVM記憶體的最大值跟作業系統有很大的關系。
簡單的說就32位處理器雖然可控記憶體空間有4GB,但是具體的作業系統會給一個限制,
這個限制一般是2GB-3GB(一般來說Windows系統下為1.5G-2G,Linux系統下為2G-3G),而64bit以上的處理器就不會有限制了
提示:注意:如果Xms超過了Xmx值,或者堆最大值和非堆最大值的總和超過了實體記憶體或者作業系統的最大限制都會引起伺服器啟動不起來。
提示:設定NewSize、MaxNewSize相等,"new"的大小最好不要大于"old"的一半,原因是old區如果不夠大會頻繁的觸發"主" GC ,大大降低了性能
JVM使用-XX:PermSize設定非堆記憶體初始值,預設是實體記憶體的1/64;
由XX:MaxPermSize設定最大非堆記憶體的大小,預設是實體記憶體的1/4。
解決方法:手動設定Heap size
修改TOMCAT_HOME/bin/catalina.bat
在“echo "Using CATALINA_BASE: $CATALINA_BASE"”上面加入以下行:
JAVA_OPTS="-server -Xms800m -Xmx800m -XX:MaxNewSize=256m"
四、性能檢查工具使用
定位記憶體洩漏:
JProfiler工具主要用于檢查和跟蹤系統(限于Java開發的)的性能。JProfiler可以通過時時的監控系統的記憶體使用情況,随時監視垃圾回收,線程運作狀況等手段,進而很好的監視JVM運作情況及其性能。
1. 應用伺服器記憶體長期不合理占用,記憶體經常處于高位占用,很難回收到低位;
2. 應用伺服器極為不穩定,幾乎每兩天重新啟動一次,有時甚至每天重新啟動一次;
3. 應用伺服器經常做Full GC(Garbage Collection),而且時間很長,大約需要30-40秒,應用伺服器在做Full GC的時候是不響應客戶的交易請求的,非常影響系統性能。
因為開發環境和産品環境會有不同,導緻該問題發生有時會在産品環境中發生,通常可以使用工具跟蹤系統的記憶體使用情況,在有些個别情況下或許某個時刻确實是使用了大量記憶體導緻out of memory,這時應繼續跟蹤看接下來是否會有下降,
如果一直居高不下這肯定就因為程式的原因導緻記憶體洩漏。
五、不健壯代碼的特征及解決辦法
1、盡早釋放無用對象的引用。好的辦法是使用臨時變量的時候,讓引用變量在退出活動域後,自動設定為null,暗示垃圾收集器來收集該對象,防止發生記憶體洩露。
對于仍然有指針指向的執行個體,jvm就不會回收該資源,因為垃圾回收會将值為null的對象作為垃圾,提高GC回收機制效率;
2、我們的程式裡不可避免大量使用字元串處理,避免使用String,應大量使用StringBuffer,每一個String對象都得獨立占用記憶體一塊區域;
String str = "aaa";
String str2 = "bbb";
String str3 = str + str2;//假如執行此次之後str ,str2以後再不被調用,那它就會被放在記憶體中等待Java的gc去回收,程式内過多的出現這樣的情況就會報上面的那個錯誤,建議在使用字元串時能使用 StringBuffer就不要用String,這樣可以省不少開銷;
3、盡量少用靜态變量,因為靜态變量是全局的,GC不會回收的;
4、避免集中建立對象尤其是大對象,JVM會突然需要大量記憶體,這時必然會觸發GC優化系統記憶體環境;顯示的聲明數組空間,而且申請數量還極大。
這是一個案例想定供大家警戒
使用jspsmartUpload作檔案上傳,運作過程中經常出現java.outofMemoryError的錯誤,
檢查之後發現問題:元件裡的代碼
m_totalBytes = m_request.getContentLength();
m_binArray = new byte[m_totalBytes];
問題原因是totalBytes這個變量得到的數極大,導緻該數組配置設定了很多記憶體空間,而且該數組不能及時釋放。解決辦法隻能換一種更合适的辦法,至少是不會引發outofMemoryError的方式解決。參考:http://bbs.xml.org.cn/blog/more.asp?name=hongrui&id=3747
5、盡量運用對象池技術以提高系統性能;生命周期
java雖然是自動回收記憶體,但是應用程式,尤其伺服器程式最好根據業務情況指明記憶體配置設定限制。否則可能導緻應用程式宕掉。
舉例說明含義:
-Xms128m
表示JVM Heap(堆記憶體)最小尺寸128MB,初始配置設定
-Xmx512m
表示JVM Heap(堆記憶體)最大允許的尺寸256MB,按需配置設定。
說明:如果-Xmx不指定或者指定偏小,應用可能會導緻java.lang.OutOfMemory錯誤,此錯誤來自JVM不是Throwable的,無法用try...catch捕捉。
PermSize和MaxPermSize指明虛拟機為java永久生成對象(Permanate generation)如,class對象、方法對象這些可反射(reflective)對象配置設定記憶體限制,這些記憶體不包括在Heap(堆記憶體)區之中。
-XX:PermSize=64MB 最小尺寸,初始配置設定
-XX:MaxPermSize=256MB 最大允許配置設定尺寸,按需配置設定
過小會導緻:java.lang.OutOfMemoryError: PermGen space
MaxPermSize預設值和-server -client選項相關。
-server選項下預設MaxPermSize為64m
-client選項下預設MaxPermSize為32m
經驗:
1、慎用最小限制選項Xms,PermSize已節約系統資源。
一 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中預設采用的一種方式,我們平時建立的引用都屬于強引用。如果一個對象沒有強引用,那麼對象就會被回收。
- 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
Concurrent Collector通過并行的方式進行垃圾收集,這樣就減少了垃圾收集器收集一次的時間,這種GC在實時性要求高于吞吐量的時候比較有用。此種GC可以用 參數-XX:UseConcMarkSweepGC配置,此GC主要用于老生代和Perm代的收集。