常見OOM現象


《java 特種兵 上冊》 3.6 常見的OOM現象( 136-146頁),看此節后的總結。

OOM的實際場景是很多的,這里介紹常見的,同時結合網絡與實際測試中的一些資料信息。

 

一.HeapSize OOM(堆空間內存溢出)

關鍵字:java.lang.OutOfMemoryError:java heap space

意思:堆空間溢出。老年代區域剩余的內存,已經無法滿足將要晉升到老年代區域的對象大小,會報此錯。

 一般來說,絕大部分都是這種情況。大量空間占據了堆空間,而這些對象持有強引用,導致無法回收,當對象大小之和大於由xmx參數指定的堆空間大小時,溢出錯誤就發生了。

發生堆內存不足的原因可能有:

1)設置的堆內存太小,而系統運行需要的內存要超過這個設置值

2)內存泄露。關注系統穩定運行期,full gc每次gc后的可用內存值是否一直在增大。

3)由於設計原因導致系統需要過多的內存,如系統中過多地緩存了數據庫中的數據,這屬於設計問題,需要通過設計減少內存的使用。

4)分析線程執行模型和它們持有的JVM里的短生命對象

 

代碼層面處理方式和手段:

1)看代碼是否有問題;.eg:List.add(" ")在一個死循環中不斷的調用add卻沒有remove。---間接內存泄露

2)代碼沒有問題,並發導致。

並發導致內存還不能被gc掉,或很多對象還會引用,那就說明對象所在的生命周期范圍內的代碼部分還沒執行結束。

解決方法有:

1.代碼提速。這樣可以使得相同對象的生存時間更短。更快被GC。

2.對於長生命周期對象(如I/O操作)對象后續不用了,objecft=null可以輔助GC,一旦方法脫離了作用域,相應的局部變量應用就會被注銷。

3.代碼無法優化后,程序跑的飛快,還是出現OOM,考慮到去修改參數配置

 eg:堆空間的大小,堆空間要設置的足夠大(相對),如果太大,發生FULL GC會很恐怖

4.內存泄露:內存可能在某些情況增加幾十字節空間但未能釋放,每次被GC,很老的對象被GC的較慢。

eg1:Session中放數據,Session回話消失才會被注銷,但是會話要很長的時間才會被注銷。

eg2.不斷full gc,每次時間變長頻率變小。當次數達到一定量時,平均full gc時間達到一定比例時會報:OutOfMemoryError:GC over head limit exceeded

不斷的FULLGC就是不拋出OOM的現象,出現這種現象通常是一些藏匿的Bug或配置導致。

eg3:Tomcat的session導致,session信息保存在全局的currentHashMap中,大量的HTTPClient訪問創建的臨時session。但並沒有保存系統中為之分配的SessionKey中和相關的Cookies信息,導致每次請求都創建session(幾十個字節,request.getSession被調用時),一般看不出來,是一個堆積如山的過程。

多種OOM,大多是指系統一個靜態引用指向了一個只增不減的對象(例如 hashMap)

 

GC效率低下引起的OOM

如果堆空間小,那GC所占時間就多,回收所釋放的內存不會少。根據GC占用的系統時間,以及釋放內存的大小,虛機會評估GC的效率,一旦虛機認為GC的效率過低,就有可能直接拋出OOM異常。但這個判定不會太隨意。

Sun 官方對此的定義是:“並行/並發回收器在GC回收時間過長時會拋出OutOfMemroyError。過長的定義是,超過98%的時間用來做GC並且回收 了不到2%的堆內存。用來避免內存過小造成應用不能正常工作。“

一般虛機會檢查幾項:

  ● 花在GC上的時間是否超過了98%

  ● 老年代釋放的內存是否小於2%

  ● eden區釋放的內存是否小於2%

  ● 是否連續最近5次GC都出現了上述幾種情況(注意是同時出現)

只有滿足所有條件,虛機才會拋出OOM:GC overhead limit exceeded

這個只是輔助作用,幫助提示系統分配的堆可能大小,並不強制開啟。有關閉開關-XX:-UseGCOverheadLimit來禁止這種。

