JVM調優專題-內存溢出及解決方案

什麼是內存溢出

JVM運行過程中,程序不斷的申請內存空間用於保存運行時數據,當程序申請的內存空間系統無法滿足時,就會拋出內存溢出錯誤。內存溢出發生的區域以及相應的解決方案都不相同,下面我們逐一分析內存溢出類型及解決方案。

OutOfMemoryError與StackOverflowError

JVM內存溢出分爲兩種情況,OutOfMemoryError和StackOverflowError。

  • OutOfMemoryError是在程序無法申請到足夠的內存的時候拋出的異常。
  • StackOverflowError是線程申請的棧深度大於虛擬機所允許的深度所拋出的異常。

ERROR和Exception是有區別

Exception 和 Error 體現了 Java 平臺設計者對不同異常情況的分類。

  • Exception 是程序正常運行中,可以預料的意外情況,可能並且應該被捕獲,進行相應處理。
  • Error 是指在正常情況下,不大可能出現的情況,絕大部分的 Error 都會導致程序(比如 JVM 自身)處於非正常的、不可恢復狀態。

OutOfMemoryError

OutOfMemoryError是在程序無法申請到足夠的內存的時候拋出的異常,導致OutOfMemoryError異常的常見原因有以下幾種:

  • 內存中加載的數據量過於龐大,如一次從數據庫取出過多數據;
  • 集合類中有對對象的引用,使用完後未清空,使得JVM不能回收;
  • 代碼中存在死循環或循環產生過多重複的對象實體;
  • 使用的第三方軟件中的BUG;
  • 啓動參數內存值設定的過小;

在不同的Web服務器或程序中,此錯誤常見的錯誤提示如下:

  • tomcat: java.lang.OutOfMemoryError: PermGen space
  • tomcat: java.lang.OutOfMemoryError: Java heap space
  • weblogic: Root cause of ServletException java.lang.OutOfMemoryError
  • resin: java.lang.OutOfMemoryError
  • java: java.lang.OutOfMemoryError

OOM錯誤發生的場景很多,比如下面這段代碼,最終會發生OutOfMemoryError,爲了能更快的出現錯誤,我們可以設置一下jvm中堆的最大值,設置jvm值的方法是通過-Xms(堆的最小值),-Xmx(堆的最大值)

    public static void main(String[] args) {
        List<UserBean> users = new ArrayList<UserBean>();
        while (true) {
            users.add(new UserBean());
        }
    }

StackOverflowError

StackOverflowError代表的是,當程序中棧深度所需空間大小,超過了虛擬機分配給線程的棧大小時就會出現此error。StackOverflowError發生於單個線程的棧大小無法滿足程序所需的棧空間大小時。

java棧是java虛擬機的一個重要的組成部分,在棧裏進行線程操作,存放方法參數等等。每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應着一次次的 Java 方法調用。棧在初始化過後是有一定的大小的,也可通過jvm參數-Xss設置每個線程的堆棧大小。棧幀中存儲着局部變量表、操作數(operand)棧、動態鏈接、方法正常退出或者異常退出的定義等。

棧深度可理解爲單個線程的堆棧空間最多能產生多少個棧幀,當堆棧總大小不變時,棧幀存儲的信息越多,棧幀越大,每個線程堆棧深度越小。

以下代碼將會報StackOverflowError:

    public static void test(String str){
        System.out.println(str);
        test(str);
    }

內存溢出發生的區域

通常可以把 JVM 內存區域分爲下面幾個方面,其中,有的區域是以線程爲單位,而有的區域則是整個 JVM 進程爲單位的。

  • Method Area(方法區)
  • Java stack(java 虛擬機棧)
  • Native MethodStack(本地方法棧)
  • Heap(堆)
  • Program Counter Regster(程序計數器)

從下圖中看出方法區和堆用黃色標記,和其他三個區域的不同點就是,方法區和堆是線程共享的,所有的運行在jvm上的程序都能訪問這兩個區域,堆,方法區和虛擬機的生命週期一樣,隨着虛擬機的啓動而存在,而棧和程序計數器是依賴用戶線程的啓動和結束而建立和銷燬。

Program Counter Regster(程序計數器):每一個用戶線程對應一個程序計數器,用來指示當前線程所執行字節碼的行號。由程序計數器給文字碼解釋器提供下一條要執行的字節碼的的位置。根據jvm規範,這個區域不會發生內存溢出。

