欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java內(nèi)存溢出(OOM)排查優(yōu)化指南

 更新時間:2025年05月09日 10:32:28   作者:碼到π退休  
OutOfMemoryError,也就是臭名昭著的 OOM(內(nèi)存溢出),相信很多球友都遇到過,相對于常見的業(yè)務(wù)異常,如數(shù)組越界、空指針等,OOM 問題 更難難定位和解決,本文就給大家介紹了Java內(nèi)存溢出(OOM)排查優(yōu)化指南,需要的朋友可以參考下

前言

OutOfMemoryError,也就是臭名昭著的 OOM(內(nèi)存溢出),相信很多球友都遇到過,相對于常見的業(yè)務(wù)異常,如數(shù)組越界、空指針等,OOM 問題 更難難定位和解決。

這篇內(nèi)容就以之前碰到的一次線上內(nèi)存溢出的定位、解決問題的方式展開;希望能對碰到類似問題的球友帶來思路和幫助。

主要從表現(xiàn)-->排查-->定位-->解決 四個步驟來分析和解決問題。

內(nèi)存溢出和內(nèi)存泄露

在 Java 中,和內(nèi)存相關(guān)的問題主要有兩種,內(nèi)存溢出內(nèi)存泄漏。

  • 內(nèi)存溢出Out Of Memory):就是申請內(nèi)存時,JVM 沒有足夠的內(nèi)存空間。通俗說法就是去蹲坑發(fā)現(xiàn)坑位滿了。
  • 內(nèi)存泄露Memory Leak):就是申請了內(nèi)存,但是沒有釋放,導致內(nèi)存空間浪費。通俗說法就是有人占著茅坑不拉屎。

內(nèi)存溢出

在 JVM 的內(nèi)存區(qū)域中,除了程序計數(shù)器,其他的內(nèi)存區(qū)域都有可能發(fā)生內(nèi)存溢出。

大家都知道,Java 堆中存儲的都是對象,或者叫對象實例,那只要我們不斷地創(chuàng)建對象,并且保證 GC Roots 到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那么就一定會產(chǎn)生內(nèi)存溢出。

比如說運行下面這段代碼:

public class OOM {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        while (true) {
            list.add(new Object());
        }
    }
}

運行程序的時候記得設(shè)置一下 VM 參數(shù):-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError,限制堆內(nèi)存大小為 20M,并且不允許擴展,并且當發(fā)生 OOM 時 dump 出當前內(nèi)存的快照。

運行結(jié)果如下:

內(nèi)存泄露

內(nèi)存泄露是指程序中己動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費,導致程序運行速度減慢甚至系統(tǒng)崩潰等嚴重后果。

簡單來說,就是應(yīng)該被垃圾回收的對象沒有回收掉,導致占用的內(nèi)存越來越多,最終導致內(nèi)存溢出。

在上圖中:對象 X 引用對象 Y,X 的生命周期比 Y 的生命周期長,Y 生命周期結(jié)束的時候,垃圾回收器不會回收對象 Y。

來看下面的例子:

public class MemoryLeak {
    public static void main(String[] args) {
      try{
          Connection conn =null;
          Class.forName("com.mysql.jdbc.Driver");
          conn =DriverManager.getConnection("url","","");
          Statement stmt =conn.createStatement();
          ResultSet rs =stmt.executeQuery("....");
      } catch(Exception e){//異常日志
      } finally {
        // 1.關(guān)閉結(jié)果集 Statement
        // 2.關(guān)閉聲明的對象 ResultSet
        // 3.關(guān)閉連接 Connection
    }
  }
}

創(chuàng)建的連接不再使用時,需要調(diào)用 close 方法關(guān)閉連接,只有連接被關(guān)閉后,GC 才會回收對應(yīng)的對象(Connection,Statement,ResultSet,Session)。忘記關(guān)閉這些資源會導致持續(xù)占有內(nèi)存,無法被 GC 回收。

這樣就會導致內(nèi)存泄露,最終導致內(nèi)存溢出。

換句話說,內(nèi)存泄露不是內(nèi)存溢出,但會加快內(nèi)存溢出的發(fā)生。

內(nèi)存溢出后的表象

之前生產(chǎn)環(huán)境爆出的內(nèi)存溢出問題會隨著業(yè)務(wù)量的增長,出現(xiàn)的頻次也越來越高。

應(yīng)用程序的業(yè)務(wù)邏輯非常簡單,就是從 Kafka 中將數(shù)據(jù)消費下來,然后批量的做持久化操作。

OOM 現(xiàn)象則是隨著 Kafka 的消息越多,出現(xiàn)異常的頻次就越快。由于當時還有其他工作所以只能讓運維做重啟,并且監(jiān)控好堆內(nèi)存以及 GC 情況。