此參數的意義:當頻繁Full GC導致程序僵死現象,一致耗着,如果加上-XX:+UseGCOverheadLimit參數就可以讓程序提前退出,避免僵死程序長期占用資源。

 

遇到此種情況如何解決?

解決這種問題兩種方法:

1)增加參數,- XX:-UseGCOverheadLimit,關閉這個特性,同時增加heap大小,-Xmx1024m。

2)排查並優化消耗內存資源代碼。

如果生產環境中遇到了這個問題,在不知道原因時可以通過-verbose:gc -XX:+PrintGCDetails看下到底什么原因造成了異常。通常原因都是因為old區占用過多導致頻繁Full GC,最終導致GC overhead limit exceed。如果gc log不夠可以借助於JProfile等工具查看內存的占用,old區是否有內存泄露。分析內存泄露還有一個方法-XX:+HeapDumpOnOutOfMemoryError,這樣OOM時會自動做Heap Dump,可以拿MAT來排查了。還要留意young區,如果有過多短暫對象分配,可能也會拋這個異常。

日志的信息不難理解,就是每次gc時打條日志,記錄GC的類型,前后大小和時間。舉個例子。

33.125: [GC [DefNew: 16000K->16000K(16192K), 0.0000574 secs][Tenured: 2973K->2704K(16384K), 0.1012650 secs] 18973K->2704K(32576K), 0.1015066 secs]

100.667:[Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] 

GC和Full GC代表gc的停頓類型,Full GC代表stop-the-world。箭頭兩邊是gc前后的區空間大小,分別是young區、tenured區和perm區,括號里是該區的總大小。冒號前面是gc發生的時間,單位是秒,從jvm啟動開始計算。DefNew代表Serial收集器,為Default New Generation的縮寫,類似的還有PSYoungGen,代表Parallel Scavenge收集器。這樣可以通過分析日志找到導致GC overhead limit exceeded的原因,通過調節相應的參數解決問題。

 

案例一原文

現象:某應用系統負載升高,響應變慢,發現應用進行頻繁GC,甚至出現OutOfMemroyError: GC overhead limt exceed的錯誤日志。 

原因:因為這個項目是個歷史項目,使用了Hibernate ORM框架,在Hibernate中開啟了二級緩存,使用了Ehcache;但是在Ehcache中沒有控制緩存對象的個數,緩存對象增多,導致內存緊張,所以進行了頻繁的GC操作。 

總結:使用本地緩存(如Ehcache、OSCache、應用內存)時,一定要嚴格控制緩存對象的個數及聲明周期。

 

案例二原文

現象:某應用系統改變用戶web接口(使用JSON作為數據負載)后,性能下降,出現了“OutOfMemoryError: Java heap space”問題並伴有過多的垃圾收集。通過verbose:gc 日志分析確認32-bit HotSpot JVM 老年代空間(1 GB 容量)被完全消耗。

分析過程:

1)線程轉儲數據分析,這種數據通常會告訴在JVM堆里面內存分配的罪魁禍首線程。同樣的它也會顯示任何一個嘗試從遠程系統發送和接受數據的貪婪或者阻塞的線程。觀察到的OOM事件和阻塞線程之間有很近的關聯關系。線程出現了阻塞並且花費了很長時間從遠程系統里讀和接收JSON響應。

2)堆轉儲分析,找出堵塞線程和JVM堆轉儲分析之間的關聯,並且確定這個阻塞線程從堆里占用了多少內存。

--》MAT工具打開文件

-》直方圖查看前通過過ExecuteThread(用於對象的創建和執行)過濾。總共有多少個線程被創建,這些線程總共持有的內存占用是多少。--判斷問題核心是不是源於線程自己的內存占有。

--》深入分析線程內存持有。右鍵點擊ExecuteThread 類並且選擇“列出所有外部引用的對象”。--發現“STUCK(阻塞)”線程和大量內存占用有很大關系。

