一文盤點Java中常見內(nèi)存泄漏場景與解決方法
今天我們來一起聊一聊有哪些情況會導(dǎo)致內(nèi)存泄漏。
什么是 內(nèi)存泄漏 呢
內(nèi)存泄漏 是指對象 已經(jīng)不再被程序使用,但因為某些原因 無法被垃圾回收器回收,長期占用內(nèi)存,最終可能引發(fā) OOM(OutOfMemoryError)。
接下來我們看一下常見的幾類內(nèi)存泄漏場景。
1、生命周期長的集合
將對象放入 靜態(tài) 或 生命周期很長 的集合(如 public static List list = new ArrayList<>();),即使后面不再需要,集合仍持有其引用,導(dǎo)致無法GC。
2、未關(guān)閉的資源
連接、流等資源未調(diào)用 close() 方法關(guān)閉。這些資源不僅占用內(nèi)存,還可能占用文件句柄(操作系統(tǒng)分配的唯一標識,憑它,你才能操作文件資源)、網(wǎng)絡(luò)連接等系統(tǒng)資源。比如 數(shù)據(jù)庫連接、文件流(FileInputStream)、Socket連接 等。
public class FileTest {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
// 讀取文件,未調(diào)用 fis.close()
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
// 未調(diào)用 fis.close() → fis 持有 Native 引用,無法回收
}
}
}
3、ThreadLocal 使用不當
將對象存入 ThreadLocal 后,未在后續(xù)調(diào)用 remove() 清理。若線程來自線程池(會復(fù)用),其 ThreadLocalMap 中的值會一直存活。
public class ThreadLocalTest {
private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 線程池(核心線程長期存活)
TThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
4,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadFactoryBuilder().setNameFormat("my-thread-pool-%d").setDaemon(false).setPriority(Thread.NORM_PRIORITY).build(),
new ThreadPoolExecutor.AbortPolicy()
);
executor.submit(() -> {
User user = new User("李四", 30);
userThreadLocal.set(user); // 存儲到 ThreadLocal
// 業(yè)務(wù)執(zhí)行完畢,未調(diào)用 remove()
// 核心線程不會銷毀,ThreadLocal 仍持有 user 引用
});
}
}
ps:未進行 remove() ,還可能會導(dǎo)致 ThreadLocal 取值串門。
4、內(nèi)部類與外部類引用
非靜態(tài)內(nèi)部類(或匿名類)會 隱式持有 外部類的引用。如果內(nèi)部類實例生命周期更長(如被緩存或另一個線程引用),會阻止外部類被回收。
public class OuterClass {
private byte[] bigData = new byte[1024 * 1024 * 10]; // 10MB 大對象
// 非靜態(tài)內(nèi)部類
class InnerClass {
// 內(nèi)部類隱式持有 OuterClass 引用
}
public InnerClass createInner() {
return new InnerClass();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
InnerClass inner = outer.createInner();
// 置空外部類引用,但 inner 仍持有 outer 引用
outer = null;
// 若 inner 被靜態(tài)變量/線程長期持有 → outer 對象(含 bigData)無法回收
}
}
5、 監(jiān)聽器與回調(diào)
注冊了 監(jiān)聽器 或 回調(diào) 后,在對象不再需要時 沒有注銷,導(dǎo)致源對象仍持有監(jiān)聽器的引用(比如 事件監(jiān)聽器、消息隊列的消費者等)。
排查工具推薦
- MAT(Memory Analyzer Tool): 分析堆 Dump 文件,定位泄漏對象、引用鏈(誰在持有泄漏對象);
- VisualVM: JDK 自帶工具,監(jiān)控內(nèi)存占用趨勢,生成堆 Dump,簡單排查泄漏。
6、靜態(tài)集合類
? 如 ArrayList、HashMap 等等集合類在類中創(chuàng)建為靜態(tài)變量時,那么他們的生命周期與程序是一致的,由于一些集合沒有刪除元素的特性或者并沒有對其包含的對象進行處理,導(dǎo)致容器中對象的生命周期與容器一致,不能被釋放回收,然而對象本已經(jīng)不再使用,就造成了內(nèi)存泄漏。這樣看來,基本特征為長生命周期對象持有短生命周期對象的引用,盡管短生命周期對象已不再使用,但是因為長生命周期對象的引用使其不能被GC回收。
7、變量的作用域不合理
? 如本該唯一定義在某方法的變量定義在了全局變量。一般來講,一個變量的作用范圍大于其所被使用的范圍,可能發(fā)生內(nèi)存泄漏,表現(xiàn)在存在時間大于使用時間,即使用完了但是還不能被回收,就比如下面的列子。另外,如果沒有及時的將未使用的對象置 null,也有可能導(dǎo)致內(nèi)存泄漏。
package com.liqia.common.core;
/**
* 內(nèi)存泄漏例子
*
* @author chenq
*/
public class DemoMemoryLeak {
/**
* 消息
*/
private String info;
public void receiveAndSaveInfo() {
// 模擬接受消息
receiveInfo();
// 模擬存儲消息
saveInfo();
}
}
? 這里的變量 info 在方法 receiveAndSaveInfo 中進行賦值和保存,在該方法執(zhí)行完畢后本應(yīng)該被 GC 回收,但由于全局變量的生命周期是跟隨對象的,所有當方法執(zhí)行完不能被回收,可能造成內(nèi)存泄漏。
8、內(nèi)部類持有外部類
? 如果一個外部類的實例對象的方法返回了一個內(nèi)部類的實例對象,這個內(nèi)部類對象被長期引用了,即使那個外部類實例對象不再被使用,但由于內(nèi)部類持有外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會造成內(nèi)存泄露。
9、改變哈希值
? 當一個對象被存儲進 HashSet 集合中以后,就不能修改這個對象中的那些參與計算哈希值的字段了,否則,對象修改后的哈希值與最初存儲進 HashSet 集合中時的哈希值就不同了,在這種情況下,即使在 contains 方法使用該對象的當前引用作為的參數(shù)去 HashSet 集合中檢索對象,也將返回找不到對象的結(jié)果,這也會導(dǎo)致無法從 HashSet 集合中單獨刪除當前對象,造成內(nèi)存泄露。
10、棧引起的內(nèi)存泄漏
? 這段模擬棧操作的代碼存在隱蔽的內(nèi)存泄漏問題。定位到pop()函數(shù),在return語句中,當我們彈出一個元素時,只是簡單的讓棧頂指針(size)-1。邏輯上,棧中的這個元素已經(jīng)彈出,已經(jīng)沒有用了。但是事實上,被彈出的元素依然存在于elements數(shù)組中,它依然被elements數(shù)組所引用,GC是無法回收被引用著的對象的。也許你期望等這整個棧失去引用(將被GC回收時),棧內(nèi)的elements數(shù)組一起被GC回收。但是實際的使用過程中,又有誰能夠預(yù)料到這個棧會存活多長時間。為了保險起見,我們需要在彈出一個元素的時候,就讓這個元素失去引用,便于GC回收。我們只需要讓Pop()函數(shù)彈出時,同時解除對彈出元素的引用即可。
package com.liqia.common.core;
import java.util.Arrays;
import java.util.EmptyStackException;
/**
* 內(nèi)存泄漏例子
*
* @author chenq
*/
public class DemoMemoryLeak {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;
public DemoMemoryLeak() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object o) {
ensureCapacity();
elements[size++] = o;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
// Object o = elements[--size];
// elements[size] = null;
// return o;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, size * 2 + 1);
}
}
}
11、緩存泄漏
? 內(nèi)存泄漏的另一個常見來源是緩存,一旦你把對象引用放入到緩存中,他就很容易遺忘,對于這個問題,可以使用WeakHashMap代表緩存,此種Map的特點是,當除了自身有對key的引用外,此key沒有其他引用那么此map會自動丟棄此值。
這里我只列了常見的幾種情況,歡迎大家補充其他內(nèi)存泄漏場景。
到此這篇關(guān)于一文盤點Java中常見內(nèi)存泄漏場景與解決方法的文章就介紹到這了,更多相關(guān)Java內(nèi)存泄漏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Spring的Maven項目實現(xiàn)發(fā)送郵件功能的示例
這篇文章主要介紹了基于Spring的Maven項目實現(xiàn)發(fā)送郵件功能,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友們下面隨著小編來一起學(xué)習學(xué)習吧2020-03-03
java通過MySQL驅(qū)動攔截器實現(xiàn)執(zhí)行sql耗時計算
本文主要介紹了java通過MySQL驅(qū)動攔截器實現(xiàn)執(zhí)行sql耗時計算,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友們下面隨著小編來一起學(xué)習學(xué)習吧2023-03-03
Mybatis返回單個實體或者返回List的實現(xiàn)
這篇文章主要介紹了Mybatis返回單個實體或者返回List的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友們下面隨著小編來一起學(xué)習學(xué)習吧2020-07-07
基于SpringBoot2.0默認使用Redis連接池的配置操作
這篇文章主要介紹了基于SpringBoot2.0默認使用Redis連接池的配置操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12

