Java OOM 常見情況
原文:https://blog.csdn.net/qq_42447950/article/details/81435080
1)什么是OOM?
OOM,全稱“Out Of Memory”,翻譯成中文就是“內存用完了”,來源於java.lang.OutOfMemoryError。看下關於的官方說明: Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. 意思就是說,當JVM因為沒有足夠的內存來為對象分配空間並且垃圾回收器也已經沒有空間可回收時,就會拋出這個error(注:非exception,因為這個問題已經嚴重到不足以被應用處理)。
2)為什么會OOM?
為什么會沒有內存了呢?原因不外乎有兩點:
1)分配的少了:比如虛擬機本身可使用的內存(一般通過啟動時的VM參數指定)太少。
2)應用用的太多,並且用完沒釋放,浪費了。此時就會造成內存泄露或者內存溢出。
內存泄露:申請使用完的內存沒有釋放,導致虛擬機不能再次使用該內存,此時這段內存就泄露了,因為申請者不用了,而又不能被虛擬機分配給別人用。
內存溢出:申請的內存超出了JVM能提供的內存大小,此時稱之為溢出。
在之前沒有垃圾自動回收的日子里,比如C語言和C++語言,我們必須親自負責內存的申請與釋放操作,如果申請了內存,用完后又忘記了釋放,比如C++中的new了但是沒有delete,那么就可能造成內存泄露。偶爾的內存泄露可能不會造成問題,而大量的內存泄露可能會導致內存溢出。
而在Java語言中,由於存在了垃圾自動回收機制,所以,我們一般不用去主動釋放不用的對象所占的內存,也就是理論上來說,是不會存在“內存泄露”的。但是,如果編碼不當,比如,將某個對象的引用放到了全局的Map中,雖然方法結束了,但是由於垃圾回收器會根據對象的引用情況來回收內存,導致該對象不能被及時的回收。如果該種情況出現次數多了,就會導致內存溢出,比如系統中經常使用的緩存機制。Java中的內存泄露,不同於C++中的忘了delete,往往是邏輯上的原因泄露。
3)OOM的類型
JVM內存模型:
按照JVM規范,JAVA虛擬機在運行時會管理以下的內存區域:
- 程序計數器:當前線程執行的字節碼的行號指示器,線程私有
- JAVA虛擬機棧:Java方法執行的內存模型,每個Java方法的執行對應着一個棧幀的進棧和出棧的操作。
- 本地方法棧:類似“ JAVA虛擬機棧 ”,但是為native方法的運行提供內存環境。
- JAVA堆:對象內存分配的地方,內存垃圾回收的主要區域,所有線程共享。可分為新生代,老生代。
- 方法區:用於存儲已經被JVM加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。Hotspot中的“永久代”。
- 運行時常量池:方法區的一部分,存儲常量信息,如各種字面量、符號引用等。
- 直接內存:並不是JVM運行時數據區的一部分, 可直接訪問的內存, 比如NIO會用到這部分。
按照JVM規范,除了程序計數器不會拋出OOM外,其他各個內存區域都可能會拋出OOM。
最常見的OOM情況有以下三種:
- java.lang.OutOfMemoryError: Java heap space ------>java堆內存溢出,此種情況最常見,一般由於內存泄露或者堆的大小設置不當引起。對於內存泄露,需要通過內存監控軟件查找程序中的泄露代碼,而堆大小可以通過虛擬機參數-Xms,-Xmx等修改。
- java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法區溢出了,一般出現於大量Class或者jsp頁面,或者采用cglib等反射機制的情況,因為上述情況會產生大量的Class信息存儲於方法區。此種情況可以通過更改方法區的大小來解決,使用類似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,過多的常量尤其是字符串也會導致方法區溢出。
- java.lang.StackOverflowError ------> 不會拋OOM error,但也是比較常見的Java內存溢出。JAVA虛擬機棧溢出,一般是由於程序中存在死循環或者深度遞歸調用造成的,棧大小設置太小也會出現此種溢出。可以通過虛擬機參數-Xss來設置棧的大小。
4)OOM分析--heapdump
要dump堆的內存鏡像,可以采用如下兩種方式:
設置JVM參數-XX:+HeapDumpOnOutOfMemoryError,設定當發生OOM時自動dump出堆信息。不過該方法需要JDK5以上版本。
使用JDK自帶的jmap命令。"jmap -dump:format=b,file=heap.bin <pid>" 其中pid可以通過jps獲取。
dump堆內存信息后,需要對dump出的文件進行分析,從而找到OOM的原因。常用的工具有:
mat: eclipse memory analyzer, 基於eclipse RCP的內存分析工具。詳細信息參見:http://www.eclipse.org/mat/,推薦使用。
jhat:JDK自帶的java heap analyze tool,可以將堆中的對象以html的形式顯示出來,包括對象的數量,大小等等,並支持對象查詢語言OQL,分析相關的應用后,可以通過http://localhost:7000來訪問分析結果。不推薦使用,因為在實際的排查過程中,一般是先在生產環境 dump出文件來,然后拉到自己的開發機器上分析,所以,不如采用高級的分析工具比如前面的mat來的高效。
這個鏈接:http://www.ibm.com/developerworks/cn/opensource/os-cn-ecl-ma/index.html中提供了一個采用mat分析的例子 。
注意:因為JVM規范沒有對dump出的文件的格式進行定義,所以不同的虛擬機產生的dump文件並不是一樣的。在分析時,需要針對不同的虛擬機的輸出采用不同的分析工具(當然,有的工具可以兼容多個虛擬機的格式)。IBM HeapAnalyzer也是分析heap的一個常用的工具。
Java 堆溢出
GC Roots 到對象之間有可達路徑來避免垃圾回收機智清理這些對象 就會在對象數量達到最大堆的絨容量 然后產生內存溢出異常
Eclipse 的 Debug 頁面中設置虛擬機參數
代碼:
-verbose:gc -Xms20M -Xmx20M Xmn10M -XX:+PrintGCDaetails -XX:SurvivorRatio=8
JAVA堆的大小設置最小值 -Xms
JAVA堆的大小設置最大值 -Xmx
解決異常
首先通過內存映射分析工具 如 Eclipse Memory Analyzer 堆 dump出的異常堆轉儲進行快照解析確認內存中的對象是否是必要的 也就是先分清楚是 內存泄漏 Memory Leak 還是Memory Overflow 如果是內存泄漏 可通過工具進一步查看泄露的對象到GC Roots的引用鏈 就能找到泄露對象是怎么通過路徑與 GC Roots 相關聯導致垃圾收集器無法回收他們如果不存在泄露 就檢查堆參數 -Xmx 與 -Xms 與機器物理內存對比是否還可以調大 從代碼上檢測 是否是某些對象的生命周期過長持有狀態時間過長 嘗試減少代碼運行期間的內存消耗
虛擬機棧 和 本地方法棧 溢出
HotSpot 虛擬機不區分 虛擬機棧 和 本地方法棧 因此Xoss(設置本地方法棧內存大小)參數雖然存在但是是無效的
虛擬機棧 和 本地方法棧兩種異常:
線程請求棧的深度大於虛擬機允許的最大深度 拋出 StackOverflowError
如果虛擬機在擴展棧是無法申請到足夠的內存空間 拋出 Out Of Menmory Error
注:單個線程下 無論是棧幀太大還是虛擬機棧容量太小當內存無法分配的時候拋出的都是StackOverflowError
運行時常量池溢出
如果向運行時常亮去添加內容最簡單的做法就是使用 String.intern() 這個 Narive 方法 該方法作用是如果池中包含一個等於此String對象的字符串 則返回代表池中這個字符串的 String 對象 否則將該 String 對象 的字符串添加到常量池中並返回此 String 對象的引用-XX:PermSize 方法區最小 -XX:MaxPermSize 方法區最大值如果運行時常量池 OOM 提示信息為 OutOfMemoryError : PermGen space 表示運行時常量池屬於方法區的一部分
方法區溢出
方法區是用於存放 Class信息的 如類名 訪問修飾符 常量池 等當前主流AOP框架 (Spring Hibernate 等) 都會使用到 CFLib 這類字節碼技術對類進行加強 增強的類越多 就越需要越大的方法區來保證 動態生成的Class 可以載入內存在動態生成大量 Class 的應用中 需要特別注意類的回收狀況 注:除了使用GCLib字節碼增強意外 常見的還有 JSP 或者動態產生JSP的應用文件(JSP在第一次運行時要編譯為JAVA類)或是基於 OSGi(注: OSGi(Open Service Gateway Initiative)技術是Java動態化模塊化系統的一系列規范。OSGi一方面指維護OSGi規范的OSGI官方聯盟,另一方面指的是該組織維護的基於Java語言的服務(業務)規范。簡單來說,OSGi可以認為是Java平台的模塊層。) 的應用即使是同一個類唄不同的加載器加載也會視為不同的類
本機直接內存溢出
DirectMemory 可通過-XX:MaxDirectMemorySize 指定 如果不指定則和JAVA堆的最大值一樣使用DirectByteBuffer分配內存也會拋出內存溢出異常 但是他拋出異常的時候並沒有向計算機申請分配內存 而是通過計算機內存得知是否可以分配於是拋出異常申請分配內存的方法是unsafe.allocateMemory();
================ End