---》線程局部變量鑒別。展開幾個線程示例並了解內存占用的原始來源。---發現根源在於大量的JSON數據響應。

總結:發現少量的線程花費太多時間去讀取和接收JSON響應,這是大量數據負載的一個明顯的症狀。通過方法局部變量創建的短生命對象也會出現在堆轉儲分析中。然而,其中的一些僅僅能被他們的父線程看到,這是由於他們沒有被其他對象引用,比如這個例子。為了找出真正的調用者你或許也需要分析這個線程棧,隨后通過代碼審查確定最終的根源。在某些情形下,大量的JSON數據可以達到45M以上。使用了32位JVM而且僅僅只有1G的老年代,結果只需要幾個線程就足夠觸發一些性能下降。

 

案例三:jsp頁面導致tomcat內存溢出

背景:測試kafka隊列時,消費者使用一個jsp頁面模擬,使用tomcat中間件。

現象:測試一段時間后,發現JVM內存持續增加,漲到一定值后就一直頻繁做gc且JVM內存並沒有釋放多少,同時頻繁gc引起CPU使用率變高。dump堆內存,分析查看主要是session占用。

分析:發現每請求一次jsp頁面,會產生一個session對象,並且這個對象30分鍾(web.xml里設置的)后才過期。

解決方法:

1.在page指令里添加session=“false”。設置jsp頁面session參數  <%@ page session="false" language="java" pageEncoding="UTF-8" %>

2.在web.xml里把session的過期時間設成0。

參考:JSP頁面導致tomcat內存溢出一例session不及時釋放導致內存溢出的性能問題分析

 

 

 

二.PermGen OOM(永久代內存溢出)

關鍵字:java.lang.OutOfMemoryError:PermGen space

永久代(PermGen space)是JVM實現方法區的地方,因此該異常主要設計到方法區和方法區中的常量池。永久代存放的東西有class和一些常量。perm是放永久區的。如果一個系統定義了太多的類型,那永久區可能會溢出。jdk1.8中,被稱為元數據區。

A.常量池(JDK1.6,JDK1.7以后常量池不會放在永久代中了。)

string常量對象會在常量池(包含類名,方法名,屬性名等信息)中以hash方式存儲和訪問,hash表默認的大小為1009,當string過多時,可以通過修改-xx:stringtableSize參數來增加Hash元素的個數,減少Hash沖突。

當常量池需要的空間大於常量池的實際空間時,也會拋出OutOfMemoryError: PermGen space異常。

例如,Java中字符串常量是放在常量池中的,String.intern()這個方法運行的時候,會檢查常量池中是否存和本字符串相等的對象,如果存在直接返回對常量池中對象的引用,不存在的話,先把此字符串加入常量池,然后再返回字符串的引用。那么可以通過String.intern方法來模擬一下運行時常量區的溢出.

 

B.class加載

由於class被卸載的條件十分的苛刻,這個class所對應的classLoader下面所有的class都沒有活對象的應用才會被卸載。

方法區(Method Area)不僅包含常量池,而且還保存了所有已加載類的元信息。當加載的類過多,方法區放不下所有已加載的元信息時,就會拋出OutOfMemoryError: PermGen space異常。主要有以下場景: 

  • 使用一些應用服務器的熱部署的時候,會遇到熱部署幾次以后發現內存溢出了,這種情況就是因為每次熱部署的后,原來的class沒有被卸載掉。

  • 如果應用程序本身比較大,涉及的類庫比較多,但分給永久代的內存(-XX:PermSize和-XX:MaxPermSize來設置)比較小的時候也可能出現此種問題。

解決方法:在每次CGlib動態創建時,都重新給它設置一個classLoader,這樣在運行代碼就不會出現OOM,會發現大量的class被卸載。

VisualVm工具,查看PermGen標簽頁、類加載標簽頁中的趨勢。隨着類裝載的數量增加,最終會出現了java.lang.OutOfMemoryError: PermGen space。

 

