一文盤(pán)點(diǎn)Java中常見(jiàn)內(nèi)存泄漏場(chǎng)景與解決方法
今天我們來(lái)一起聊一聊有哪些情況會(huì)導(dǎo)致內(nèi)存泄漏。
什么是 內(nèi)存泄漏 呢
內(nèi)存泄漏 是指對(duì)象 已經(jīng)不再被程序使用,但因?yàn)槟承┰?無(wú)法被垃圾回收器回收,長(zhǎng)期占用內(nèi)存,最終可能引發(fā) OOM(OutOfMemoryError)。
接下來(lái)我們看一下常見(jiàn)的幾類(lèi)內(nèi)存泄漏場(chǎng)景。
1、生命周期長(zhǎng)的集合
將對(duì)象放入 靜態(tài) 或 生命周期很長(zhǎng) 的集合(如 public static List list = new ArrayList<>();),即使后面不再需要,集合仍持有其引用,導(dǎo)致無(wú)法GC。
2、未關(guān)閉的資源
連接、流等資源未調(diào)用 close() 方法關(guān)閉。這些資源不僅占用內(nèi)存,還可能占用文件句柄(操作系統(tǒng)分配的唯一標(biāo)識(shí),憑它,你才能操作文件資源)、網(wǎng)絡(luò)連接等系統(tǒng)資源。比如 數(shù)據(jù)庫(kù)連接、文件流(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 引用,無(wú)法回收
}
}
}
3、ThreadLocal 使用不當(dāng)
將對(duì)象存入 ThreadLocal 后,未在后續(xù)調(diào)用 remove() 清理。若線(xiàn)程來(lái)自線(xiàn)程池(會(huì)復(fù)用),其 ThreadLocalMap 中的值會(huì)一直存活。
public class ThreadLocalTest {
private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 線(xiàn)程池(核心線(xiàn)程長(zhǎng)期存活)
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); // 存儲(chǔ)到 ThreadLocal
// 業(yè)務(wù)執(zhí)行完畢,未調(diào)用 remove()
// 核心線(xiàn)程不會(huì)銷(xiāo)毀,ThreadLocal 仍持有 user 引用
});
}
}
ps:未進(jìn)行 remove() ,還可能會(huì)導(dǎo)致 ThreadLocal 取值串門(mén)。
4、內(nèi)部類(lèi)與外部類(lèi)引用
非靜態(tài)內(nèi)部類(lèi)(或匿名類(lèi))會(huì) 隱式持有 外部類(lèi)的引用。如果內(nèi)部類(lèi)實(shí)例生命周期更長(zhǎng)(如被緩存或另一個(gè)線(xiàn)程引用),會(huì)阻止外部類(lèi)被回收。
public class OuterClass {
private byte[] bigData = new byte[1024 * 1024 * 10]; // 10MB 大對(duì)象
// 非靜態(tài)內(nèi)部類(lèi)
class InnerClass {
// 內(nèi)部類(lèi)隱式持有 OuterClass 引用
}
public InnerClass createInner() {
return new InnerClass();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
InnerClass inner = outer.createInner();
// 置空外部類(lèi)引用,但 inner 仍持有 outer 引用
outer = null;
// 若 inner 被靜態(tài)變量/線(xiàn)程長(zhǎng)期持有 → outer 對(duì)象(含 bigData)無(wú)法回收
}
}
5、 監(jiān)聽(tīng)器與回調(diào)
注冊(cè)了 監(jiān)聽(tīng)器 或 回調(diào) 后,在對(duì)象不再需要時(shí) 沒(méi)有注銷(xiāo),導(dǎo)致源對(duì)象仍持有監(jiān)聽(tīng)器的引用(比如 事件監(jiān)聽(tīng)器、消息隊(duì)列的消費(fèi)者等)。
排查工具推薦
- MAT(Memory Analyzer Tool): 分析堆 Dump 文件,定位泄漏對(duì)象、引用鏈(誰(shuí)在持有泄漏對(duì)象);
- VisualVM: JDK 自帶工具,監(jiān)控內(nèi)存占用趨勢(shì),生成堆 Dump,簡(jiǎn)單排查泄漏。
6、靜態(tài)集合類(lèi)
? 如 ArrayList、HashMap 等等集合類(lèi)在類(lèi)中創(chuàng)建為靜態(tài)變量時(shí),那么他們的生命周期與程序是一致的,由于一些集合沒(méi)有刪除元素的特性或者并沒(méi)有對(duì)其包含的對(duì)象進(jìn)行處理,導(dǎo)致容器中對(duì)象的生命周期與容器一致,不能被釋放回收,然而對(duì)象本已經(jīng)不再使用,就造成了內(nèi)存泄漏。這樣看來(lái),基本特征為長(zhǎng)生命周期對(duì)象持有短生命周期對(duì)象的引用,盡管短生命周期對(duì)象已不再使用,但是因?yàn)殚L(zhǎng)生命周期對(duì)象的引用使其不能被GC回收。
7、變量的作用域不合理
? 如本該唯一定義在某方法的變量定義在了全局變量。一般來(lái)講,一個(gè)變量的作用范圍大于其所被使用的范圍,可能發(fā)生內(nèi)存泄漏,表現(xiàn)在存在時(shí)間大于使用時(shí)間,即使用完了但是還不能被回收,就比如下面的列子。另外,如果沒(méi)有及時(shí)的將未使用的對(duì)象置 null,也有可能導(dǎo)致內(nèi)存泄漏。
package com.liqia.common.core;
/**
* 內(nèi)存泄漏例子
*
* @author chenq
*/
public class DemoMemoryLeak {
/**
* 消息
*/
private String info;
public void receiveAndSaveInfo() {
// 模擬接受消息
receiveInfo();
// 模擬存儲(chǔ)消息
saveInfo();
}
}
? 這里的變量 info 在方法 receiveAndSaveInfo 中進(jìn)行賦值和保存,在該方法執(zhí)行完畢后本應(yīng)該被 GC 回收,但由于全局變量的生命周期是跟隨對(duì)象的,所有當(dāng)方法執(zhí)行完不能被回收,可能造成內(nèi)存泄漏。
8、內(nèi)部類(lèi)持有外部類(lèi)
? 如果一個(gè)外部類(lèi)的實(shí)例對(duì)象的方法返回了一個(gè)內(nèi)部類(lèi)的實(shí)例對(duì)象,這個(gè)內(nèi)部類(lèi)對(duì)象被長(zhǎng)期引用了,即使那個(gè)外部類(lèi)實(shí)例對(duì)象不再被使用,但由于內(nèi)部類(lèi)持有外部類(lèi)的實(shí)例對(duì)象,這個(gè)外部類(lèi)對(duì)象將不會(huì)被垃圾回收,這也會(huì)造成內(nèi)存泄露。
9、改變哈希值
? 當(dāng)一個(gè)對(duì)象被存儲(chǔ)進(jìn) HashSet 集合中以后,就不能修改這個(gè)對(duì)象中的那些參與計(jì)算哈希值的字段了,否則,對(duì)象修改后的哈希值與最初存儲(chǔ)進(jìn) HashSet 集合中時(shí)的哈希值就不同了,在這種情況下,即使在 contains 方法使用該對(duì)象的當(dāng)前引用作為的參數(shù)去 HashSet 集合中檢索對(duì)象,也將返回找不到對(duì)象的結(jié)果,這也會(huì)導(dǎo)致無(wú)法從 HashSet 集合中單獨(dú)刪除當(dāng)前對(duì)象,造成內(nèi)存泄露。
10、棧引起的內(nèi)存泄漏
? 這段模擬棧操作的代碼存在隱蔽的內(nèi)存泄漏問(wèn)題。定位到pop()函數(shù),在return語(yǔ)句中,當(dāng)我們彈出一個(gè)元素時(shí),只是簡(jiǎn)單的讓棧頂指針(size)-1。邏輯上,棧中的這個(gè)元素已經(jīng)彈出,已經(jīng)沒(méi)有用了。但是事實(shí)上,被彈出的元素依然存在于elements數(shù)組中,它依然被elements數(shù)組所引用,GC是無(wú)法回收被引用著的對(duì)象的。也許你期望等這整個(gè)棧失去引用(將被GC回收時(shí)),棧內(nèi)的elements數(shù)組一起被GC回收。但是實(shí)際的使用過(guò)程中,又有誰(shuí)能夠預(yù)料到這個(gè)棧會(huì)存活多長(zhǎng)時(shí)間。為了保險(xiǎn)起見(jiàn),我們需要在彈出一個(gè)元素的時(shí)候,就讓這個(gè)元素失去引用,便于GC回收。我們只需要讓Pop()函數(shù)彈出時(shí),同時(shí)解除對(duì)彈出元素的引用即可。
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)存泄漏的另一個(gè)常見(jiàn)來(lái)源是緩存,一旦你把對(duì)象引用放入到緩存中,他就很容易遺忘,對(duì)于這個(gè)問(wèn)題,可以使用WeakHashMap代表緩存,此種Map的特點(diǎn)是,當(dāng)除了自身有對(duì)key的引用外,此key沒(méi)有其他引用那么此map會(huì)自動(dòng)丟棄此值。
這里我只列了常見(jiàn)的幾種情況,歡迎大家補(bǔ)充其他內(nèi)存泄漏場(chǎng)景。
到此這篇關(guān)于一文盤(pán)點(diǎn)Java中常見(jiàn)內(nèi)存泄漏場(chǎng)景與解決方法的文章就介紹到這了,更多相關(guān)Java內(nèi)存泄漏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot集成Sorl搜索客戶(hù)端的實(shí)現(xiàn)代碼
本篇文章主要介紹了Spring Boot集成Sorl搜索客戶(hù)端的實(shí)現(xiàn)代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11
Java中進(jìn)行異步調(diào)用失敗的解決方法詳解
異步編程是一種非阻塞的編程模式,允許程序在等待某個(gè)操作完成時(shí)繼續(xù)執(zhí)行其他任務(wù),而不是一直等待,下面我們就來(lái)看看異步調(diào)用失敗的相關(guān)解決方法吧2026-01-01
SpringBoot通過(guò)RedisTemplate執(zhí)行Lua腳本的方法步驟
這篇文章主要介紹了SpringBoot通過(guò)RedisTemplate執(zhí)行Lua腳本的方法步驟,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02
生成8位隨機(jī)不重復(fù)的數(shù)字編號(hào)的方法
生成隨機(jī)不重復(fù)的數(shù)字編號(hào)在某些情況下也會(huì)用到,本文以生成8位隨機(jī)不重復(fù)的數(shù)字編號(hào)為例與大家分享下具體的實(shí)現(xiàn)過(guò)程,感興趣的朋友可以參考下2013-09-09
SpringBoot上傳臨時(shí)文件被刪除引起報(bào)錯(cuò)的解決
這篇文章主要介紹了SpringBoot上傳臨時(shí)文件被刪除引起報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
Java+opencv3.2.0實(shí)現(xiàn)重映射
這篇文章主要為大家詳細(xì)介紹了Java+opencv3.2.0實(shí)現(xiàn)重映射的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02
java中判斷map<String,List>是否有key的四種方法
這篇文章主要為大家詳細(xì)介紹了java中判斷map<String,List>是否有key的四種方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-12-12

