1. 常見的幾種OOM
常見的OOM錯誤有以下幾種:
java.lang.StackOverflowError
java.lang.OutOfMemoryError : java heap space
java.lang.OutOfMemoryError : GC overhead limit exceeded
java.lang.OutOfMemoryError : Direct buffer memory
java.lang.OutOfMemoryError : unable to create new native thread
java.lang.OutOfMemoryError : Metaspace
java.lang.OutOfMemoryError: Out of swap space
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
Out of memory:Kill process or sacrifice child
OOM(OutOfMemoryError) 問題歸根結底三點原因:
- 本身資源不夠
- 申請的太多內存
- 資源耗盡
解決思路,換成Java服務分析,三個原因也可以解讀爲:
- 有可能是內存分配確實過小,而正常業務使用了大量內存
- 某一個對象被頻繁申請,卻沒有釋放,內存不斷泄漏,導致內存耗盡
- 某一個資源被頻繁申請,系統資源耗盡,例如:不斷創建線程,不斷髮起網絡連接
因此,針對解決思路,快速定位OOM問題的步驟是:
- 確認是不是內存本身就分配過小
- 找到最耗內存的對象
- 確認是否是資源耗盡
1.1 java.lang.StackOverflowError
原因:在一個函數中調用自己會產生這個錯誤,函數調用棧太深了
代碼示例:
//-Xms10m -Xmx10m
public class StackOverflowErrorDemo {
public static void main(String[] args) {
stackOverflowError();
}
private static void stackOverflowError() {
stackOverflowError();
//Exception in thread "main" java.lang.StackOverflowError
}
}
//控制檯結果
Exception in thread "main" java.lang.StackOverflowError
解決方案:1.避免循環遞歸調用,2.調大棧的內存,3.內存泄露(Memory leak)
1.2 java.lang.OutOfMemoryError : java heap space
原因:1. 創建了一個很大的對象 2.超出預期的訪問量/數據量,3.內存泄漏( Java中的內存泄漏, 就是那些邏輯上不再使用的對象, 卻沒有被 垃圾收集程序 給幹掉. 從而導致垃圾對象繼續佔用堆內存中, 逐漸堆積)
代碼示例:
//-Xms2m -Xmx2m -XX:+HeapDumpOnOutOfMemoryError
public class OutOfMemoryErrorJavaHeapSpaceDemo {
static final int SIZE=3*1024*1024;
public static void main(String[] a) {
int[] i = new int[SIZE];
}
}
//控制檯結果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
內存泄漏的例子
import java.util.*;
public class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static void main(String[] args) {
Map m = new HashMap();
while (true){
for (int i = 0; i < 10000; i++){
if (!m.containsKey(new Key(i))){
m.put(new Key(i), "Number:" + i);
}
}
System.out.println("m.size()=" + m.size());
}
}
}
粗略一看, 可能覺得沒什麼問題, 因爲這最多緩存 10000 個元素嘛! 但仔細審查就會發現, Key 這個類只重寫了 hashCode() 方法, 卻沒有重寫 equals() 方法, 於是就會一直往 HashMap 中添加更多的 Key。
JAVA equals()與hashCode()方法之間的設計實現原則爲:
如果兩個對象相等(使用equals()方法),那麼必須擁有相同的哈希碼(使用hashCode()方法).
即使兩個對象有相同的哈希值(hash code),他們不一定相等.意思就是: 多個不同的對象,可以返回同一個hash值.
hashCode()的默認實現是爲不同的對象返回不同的整數.有一個設計原則是,hashCode對於同一個對象,不管內部怎麼改變,應該都返回相同的整數值.
在上面的例子中,因爲未定義自己的equals()實現,因此默認實現對兩個對象返回兩個不同的整數,這種情況破壞了約定原則。
ThreadLocal 緩存了當前線程所持有的 request 對象的java.lang.OutOfMemoryError : java heap space問題排查
1.3 java.lang.OutOfMemoryError : GC overhead limit exceeded
原因:執行垃圾收集的時間比例太大,有效的運算量太小,默認情況下,如果GC花費的時間超過98% 並且GC回收的內存少於2%,jvm就會拋出這個錯誤
代碼示例:
//-Xmx10M -Xms10m -XX:MaxMetaspaceSize=10M
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
while(true){
list.add(UUID.randomUUID().toString().intern());
}
}
//Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
解決辦法:1. 找到哪類對象佔用了最多的內存,然後看是否增大堆內存,2. 需要進行GC turning
1.4 java.lang.OutOfMemoryError : Direct buffer memory
原因:主要是NIO 引起的,寫NIO程序經常用到ByteBuffer來讀取或者寫入數據,這是一種基於通道和緩衝區的I/O方式,它可以使用native函數庫直接分配堆外內存,然後通過一個存儲在Java堆裏面的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在JAVA堆和native堆中來回切換。
ByteBuffer.allocate(capability) 這種方法是分配JVM堆內存,屬於GC管轄範圍,由於需要拷貝所以速度相對慢
ByteBuffer.allocateDirect(capability)這種方式是分配OS本地內存,不屬於GC範圍,由於不需要內存拷貝所以速度快。JVM執行GC不會回DirectByteBuffer收對象,這時JVM堆內存充足但是本地內存可能使用光了,再次分配本地內存就會出現Direct buffer memory
代碼示例:
//-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
public static void main(String[] args){
System.out.println("maxDirectMemory : " + sun.misc.VM.maxDirectMemory() / (1024 * 1024) + "MB");
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024);
byteBuffer.putChar(‘a');
}
//輸出
[GC (System.gc()) [PSYoungGen: 1315K->464K(2560K)] 1315K->472K(9728K), 0.0008907 secs] [Times: user=0.00 sys=0.00, real=
[Full GC (System.gc()) [PSYoungGen: 464K->0K(2560K)] [ParOldGen: 8K->359K(7168K)] 472K->359K(9728K), [Metaspace: 3037K->
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
解決辦法一:
用來nio,但是direct buffer不夠情況
1)檢查是否直接或間接使用了nio,例如手動調用生成buffer的方法或者使用了nio容器如netty,jetty,tomcat等等;
2)-XX:MaxDirectMemorySize加大,該參數默認是64M,可以根據需求調大試試;
3)檢查JVM參數裏面有無:-XX:+DisableExplicitGC,如果有就去掉.
解決辦法二:實例化client後沒有關閉此對象
調用elasticsearch的client實例化對象後,要及時關閉客戶端對象,沒有關閉,造成實例化的對象越來越多,佔用的內存也越來越多,最後內存就溢出了
/**
* 關閉client
*
* @param client
*/
public static void closeClient(Client client) {
if (client != null)
client.close();
client = null;
}
1.5 java.lang.OutOfMemoryError : unable to create new native thread
原因:1.應用創建了太多的線程,一個應用進程創建了多個線程,超過系統承載能力
2.你的服務器並不允許你的程序創建這麼多線程,Linux系統普通用戶默認單個進程可以創建的線程數是1024個
代碼示例:
while(true){
new Thread(new Runnable(){
public void run() {
try {
Thread.sleep(10000000);
} catch(InterruptedException e) { }
}
}).start();
}
解決方案:
1.想辦法降低你的應用程序創建的線程數量。
2. 對於確實需要創建很多線程的修改liunx配置,擴大Linux默認限制。
命令:ulimit -u 查看
vi /etc/security/limits.d/90-nproc.conf 增加一條用戶記錄配置大小
1.6 java.lang.OutOfMemoryError : Metaspace
原因:由於方法區被移至 Metaspace,所以 Metaspace 的使用量與 JVM 加載到內存中的 class 數量/大小有關。主要原因, 是加載到內存中的 class 數量太多或者體積太大導致元數據區(Metaspace) 已被用滿。Java8 後使用元空間代替了永久代,元空間是方法區在HotSpot中的實現,它與持久代最大的區別是:元空間並不在虛擬機中的內存中而是使用本地內存。
元空間存放的信息:1. 虛擬機加載的類信息 ; 2 常量池;3.靜態變量,4.即時編譯後的代碼
代碼示例:
//-Xmx10M -Xms10m -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
public class Metaspace {
static javassist.ClassPool cp = javassist.ClassPool.getDefault();
public static void main(String[] args) throws Exception{
for (int i = 0; ; i++) {
Class c = cp.makeClass("Metaspace.demo.Generated" + i).toClass();
}
}
}
//Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
解決方案:
1. -XX:MaxMetaspaceSize=512m
但需要注意, 不限制Metaspace內存的大小, 假若物理內存不足, 有可能會引起內存交換(swapping), 嚴重拖累系統性能。 此外,還可能造成native內存分配失敗等問題。
1.7 java.lang.OutOfMemoryError: Out of swap space
原因:往往是由操作系統級別的問題引起的,例如:
1.操作系統配置的交換空間不足。
2.系統上的另一個進程消耗所有內存資源。
3.還有可能是本地內存泄漏導致應用程序失敗,比如:應用程序調用了native code連續分配內存,但卻沒有被釋放。
解決方案:
第一種,升級機器以包含更多內存
也是最簡單的方法, 增加虛擬內存(swap space) 的大小. 各操作系統的設置方法不太一樣, 比如Linux,可以使用下面的命令設置:
swapoff -a
dd if=/dev/zero of=swapfile bs=1024 count=655360
mkswap swapfile
swapon swapfile
第二種,優化應用程序以減少其內存佔用
1.8 java.lang.OutOfMemoryError: Requested array size exceeds VM limit
原因:數組太大, 最終長度超過平臺限制值, 但小於 Integer.MAX_INT。
代碼示例:
for (int i = 3; i >= 0; i--) {
try {
int[] arr = new int[Integer.MAX_VALUE-i];
System.out.format("Successfully initialized an array with %,d elements.\n", Integer.MAX_VALUE-i);
} catch (Throwable t) {
t.printStackTrace();
}
}
//Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
此示例也展示了這個錯誤比較罕見的原因 —— 要取得JVM對數組大小的限制, 要分配長度差不多等於 Integer.MAX_INT 的數組. 這個示例運行在64位的Mac OS X, Hotspot 7平臺時, 只有兩個長度會拋出這個錯誤: Integer.MAX_INT-1 和 Integer.MAX_INT。
解決:
- 需要檢查業務代碼, 確認是否真的需要那麼大的數組。如果可以減小數組長度,如果不行,可能需要把數據拆分爲多個塊, 然後根據需要按批次加載。
- 修改程序邏輯。例如拆分成多個小塊,按批次加載; 或者放棄使用標準庫,而是自己處理數據結構,比如使用 sun.misc.Unsafe 類, 通過Unsafe工具類可以像C語言一樣直接分配內存。
1.9 Out of memory:Kill process or sacrifice child
原因:爲了理解這個錯誤,我們需要補充一點操作系統的基礎知識。操作系統是建立在進程的概念之上,這些進程在內核中作業,其中有一個非常特殊的進程,名叫“內存殺手(Out of memory killer)”。當內核檢測到系統內存不足時,OOM killer被激活,然後選擇一個進程殺掉。哪一個進程這麼倒黴呢?選擇的算法和想法都很樸實:誰佔用內存最多,誰就被幹掉。
解決方案
解決這個問題最有效也是最直接的方法就是升級內存,其他方法諸如:調整OOM Killer配置、水平擴展應用,將內存的負載分攤到若干小實例上