示例:如果不斷產生新類,而沒有回收,那最終很可能會導致永久區溢出。
解決的話從幾方面入手:
● 增加MaxPermSize
● 減少系統需要的類數量
● 使用classloader合理的裝載各個類,並定期進行回收

 

加+PrintGCDetails參數,打印日志可看地gc情況。+TraceClassUnloading,查看日志。  

 

 

什么是Java的永久代(PermGen)內存泄漏 

在反射的過程中,會有一些新的類被動態創建出來,如果系統中頻繁地有新的類被動態創建出來,並且將禁止了class的GC,此時很容易導致永久內存區溢出。

增加-verbose:class很容易觀察到class輸出。

最好不要加-noclassgc選項(運行期間就不會做class的GC),加上這個選項后,勢必需要更大的永久內存,很容易造成永久內存區溢出。

java.lang.OutOfMemoryError: Metaspace

JDK1.7中,存儲在永久代的部分數據就已經轉移到了Java Heap或者是 Native Heap,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。但永久代仍存在於JDK1.7中,並沒完全移除。 
JDK 8.HotSpot JVM使用本地化的內存存放類的元數據,這個空間叫做元空間(Metaspace)。官方定義:”In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace”。元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過以下參數來指定元空間的大小:-XX:MetaspaceSize、-XX:MaxMetaspaceSize。 

VisualVm工具,查看Metaspace標簽頁、類加載標簽頁中的趨勢,可以看到使用的空間大小隨類裝入的數量增加而增加,這也說明了Metaspace是用來存放類的元數據的。 

 

三.DirectBuffer OOM(直接內存內存溢出)

關鍵字:OutOfMemoryError: Direct buffer memory 

Java中普通I/O用輸入/輸出流方式實現,輸入流InputStream(終端—>直接內存->JVM),輸出流(JVM->直接內存->終端),這一過程中有kenel與JVM之間的拷貝(很多次),為了使用直接內存,Java是有一塊區域叫DirectBuffer,不是JavaHeap而是cHeap的一部分。

NIO支持直接內存的使用,也就是通過java代碼,獲得一塊堆外的內存空間,這塊空間是直接向操作系統申請的。它的申請速度比堆內存慢,但訪問速度快。
對於那些可復用的,並會被經常訪問的空間,使用直接內存可提高系統性能。但由於直接內存沒有被java虛機完全托管,若使用不當,也容易觸發溢出,導致宕機。

ByteBuffer有兩種一種是heap ByteBuffer,該類對象分配在JVM的堆內存里面,直接由Java虛擬機負責垃圾回收,一種是direct ByteBuffer是通過jni在虛擬機外內存中分配的。通過jmap無法查看該快內存的使用情況。只能通過top來看它的內存使用情況。JVM堆內存大小可以通過-Xmx來設置,同樣的direct ByteBuffer可以通過-XX:MaxDirectMemorySize來設置,此參數的含義是當Direct ByteBuffer分配的堆外內存到達指定大小后,即觸發Full GC。注意該值是有上限的,默認是64M,最大為sun.misc.VM.maxDirectMemory(),在程序中中可以獲得-XX:MaxDirectMemorySize的設置的值。

direct ByteBuffer通過full gc來回收內存的,direct ByteBuffer會自己檢測情況而調用system.gc(),但是如果參數中使用了DisableExplicitGC那么就無法回收該快內存了,-XX:+DisableExplicitGC標志自動將System.gc()調用轉換成一個空操作,就是應用中調用System.gc()會變成一個空操作。那么如果設置了就需要手動來回收內存。那么除了FULL GC還有別的能回收direct ByteBuffer嗎?CMS GC會回收Direct ByteBuffer的內存。

直接內存不一定觸發full gc(除非直接內存使用量達到了-XX:MaxDirectMemorySize的設置),保證它不溢出的方法是合理進行full gc(full gc時會對這里做gc)的執行,或設定一個系統實際可達的-XX:MaxDirectMemorySize值(默認是-xmx的設置)。

如果系統的堆內存少有GC發生,而直接內存申請頻繁,會比較容易導致直接內存溢出。(32位機器比較明顯)
為避免直接內存溢出,

1)合理執行顯示GC,可降低概率;

2)設置合理的-XX:MaxDirectMemorySize值,避免發生;

3)設置一個較小的堆在32 虛機上可是到更多的內存用於直接內存。

手動釋放本地內存?DirectByteBuffer持有一個Cleaner對象,該對象有一個clean()方法可用於釋放本地內存,需要的時候可以調用這個方法手動釋放本地內存。

如果應用里有任何地方用了direct memory,那么使用-XX:+DisableExplicitGC要小心。如果用了該參數而且遇到direct memory的OOM,可以嘗試去掉該參數看是否能避開這種OOM。如果擔心System.gc()調用造成full GC頻繁,可以嘗試 -XX:+ExplicitGCInvokesConcurrent 參數 ([HotSpot VM] JVM調優的"標准參數"的各種陷阱)。

 

eg:設置-xx:MaxDirectMemorysize:256    分配一個ByteBufferallocateDirect(257*1024*1024)就會導致OOM。

 

本地內存泄露?

分析gc信息,不存在Java堆內存的泄漏,但分析java進程,發現Java進程的總內存越來越大,並且無停止上漲的跡象,直到整個系統崩潰。它的定位比較復雜。

可能原因有:

1)如果系統存在JNI調用,本地內存泄露可能存在於JVM代碼中。結合pmap等命令初步確認。

2)JDK的bug。如果第一步沒問題,通過更新jdk版本,排除下。

3)操作系統bug 

本地內存泄漏可能還會引發異常:java.lang.OutOfMemoryError: unable to create new native thread

 

四.StackOverflowError(棧內存溢出錯誤)

關鍵字:StackOverflowError

棧(JVM Stack)存放主要是棧幀( 局部變量表, 操作數棧 , 動態鏈接 , 方法出口信息 )的地方。注意區分棧和棧幀:棧里包含棧幀。

與線程棧相關的內存異常有兩個:

  • StackOverflowError(方法調用層次太深,內存不夠新建棧幀)

  • OutOfMemoryError(線程太多,內存不夠新建線程)

1.通常都是程序的問題,JVM對棧幀的大小設置已經很大了。

2.程序運行過程中,方法分派時,會分配frame來存放本地變量,棧,pc寄存器等信息,方法再調用方法會導致Java棧空間無止境的增長(死遞歸),Java的解決方法是:設置一個私有棧(不在堆內存,而是在NativeMemory),這個棧的空間大小,通過-Xss來設置,數量級在256K-1MB。如果使用空間超過了-Xss限制,請求新建棧幀時,棧所剩空間小於棧幀所需空間,就會出現StackOverflowError。

3.eg:死遞歸

死遞歸和死循環的區別:死循環類似於while(true)的操作,它的線程棧空間使用不會遞增。而死遞歸需要記錄退回的路徑,遞歸過程中調用方法,每個方法運行過程中的本地變量。也就是要記錄上下文信息。這些信息會隨着內容的增加,占用很大的內存空間。

死遞歸:

eg:1.組件的復用。

     2.子類調用父類(復用父類的方法),父類調用子類(達到多態的效果),這中間要經過許多方法,可能形成環,進而形成死遞歸。

     3.三方框架的問題。

這種一般都是程序有問題。分析它時,輸出的線程棧信息明確地說明了調用路徑。

 

 

五.其他內存溢出

A.過多線程導致OOM:unable to creat new native thread

因為虛擬機會提供一些參數來保證堆以及方法區的分配,剩下的內存基本都由棧來占有,而且每個線程都有自己獨立的棧空間(堆,方法區為線程共有)。所以:

  • 如果把虛擬機參數Xss調大了,每個線程的占用的棧空間也就變大了,那么可以建立的線程數量必然減少

  • 公式:線程棧總可用內存=JVM總內存-(-Xmx的值)- (-XX:MaxPermSize的值)- 程序計數器占用的內存

如果-Xmx或者-XX:MaxPermSize太大,那么留給線程棧可用的空間就越小,在-Xss參數配置的棧容量不變的情況下,可以創建的線程數也就越小。

