一篇文章帶你搞定JAVA內存泄漏

1、什麼是內存泄漏

內存泄漏是指無用對象(不再使用的對象)持續占有內存或無用對象的內存得不到及時釋放,從而造成內存空間的浪費稱為內存泄漏。隨著垃圾回收器活動的增加以及內存占用的不斷增加,程序性能會逐漸表現出來下降,極端情況下,會引發OutOfMemoryError導致程序崩潰。

2、內存泄漏的原因

JVM 虛擬機是使用引用計數法和可達性分析來判斷對象是否可回收,本質是判斷一個對象是否還被引用,如果沒有引用則回收。在開發的過程中,由於代碼的實現不同就會出現很多種內存泄漏問題,讓gc 系統誤以為此對象還在引用中,無法回收,造成內存泄漏。

3、內存泄漏有哪些情況

3.1 代碼中沒有及時釋放,導致內存無法回收。

下面的代碼,因為是雙向鏈表,但是斷開的不夠徹底,prev節點依然引用這當前正在使用的節點,導致無法回收

public class ListNode {
    int val;
    ListNode next;
    ListNode prev;
    ListNode() {
    }
    ListNode(int val) {
        this.val = val;
    }
    public ListNode(int val, ListNode next, ListNode prev) {
        this.val = val;
        this.next = next;
        this.prev = prev;
    }

    public static void main(String[] args) {
        ListNode curr = new ListNode(1);
        ListNode prev = new ListNode(2);
        ListNode next = new ListNode(3);
        curr.prev = prev;
        curr.next = next;
        curr.prev = null;
    }
}
    public static void main(String[] args) {
        ListNode curr = new ListNode(1);
        ListNode prev = new ListNode(2);
        ListNode next = new ListNode(3);
        curr.prev = prev;
        curr.next = next;
        curr.prev = null;
    }
}

3.2 資源未關閉造成的內存泄漏

各種連接,如數據庫連接、網絡連接和IO連接等,文件讀寫等,可以使用 try-with-resources 讀取完文件,自動資源釋放

try (RandomAccessFile raf = new RandomAccessFile(filePath, "r");) {
        Image image = null;
while((image = parseImage(raf)) != null){
            imageList.add(image);
        }
        return imageList;
} catch(Exception e){
    log.error("parse file error, path: {},", path, e);
    return null;
}

3.3 全局緩存持有的對象不使用的時候沒有及時移除,導致一直在內存中無法移除

3.4 靜態集合類

如HashMap、LinkedList等等。如果這些容器為靜態的,那麼它們的生命周期與程序一致,則容器中的對象在程序結束之前將不能被釋放,從而造成內存泄漏。生命周期長的對象持有短生命周期對象的引用,盡管短生命周期的對象不再使用,但是因為長生命周期對象持有它的引用而導致不能被回收。

3.5 堆外內存無法回收

堆外內存不受gc的管理,可能因為第三方的bug出現內存泄漏

4、內存泄漏的解決辦法

1.盡量減少使用靜態變量,或者使用完及時 賦值為 null。

2.明確內存對象的有效作用域,盡量縮小對象的作用域,能用局部變量處理的不用成員變量,因為局部變量彈棧會自動回收;

3.減少長生命周期的對象持有短生命周期的引用;

4.使用StringBuilder和StringBuffer進行字符串連接,Sting和StringBuilder以及StringBuffer等都可以代表字符串,其中String字符串代表的是不可變的字符串,後兩者表示可變的字符串。如果使用多個String對象進行字符串連接運算,在運行時可能產生大量臨時字符串,這些字符串會保存在內存中從而導致程序性能下降。

5.對於不需要使用的對象手動設置null值,不管GC何時會開始清理,我們都應及時的將無用的對象標記為可被清理的對象;

6.各種連接(數據庫連接,網絡連接,IO連接)操作,務必顯示調用close關閉。

5、內存問題排查

沒有任何一個程序員想要出現這種問題,但是出現瞭問題也要解決,內存泄漏的主要表象就是內存不足,內存告警之後如何判斷是否有內存泄漏。

第一步 首先確認邏輯問題

查看內存中對象的數量和大小,判斷是否在合理的范圍,如果在合理的范圍內,增大內存配置,調整內存比例就可以瞭。

命令:

jmap -heap pid

圖片

第二步:分析gc是否正常執行

命令:

jstat -gcutil <pid> 1000

圖片

S0 — Heap上的 Survivor space 0 區已使用空間的百分比    
S1 — Heap上的 Survivor space 1 區已使用空間的百分比    
E — Heap上的 Eden space 區已使用空間的百分比    
O   — Heap上的 Old space 區已使用空間的百分比    
P   — Perm space 區已使用空間的百分比
YGC — 從應用程序啟動到采樣時發生 Young GC 的次數
YGCT– 從應用程序啟動到采樣時 Young GC 所用的時間(單位秒)    
FGC — 從應用程序啟動到采樣時發生 Full GC 的次數
FGCT– 從應用程序啟動到采樣時 Full GC 所用的時間(單位秒)    
GCT — 從應用程序啟動到采樣時用於垃圾回收的總時間(單位秒)
LGCC - 進行GC的原因(低版本jdk可能沒有這一列)

從這裡觀察gc是否異常,也可以根據這個進行jvm內存分配調優,來提高性能降低gc對性能的損耗

第三步 確認下版本新增代碼的改動,盡快從代碼上找出問題。

第四步:開啟各種命令行和 導出 dump 各種工具分析

-XX:+HeapDumpOnOutOfMemoryError
-XX:OnError
-XX:+ShowMessageBoxOnError

推薦使用jprofile 進行本地分析,可以不用記住那麼多命令。

圖片

總結:

現在的服務器內存雖然很大,但是且用且珍惜,不要等到出現問題瞭才知道後果,在開發中規范自己代碼,用完的對象及時釋放,減少垃圾對象。出現問題瞭也不要慌,仔細分析代碼,一切都是有原因的。

本篇文章就到這裡瞭,希望能給你帶來幫助,也希望您能多多關註WalkonNet的更多內容!

推薦閱讀: