JVM有關的知識點(四)

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) 問題歸根結底三點原因:

  1. 本身資源不夠
  2. 申請的太多內存
  3. 資源耗盡

解決思路,換成Java服務分析,三個原因也可以解讀爲:

  • 有可能是內存分配確實過小,而正常業務使用了大量內存
  • 某一個對象被頻繁申請,卻沒有釋放,內存不斷泄漏,導致內存耗盡
  • 某一個資源被頻繁申請,系統資源耗盡,例如:不斷創建線程,不斷髮起網絡連接

因此,針對解決思路,快速定位OOM問題的步驟是:

  1. 確認是不是內存本身就分配過小
  2. 找到最耗內存的對象
  3. 確認是否是資源耗盡

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。

解決:

  1. 需要檢查業務代碼, 確認是否真的需要那麼大的數組。如果可以減小數組長度,如果不行,可能需要把數據拆分爲多個塊, 然後根據需要按批次加載。
  2. 修改程序邏輯。例如拆分成多個小塊,按批次加載; 或者放棄使用標準庫,而是自己處理數據結構,比如使用 sun.misc.Unsafe 類, 通過Unsafe工具類可以像C語言一樣直接分配內存。

1.9 Out of memory:Kill process or sacrifice child

原因:爲了理解這個錯誤,我們需要補充一點操作系統的基礎知識。操作系統是建立在進程的概念之上,這些進程在內核中作業,其中有一個非常特殊的進程,名叫“內存殺手(Out of memory killer)”。當內核檢測到系統內存不足時,OOM killer被激活,然後選擇一個進程殺掉。哪一個進程這麼倒黴呢?選擇的算法和想法都很樸實:誰佔用內存最多,誰就被幹掉。

解決方案
解決這個問題最有效也是最直接的方法就是升級內存,其他方法諸如:調整OOM Killer配置、水平擴展應用,將內存的負載分攤到若干小實例上

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章