如果JVM正在請求操作系統創建一個本地線程,而操作系統無法創建的時候,就會出現這個報錯信息。JVM中可以生成的最大線程數量由JVM的堆內存大小、Thread的Stack內存大小、系統最大可創建的線程數量(Java線程的實現是基於底層系統的線程機制來實現的,Windows下_beginthreadex,Linux下pthread_create)三個方面影響。Linux下JVM中可生成的最大Thread數量

簡單理解為一個jvm進程的最大線程數為:虛擬內存/(堆棧大小*1024*1024),也就是說虛擬內存越大或堆棧越小,能創建的線程越多。

 

原因:物理內存不夠用或者OS限制了單個進程使用的最大內存,大量的線程分配時,就有可能導致占用的Native Memory(線程私有棧)很多。 

每個線程開啟都會占用系統內存,線程量太大時,也會導致OOm 。線程的棧空間也是在堆外分配的,和直接內存類似。如果想讓系統支持更多的線程,那應使用一個較小的堆空間。

示例1,想要創建 1500個線程,但1200多失敗了。提示“unable to create new native thread",表示系統創建線程數量已經飽和,其原因是java進程已經達到了可使用的內存上限。

示例2,死循環中創建線程並啟動,線程內部申請變量,本身不退出。而且設置Os對進程內存的限制。

 

分析思路:

1)系統當前有過多的線程,操作系統無法再創建更多的線程。可以通過打印線程堆棧,先查看總的線程數量。可能原因:是否系統創建的線程被阻塞或者死鎖,導致系統中的線程越來越多,直到超過最大限制。或者操作系統自身能創建的線程數量太少。

2)swap分區不足。

3)堆內存設置過大。

4)系統對進程內存限制,用戶最大打開的進程數限制

max user processes用戶最大打開的進程數這個值默認是1024,看官方說明,指用戶最多可創建線程數,因為一個進程最少有一個線程,所以間接影響到最大進程數。

 

解決從以下幾方面下手:

1)減少堆空間  -xmx,可預留更多內存用於線程創建,因此程序可正常執行。

2)減少每一個線程所占用的內存空間,-xss參數指定線程的棧空間     -xmx1g -xss128K    棧空間小了,棧溢出風險會提高。

3)打開/etc/security/limits.d/90-nproc.conf,把對應用戶soft    nproc     1024這行的1024改大就行了。

總體思路:合理減少線程總數,減少最大堆空間,減少線程的棧空間也可行。 系統限制。

 

 

B.request{} byte for {}out of swap

地址空間不夠用(不一定是物理地址,還有swap,顯卡,網卡)

這個錯誤是當虛擬機向本地操作系統申請內存失敗時拋出的。這和用完了堆或者持久化中的內存的情況有些不同。這個錯誤通常是在程序已經逼近平台限制的時候產生的。這個信息是可能已經用光了物理內存以及虛擬內存了。由於虛擬內存通常是用磁盤作為交換分區,因此最先想到的解決方法可能是先增加交換分區的大小,但這個方法可能不太好用。

 

C.IoException:too many open files

這個與OOM無關,但也是系統級別問題。它發生這類問題代表系統的某些設計存在問題或某些使用已經達到了權限。

錯誤提示:打開太多的文件,也可能是本地的socket打開太多,而沒有被關閉,也就是說有太多沒有關閉套接字的導致。

 

遇到OOM時,先大致分析原因,加上內存溢出后dump內存參數,設定參數后,出OOM會dump文件。用MAT或其他工作來分析 。

 

D、java.lang.OutOfMemoryError: Requested array size exceeds VM limit

 當准備創建一個超過虛擬機允許的大小的數組時,這條錯誤就會出現在眼前。64位的操作系統上,JDK7,如果數組的長度是Integer.MAX_VALUE-1,就會出現。

 

其他可參考資料:

寫代碼實現堆溢出、棧溢出、永久代溢出、直接內存溢出


免責聲明!

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



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