在 Java 開發中,內存溢出(OutOfMemoryError,簡稱 OOM)是一個常見且棘手的問題。相比于數組越界、空指針等業務異常,OOM 問題通常更難定位和解決。本文將通過一次線上內存溢出問題的排查過程,分享從問題表現到最終解決的完整思路,希望能為遇到類似問題的開發者提供參考。
1 內存溢出與內存泄露
在 Java 中,與內存相關的問題主要有兩種:內存溢出和內存泄露。
- 內存溢出(Out Of Memory):指應用程序申請內存時,JVM 沒有足夠的內存空間。可以形象地理解為“去蹲坑發現坑位滿了”。
- 內存泄露(Memory Leak):指應用程序申請了內存但沒有釋放,導致內存空間浪費。可以形象地理解為“有人占著茅坑不拉屎”。
1.1 內存溢出
在 JVM 的內存區域中,除了程序計數器,其他內存區域都有可能發生內存溢出。Java 堆是存儲對象實例的區域,只要不斷創建對象,并確保這些對象與 GC Roots 之間存在可達路徑,避免被垃圾回收機制清除,就一定會發生內存溢出。
例如,以下代碼會不斷創建對象,最終導致內存溢出:
public class OOM {public static void main(String[] args) {List<Object> list = new ArrayList<>();while (true) {list.add(new Object());}}
}
運行該程序時,可以通過設置 JVM 參數 -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
來限制堆內存大小為 20M,并在發生 OOM 時生成內存快照。
1.2 內存泄露
內存泄露是指程序中動態分配的堆內存由于某種原因未能釋放,導致系統內存浪費,進而可能引發程序運行速度減慢甚至系統崩潰。簡單來說,內存泄露是由于應該被垃圾回收的對象未能被回收,導致內存占用不斷增加,最終可能導致內存溢出。
例如,以下代碼中,數據庫連接未關閉,導致內存泄露:
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. 關閉結果集 Statement// 2. 關閉聲明的對象 ResultSet// 3. 關閉連接 Connection}}
}
如果連接未關閉,GC 將無法回收相關對象(如 Connection
、Statement
、ResultSet
等),從而導致內存泄露。
換句話說,內存泄露不是內存溢出,但會加快內存溢出的發生。
2 內存溢出的表現
在生產環境中,內存溢出問題通常隨著業務量的增長而頻繁出現。例如,某應用程序從 Kafka 消費數據并進行批量持久化操作,隨著 Kafka 消息量的增加,OOM 問題出現的頻率也越來越高。雖然重啟可以暫時解決問題,但這并非長久之計。
3 內存泄露的排查
為了排查內存泄露問題,首先需要分析運維收集的內存數據和 GC 日志。通過 jstat
工具可以發現,老年代的內存使用率即使在發生 Full GC 后仍然居高不下,且隨著時間的推移逐漸增加。這表明應用程序中存在大量無法回收的對象。
4 內存泄露的定位
由于生產環境的內存快照文件較大(幾十 GB),使用 MAT(Memory Analyzer Tool)進行分析耗時較長。因此,我們嘗試在本地復現問題。通過將本地應用的最大堆內存設置為 150M,并模擬 Kafka 數據消費,使用 VisualVM 監控內存和 GC 情況。
經過多次嘗試,發現只有在模擬生產環境的數據量(每次從 Kafka 取出幾百條數據)時,才能復現內存溢出問題。通過 VisualVM 的 HeapDump 功能,發現 com.lmax.disruptor.RingBuffer
類型的對象占用了近 50% 的內存。
5 內存泄露的解決
通過代碼審查,發現從 Kafka 取出的數據直接放入 Disruptor 環形隊列中,而隊列的大小配置為 1024 * 1024
,導致內存中積累了大量的對象。通過將隊列大小調整為較小的值(如 2),問題得到解決。
Disruptor 是一個高性能的異步處理框架,它的核心思想是:通過無鎖的方式來實現高性能的并發處理,其性能是高于 JDK 的 BlockingQueue 的。
6 總結
雖然最終只是修改了一行代碼(或配置),但整個排查過程非常有意義。通過這次經歷,我們可以更好地理解 JVM 內存管理的機制,并掌握排查內存溢出和內存泄露問題的基本方法。同時,也提醒我們在使用高性能框架(如 Disruptor)時,必須謹慎配置參數,避免因不當使用而導致內存問題。
7 思維導圖
8 參考鏈接
一次內存溢出的排查優化實戰,徹底干掉臭名昭著的 OOM