不得不說,重啟大法真的好,能解決大量的問題,但不是長久之計。

內(nèi)存泄露的排查

于是我們想根據(jù)運維之前收集到的內(nèi)存數(shù)據(jù)、GC 日志嘗試判斷哪里出現(xiàn)了問題。

在這里插入圖片描述

結(jié)果發(fā)現(xiàn)老年代的內(nèi)存使用就算是發(fā)生 GC 也一直居高不下,而且隨著時間推移也越來越高。

結(jié)合 jstat 的日志發(fā)現(xiàn)就算是發(fā)生了 FGC,老年代也回收不了,內(nèi)存已經(jīng)到頂。

甚至有幾臺應(yīng)用 FGC 達到了上百次,時間也高的可怕。

這說明應(yīng)用的內(nèi)存使用肯定是有問題的,有許多賴皮對象始終回收不掉。

內(nèi)存泄露的定位

由于生產(chǎn)上的內(nèi)存 dump 文件非常大,達到了幾十 G。也和我們生產(chǎn)環(huán)境配置的內(nèi)存太大有關(guān)。

所以導致想使用 MAT 分析需要花費大量時間。

MAT 是 Eclipse 的一個插件,也可以單獨使用,可以用來分析 Java 的堆內(nèi)存,找出內(nèi)存泄露的原因。

因此我們就想是否可以在本地復現(xiàn),這樣就好定位的多。

為了盡快的復現(xiàn)問題,我將本地應(yīng)用最大堆內(nèi)存設(shè)置為 150M。然后在消費 Kafka 那里 Mock 了一個 while 循環(huán)一直不斷的生成數(shù)據(jù)。

同時當應(yīng)用啟動之后利用 VisualVM 連上應(yīng)用實時監(jiān)控內(nèi)存、GC 的使用情況。

結(jié)果跑了 10 幾分鐘內(nèi)存使用并沒有什么問題。根據(jù)圖中可以看出,每一次 GC 內(nèi)存都能有效的回收,所以并沒有復現(xiàn)問題。

沒法復現(xiàn)問題就很難定位。于是我們就采用了一種古老的方法——review 代碼,發(fā)現(xiàn)生產(chǎn)的邏輯和我們用 while 循環(huán) Mock 的數(shù)據(jù)還不太一樣。

果然 review 代碼是保障程序性能的第一道防線,誠不欺我。大家在寫完代碼的時候,盡量也要團隊 review 一次。

后來查看生產(chǎn)日志發(fā)現(xiàn)每次從 Kafka 中取出的都是幾百條數(shù)據(jù),而我們 Mock 時每次只能產(chǎn)生一條

為了盡可能的模擬生產(chǎn)情況便在服務(wù)器上跑了一個生產(chǎn)者程序,一直源源不斷的向 Kafka 中發(fā)送數(shù)據(jù)。

果然不出意外只跑了一分多鐘內(nèi)存就頂不住了,觀察下圖發(fā)現(xiàn) GC 的頻次非常高,但是內(nèi)存的回收卻是相形見拙。

同時后臺也開始打印內(nèi)存溢出了,這樣便復現(xiàn)出了問題。

內(nèi)存泄露的解決

從目前的表現(xiàn)來看,就是內(nèi)存中有許多對象一直存在強引用關(guān)系導致得不到回收。

于是便想看看到底是什么對象占用了這么多的內(nèi)存,利用 VisualVM 的 HeapDump 功能,就可以立即 dump 出當前應(yīng)用的內(nèi)存情況。

結(jié)果發(fā)現(xiàn) com.lmax.disruptor.RingBuffer 類型的對象占用了將近 50% 的內(nèi)存。

看到這個包自然就想到了 Disruptor 環(huán)形隊列了。

Disruptor 是一個高性能的異步處理框架,它的核心思想是:通過無鎖的方式來實現(xiàn)高性能的并發(fā)處理,其性能是高于 JDK 的 BlockingQueue 的。

再次 review 代碼發(fā)現(xiàn):從 Kafka 里取出的 700 條數(shù)據(jù)是直接往 Disruptor 里丟的。

這里也就能說明為什么第一次模擬數(shù)據(jù)沒復現(xiàn)問題了。

模擬的時候是一個對象放進隊列里,而生產(chǎn)的情況是 700 條數(shù)據(jù)放進隊列里。這個數(shù)據(jù)量就是 700 倍的差距啊。

而 Disruptor 作為一個環(huán)形隊列,在對象沒有被覆蓋之前是一直存在的。

我也做了一個實驗,證明確實如此。

我設(shè)置隊列大小為 8 ,從 0~9 往里面寫 10 條數(shù)據(jù),當寫到 8 的時候就會把之前 0 的位置覆蓋掉,后面的以此類推(類似于 HashMap 的取模定位)。