Java stackjava 虛擬機棧):這個區域是最容易出現內存異常的區域,每一個線程對應生成一個線程棧,線程每執行一個方法的時候,都會創建一個棧幀,用來存放方法的局部變量表,操作樹棧,動態連接,方法入口。jvm規範對這個區域定義了兩種內存異常。

  • 如果虛擬機在擴展棧時無法申請到足夠的內存空間則拋出OutOfMemoryError
  • 如果線程請求的棧深度大於虛擬機所允許的最大深度,將會拋出StackOverflowError

Native MethodStack(本地方法棧):和虛擬機棧一樣,不同的是處理的對象不一樣,虛擬機棧處理java的字節碼,而本地棧則是處理的Native方法。其他方面一致。

Heap()前面說了堆是所有線程都能訪問的,隨着虛擬機的啓動而存在,這塊區域很大,因爲所有的線程都在這個區域保存實例化的對象,因爲每一個類型中,每個接口實現類需要的內存不一樣,一個方法內的多個分支需要的內存也不盡相同,我們只有在運行的時候才能知道要創建多少對象,需要分配多大的地址空間。GC關注的正是這樣的部分內容,所以很多時候也將堆稱爲GC堆。堆中肯定不會拋出StackOverflowError類型的異常,所以只有OutOfMemoryError相關類型的異常。

Method Area(方法區):用於存放已被虛擬機加載的類信息,常量,靜態方法,即使編譯後的代碼。由於早期的 Hotspot JVM 實現,很多人習慣於將方法區稱爲永久代(Permanent Generation)。這個區域只能拋出OutOfMemoryError類型的錯誤,OutOfMemoryError: PermGen space。

OutOfMemoryError的類型及解決方案

在發生OOM後需要重點排查以下幾點:

  • 檢查代碼中是否有死循環或遞歸調用。
  • 檢查是否有大循環重複產生新對象實體。
  • 檢查對數據庫查詢中,是否有一次獲得全部數據的查詢。一般來說,如果一次取十萬條記錄到內存,就可能引起內存溢出。這個問題比較隱蔽,在上線前,數據庫中數據較少,不容易出問題,上線後,數據庫中數據多了,一次查詢就有可能引起內存溢出。因此對於數據庫查詢儘量採用分頁的方式查詢。
  • 檢查List、MAP等集合對象是否有使用完後,未清除的問題。List、MAP等集合對象會始終存有對對象的引用,使得這些對象不能被GC回收。

OutOfMemoryError: PermGen space

PermGen space的全稱是Permanent Generation space,是指內存的永久保存區域。這塊內存主要是被JVM存放Class和Meta信息的,Class在被Loader時就會被放到PermGen space中,它和存放類實例(Instance)的Heap區域不同,GC(Garbage Collection)不會在主程序運行期對PermGen space進行清理。

對於老版本的 Oracle JDK,因爲永久代的大小是有限的,並且 JVM 對永久代垃圾回收(如,常量池回收、卸載不再需要的類型)非常不積極,所以當我們不斷添加新類型的時候,永久代出現 OutOfMemoryError 也非常多見,尤其是在運行時存在大量動態類型生成的場合;類似 Intern 字符串緩存佔用太多空間,也會導致 OOM 問題。對應的異常信息,會標記出來和永久代相關:“java.lang.OutOfMemoryError: PermGen space”。

解決方法: 手動設置MaxPermSize大小修改TOMCAT_HOME/bin/catalina.sh

JAVA_OPTS="-server -XX:PermSize=64M -XX:MaxPermSize=128m"

OutOfMemoryError:Java heap space

發生在堆內存上的內存溢出。原因可能有很多種,例如,可能存在內存泄漏問題;也很有可能就是堆的大小不合理,比如我們要處理比較可觀的數據量,但是沒有顯式指定 JVM 堆大小或者指定數值偏小;或者出現 JVM 處理引用不及時,導致堆積起來,內存無法釋放等。

解決方案:增加jvm的內存大小。其中"-Xms128M"爲初始內存,"-Xmx256M"爲最大內存。

-Xmx2048m -Xms2048m 

最後重要提示:

但是,對於內存泄漏問題,無法通過設置啓動參數的方式來解決,這種情況下增加堆內存大小隻會延緩OOM的出現時間,治標不治本。也不推薦一開始就將堆內存大小設置的很大,這樣會掩蓋測試期間可能出現的問題,導致線上問題的出現。

對於這種情況,我們應該對程序中可能出現內存泄漏的地方進行優化。主要包括避免死循環,應該及時釋放種資源:內存, 數據庫的各種連接,防止一次載入太多的數據。導致java.lang.OutOfMemoryError的根本原因是程序不健壯。因此,從根本上解決Java內存溢出的唯一方法就是修改程序,及時地釋放沒用的對象,釋放內存空間。遇到該錯誤的時候要仔細檢查程序。

 

 

 

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