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

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

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

前言

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

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

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

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

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

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

內(nèi)存溢出

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

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

比如說(shuō)運(yùn)行下面這段代碼:

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

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

運(yùn)行結(jié)果如下:

內(nèi)存泄露

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

簡(jiǎn)單來(lái)說(shuō),就是應(yīng)該被垃圾回收的對(duì)象沒(méi)有回收掉,導(dǎo)致占用的內(nèi)存越來(lái)越多,最終導(dǎo)致內(nèi)存溢出。

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

來(lái)看下面的例子:

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)閉聲明的對(duì)象 ResultSet
        // 3.關(guān)閉連接 Connection
    }
  }
}

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

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

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

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

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

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

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

不得不說(shuō),重啟大法真的好,能解決大量的問(wèn)題,但不是長(zhǎng)久之計(jì)。

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

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

在這里插入圖片描述

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

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

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

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

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

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

所以導(dǎo)致想使用 MAT 分析需要花費(fèi)大量時(shí)間。

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

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

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

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

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

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

果然 review 代碼是保障程序性能的第一道防線,誠(chéng)不欺我。大家在寫(xiě)完代碼的時(shí)候,盡量也要團(tuán)隊(duì) review 一次。

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

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

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

同時(shí)后臺(tái)也開(kāi)始打印內(nèi)存溢出了,這樣便復(fù)現(xiàn)出了問(wèn)題。

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

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

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

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

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

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

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

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

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

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

我也做了一個(gè)實(shí)驗(yàn),證明確實(shí)如此。

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

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

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

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

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

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

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

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

小結(jié)

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

也會(huì)讓大部分覺(jué)得 JVM 這樣的黑盒難以下手的球友有一個(gè)直觀感受。

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

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

相關(guān)文章

最新評(píng)論