所以在生產(chǎn)環(huán)境上,假設(shè)我們的隊列大小是 1024,那么隨著系統(tǒng)的運行最終會導致 1024 個位置上裝滿了對象,而且每個位置都是 700 個!

于是查看了生產(chǎn)環(huán)境上 Disruptor 的 RingBuffer 配置,結(jié)果是:1024*1024

這個數(shù)量級就非常嚇人了。

為了驗證是否是這個問題,我在本地將該值設(shè)為 2 ,一個最小值試試。

同樣的 128M 內(nèi)存,也是通過 Kafka 一直源源不斷的取出數(shù)據(jù)。通過監(jiān)控如下:

跑了 20 幾分鐘系統(tǒng)一切正常,每當一次 GC 都能回收大部分內(nèi)存,最終呈現(xiàn)鋸齒狀。

這樣問題就找到了,不過生產(chǎn)上這個值具體設(shè)置多少還得根據(jù)業(yè)務(wù)情況測試才能知道,但原有的 1024*1024 是絕對不能再使用了。

小結(jié)

雖然到了最后也就改了一行代碼(還沒改,直接修改配置),但這個排查過程我覺得是很有意義的。

也會讓大部分覺得 JVM 這樣的黑盒難以下手的球友有一個直觀感受。

同時也得感嘆 Disruptor 東西雖好,也不能亂用哦!

以上就是Java內(nèi)存溢出(OOM)排查優(yōu)化指南的詳細內(nèi)容,更多關(guān)于Java內(nèi)存溢出OOM的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 從java面試題了解你所模糊的數(shù)組

    從java面試題了解你所模糊的數(shù)組

    這篇文章主要介紹了從java面試題了解你所模糊的數(shù)組,數(shù)組用來存儲一系列的數(shù)據(jù)項,其中的每一項具有相同的基本數(shù)據(jù)類型、類或相同的父類。通過使用數(shù)組,可以在很大程度上縮短和簡化程序代碼,從而提高應(yīng)用程序的效率。,需要的朋友可以參考下
    2019-06-06
  • springboot應(yīng)用訪問zookeeper的流程

    springboot應(yīng)用訪問zookeeper的流程

    這篇文章主要介紹了springboot應(yīng)用訪問zookeeper的流程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-01-01
  • SpringBoot整合redis實現(xiàn)輸入密碼錯誤限制登錄功能

    SpringBoot整合redis實現(xiàn)輸入密碼錯誤限制登錄功能

    遇到這樣的需求需要實現(xiàn)一個登錄功能,并且2分鐘之內(nèi)只能輸入5次錯誤密碼,若輸入五次之后還沒有輸入正確密碼,系統(tǒng)將會將該賬號鎖定1小時,這篇文章主要介紹了SpringBoot整合redis并實現(xiàn)輸入密碼錯誤限制登錄功能,需要的朋友可以參考下
    2024-02-02
  • Springmvc調(diào)用存儲過程,并返回存儲過程返還的數(shù)據(jù)方式

    Springmvc調(diào)用存儲過程,并返回存儲過程返還的數(shù)據(jù)方式

    這篇文章主要介紹了Springmvc調(diào)用存儲過程,并返回存儲過程返還的數(shù)據(jù)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • IDEA內(nèi)存調(diào)試插件(好用)

    IDEA內(nèi)存調(diào)試插件(好用)

    本文給大家分享IDEA中一個很有用的內(nèi)存調(diào)試插件,非常不錯,具有參考借鑒價值,需要的朋友參考下
    2018-02-02
  • Java正則表達式之分組和替換方式

    Java正則表達式之分組和替換方式

    這篇文章主要介紹了Java正則表達式之分組和替換方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • Java中常見的幾種四舍五入方法總結(jié)

    Java中常見的幾種四舍五入方法總結(jié)

    在Java編程中四舍五入是一個常見的數(shù)學運算需求,下面這篇文章主要給大家介紹了關(guān)于Java中常見的幾種四舍五入方法,文章通過代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2024-08-08
  • 分析java 中AspectJ切面執(zhí)行兩次的原因

    分析java 中AspectJ切面執(zhí)行兩次的原因

    這篇文章主要介紹了分析java 中AspectJ切面執(zhí)行兩次的原因的相關(guān)資料,希望通過本能幫助到大家,需要的朋友可以參考下
    2017-09-09
  • MyBatis-Plus逆向工程——Generator的使用

    MyBatis-Plus逆向工程——Generator的使用

    這篇文章主要介紹了MyBatis-Plus逆向工程——Generator的使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-01-01
  • 使用Swagger直接上傳文件的方法

    使用Swagger直接上傳文件的方法

    這篇文章主要介紹了使用Swagger直接上傳文件的方法,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-12-12

最新評論