在Spring Boot中淺嘗內(nèi)存泄漏的實(shí)戰(zhàn)記錄
使用靜態(tài)集合持有對象引用,阻止GC回收
關(guān)鍵點(diǎn):
使用static List作為內(nèi)存泄漏的錨點(diǎn),其生命周期與ClassLoader一致
每次請求向列表添加1MB字節(jié)數(shù)組,這些對象會(huì)持續(xù)占用堆內(nèi)存
由于集合持有強(qiáng)引用,GC無法回收這些對象
最終會(huì)導(dǎo)致OutOfMemoryError: Java heap space
可執(zhí)行代碼:
package io.renren.controller;
import org.springframework.boot.SpringApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
/**
* author: lj
* date: 2025-4
*/
@RestController
public class MemoryLeakController {
// 靜態(tài)集合會(huì)持續(xù)持有對象引用
private static List<byte[]> LEAKING_LIST = new ArrayList<>();
// 內(nèi)存泄漏端點(diǎn)
@GetMapping("/leak")
public String leakMemory() {
// 每次請求添加1MB數(shù)據(jù)(不會(huì)被釋放)
LEAKING_LIST.add(new byte[1024 * 1024]);
return "已泄漏內(nèi)存: " + LEAKING_LIST.size() + " MB";
}
// 觸發(fā)OOM的測試方法(快速驗(yàn)證)
public static void main(String[] args) throws InterruptedException {
SpringApplication.run(MemoryLeakController.class, args);
// 通過循環(huán)請求快速觸發(fā)OOM
while(true) {
new RestTemplate().getForObject("http://localhost:8080/leak", String.class);
Thread.sleep(100);
}
}
}驗(yàn)證:
1,運(yùn)行程序(啟動(dòng)時(shí)添加JVM參數(shù)限制堆大小):
//在cmd中先cd到j(luò)ar包所在目錄,執(zhí)行如下命令啟動(dòng) //-Xmx100m 當(dāng)程序需要更多內(nèi)存時(shí),JVM會(huì)嘗試分配最多100MB的堆內(nèi)存。如果超過這個(gè)限制,可能會(huì)拋出OutOfMemoryError //-Xms100m JVM在啟動(dòng)時(shí)分配的最小內(nèi)存量。如果初始堆內(nèi)存設(shè)置得過低,程序可能在運(yùn)行過程中頻繁擴(kuò)展堆內(nèi)存,影響性能。 //-XX:+HeapDumpOnOutOfMemoryError 在發(fā)生OutOfMemoryError時(shí)生成堆轉(zhuǎn)儲(chǔ)(Heap Dump)的功能 java -jar -Xmx100m -Xms100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\Temp renren-generator-1.0.0.jar
2,訪問 http://localhost:8080/leak 觸發(fā)泄漏

日志輸出顯示了內(nèi)存泄漏位置。

并且在臨時(shí)目錄中保存了一份堆轉(zhuǎn)儲(chǔ)文件,稍后使用MAT(Memory Analyzer Tool)分析。

問題定位
使用jvisualvm工具定位問題
在cmd輸入jvisualvm指令

選中應(yīng)用后,可以監(jiān)控應(yīng)用程序的性能。

觸發(fā)內(nèi)存泄露后,查看每次GC的持續(xù)時(shí)間、回收的內(nèi)存等信息。OOM之后,點(diǎn)擊界面右上角的堆Dump,打開應(yīng)用的堆轉(zhuǎn)儲(chǔ)信息。

查找最大對象

打開java.lang.Object[]的保留堆

查看LEAKING_LIST的引用鏈,至此問題定位完成。

使用MAT(Memory Analyzer Tool)工具定位問題
下載地址:https://eclipse.dev/mat/download/previous/
我的是JDK8,所以我下載了Memory Analyzer 1.10.0 Release版本。下載完成后,直接解壓,運(yùn)行其中的MemoryAnalyzer.exe文件即可啟動(dòng)MAT工具。
用mat工具打開剛剛臨時(shí)目錄中保存的堆轉(zhuǎn)儲(chǔ)文件,點(diǎn)擊Leak Suspects生成內(nèi)存泄漏報(bào)表。

點(diǎn)擊details查看java.lang.Object[]的保留堆

查看LEAKING_LIST的引用鏈,至此問題定位完成。

調(diào)優(yōu)建議
1,避免長時(shí)間持有大對象引用。
2,定期執(zhí)行集合清理操作。
@Scheduled(fixedRate = 60_000)
public void cleanLeakingData() {
LEAKING_LIST.removeIf(data -> /* 清理?xiàng)l件 */);
}--------------------------------------------------更新---------------------------------------------------------
變種實(shí)現(xiàn)方式
@SpringBootApplication
@RestController
@EnableCaching // 關(guān)鍵注解:啟用緩存
public class CacheLeakDemo {
// 模擬緩存未正確清理
@Cacheable("leakyCache")
@GetMapping("/cache-leak")
public byte[] cacheLeak() {
return new byte[1024 * 1024]; // 每次緩存1MB
}
public static void main(String[] args) {
SpringApplication.run(CacheLeakDemo.class, args);
}
}緩存泄漏原理:
@Cacheable會(huì)將每次不同參數(shù)的返回結(jié)果緩存
因?yàn)闆]有設(shè)置過期時(shí)間或大小限制,緩存會(huì)無限增長
示例中每個(gè)請求生成唯一key(默認(rèn)基于方法參數(shù)),導(dǎo)致緩存不斷累積
調(diào)優(yōu)建議
對于緩存使用WeakReference或框架(Caffeine/Ehcache)
// 使用WeakHashMap解決
private static Map<byte[], Boolean> SAFE_MAP =
Collections.synchronizedMap(new WeakHashMap<>());// 使用Caffeine緩存并設(shè)置上限
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES));
return manager;
}因?yàn)樵?Java 中,WeakHashMap 的設(shè)計(jì)目的就是通過弱引用(Weak Reference)自動(dòng)清理不再被使用的鍵值對,從而避免因?qū)ο髿埩魧?dǎo)致的內(nèi)存泄漏。
引用類型對比表:
| 引用類型 | GC行為 | 典型應(yīng)用場景 |
|---|---|---|
| 強(qiáng)引用 | 永不回收(除非顯式置為null) | 普通對象引用 |
| 軟引用 | 內(nèi)存不足時(shí)回收 | 緩存 |
| 弱引用 | 下次GC立即回收 | WeakHashMap/WeakReference |
| 虛引用 | 回收時(shí)收到通知 | 資源清理跟蹤 |
關(guān)鍵機(jī)制:
WeakHashMap 的 鍵(Key)使用弱引用存儲(chǔ)
當(dāng)鍵對象不再被其他強(qiáng)引用持有時(shí),該鍵值對會(huì)被自動(dòng)移除
值對象(Value)仍使用強(qiáng)引用,需要特別注意解耦
內(nèi)存泄漏場景 vs WeakHashMap修復(fù)方案
//使用普通HashMap導(dǎo)致泄漏
public class LeakingCache {
private static Map<byte[], String> CACHE = new HashMap<>();
// 添加大對象到緩存
public static void addToCache(byte[] key, String value) {
CACHE.put(key, value);
}
public static void main(String[] args) {
// 模擬添加后不再使用key
byte[] key = new byte[1024 * 1024]; // 1MB
addToCache(key, "大數(shù)據(jù)");
key = null; // 刪除強(qiáng)引用
// 觸發(fā)GC
System.gc();
// 緩存仍然持有key的強(qiáng)引用,導(dǎo)致1MB內(nèi)存無法回收
System.out.println("緩存大小: " + CACHE.size()); // 輸出1
}
}//使用WeakHashMap
public class SafeCache {
// 使用WeakHashMap + 同步包裝(線程安全)
private static Map<byte[], String> SAFE_CACHE =
Collections.synchronizedMap(new WeakHashMap<>());
public static void addToCache(byte[] key, String value) {
SAFE_CACHE.put(key, value);
}
public static void main(String[] args) {
byte[] key = new byte[1024 * 1024];
addToCache(key, "安全數(shù)據(jù)");
key = null; // 刪除最后一個(gè)強(qiáng)引用
// 強(qiáng)制GC(生產(chǎn)環(huán)境不要主動(dòng)調(diào)用System.gc())
System.gc();
// 給GC一點(diǎn)時(shí)間執(zhí)行
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("緩存大小: " + SAFE_CACHE.size()); // 輸出0
}
}實(shí)戰(zhàn)應(yīng)用
場景:設(shè)備連接會(huì)話管理
@RestController
public class DeviceController {
// 使用WeakHashMap管理臨時(shí)會(huì)話
private static Map<Device, Session> deviceSessions =
Collections.synchronizedMap(new WeakHashMap<>());
@PostMapping("/connect")
public String connect(@RequestBody Device device) {
Session session = new Session(device);
deviceSessions.put(device, session);
return "Connected";
}
// 當(dāng)Device對象不再被外部引用時(shí),自動(dòng)清理會(huì)話
}配置驗(yàn)證端點(diǎn)
@GetMapping("/session-count")
public int getSessionCount() {
return deviceSessions.size();
}測試方法
1,發(fā)送連接請求
curl -X POST http://localhost:8080/connect -d '{"id":"device1"}'
2,立即調(diào)用/session-count查看數(shù)量
3,停止持有Device對象引用后觸發(fā)GC
4,再次檢查會(huì)話數(shù)量增強(qiáng)版緩存實(shí)現(xiàn)(帶自動(dòng)清理)
public class AdvancedCache<K, V> {
private final Map<K, V> cache =
new WeakHashMap<>();
private final ReferenceQueue<K> queue =
new ReferenceQueue<>();
public void put(K key, V value) {
// 清理已回收的條目
processQueue();
cache.put(key, value);
}
private void processQueue() {
Reference<? extends K> ref;
while ((ref = queue.poll()) != null) {
// 這里可以觸發(fā)回調(diào)清理相關(guān)資源
System.out.println("清理?xiàng)l目: " + ref);
}
}
}代碼測試片段
// 測試插入100萬條數(shù)據(jù)
IntStream.range(0, 1_000_000).forEach(i -> {
Object key = new Object();
map.put(key, "Value-" + i);
});
// 強(qiáng)制GC后統(tǒng)計(jì)剩余條目
System.gc();
Thread.sleep(1000);
System.out.println("剩余條目: " + map.size());測試結(jié)果:
| Map類型 | 初始條目 | GC后剩余條目 | 內(nèi)存占用(MB) |
|---|---|---|---|
| HashMap | 1,000,000 | 1,000,000 | 85.3 |
| WeakHashMap | 1,000,000 | 3,214 | 6.7 |
場景:設(shè)備狀態(tài)臨時(shí)緩存
public class DeviceStateManager {
// Key: 設(shè)備對象,Value: 最后上報(bào)時(shí)間
private final WeakHashMap<Device, Long> lastReportTime =
new WeakHashMap<>();
// 更新狀態(tài)
public void updateState(Device device) {
lastReportTime.put(device, System.currentTimeMillis());
}
// 獲取在線設(shè)備列表(需配合ReferenceQueue清理)
public List<Device> getOnlineDevices() {
return new ArrayList<>(lastReportTime.keySet());
}
}優(yōu)勢分析:
當(dāng)設(shè)備斷開連接且不再被其他模塊引用時(shí),自動(dòng)清理狀態(tài)
避免因設(shè)備頻繁上下線導(dǎo)致的內(nèi)存增長
適合作為二級(jí)緩存,配合持久化存儲(chǔ)使用
綜上:
WeakHashMap 是解決特定類型內(nèi)存泄漏的有效工具,但需要充分理解其工作原理和適用場景。在實(shí)際物聯(lián)網(wǎng)系統(tǒng)中,通常需要結(jié)合軟引用、引用隊(duì)列等機(jī)制構(gòu)建更健壯的緩存系統(tǒng)。
----------------------------------------------基礎(chǔ)信息補(bǔ)充--------------------------------------------------------
除了上方方法,也能通過JDK自帶的工具jmap,jconsole來獲得一個(gè)堆轉(zhuǎn)儲(chǔ)文件。
jvm(java虛擬機(jī))管理的內(nèi)存大致包括三種不同類型的內(nèi)存區(qū)域:
PermanentGeneration space(永久保存區(qū)域)、Heap space(堆區(qū)域)、JavaStacks(Java棧)。
1,其中永久保存區(qū)域主要存放Class(類)和Meta的信息,Class第一次被Load的時(shí)候被放入PermGenspace區(qū)域,Class需要存儲(chǔ)的內(nèi)容主要包括方法和靜態(tài)屬性。
2,堆區(qū)域用來存放Class的實(shí)例(即對象),對象需要存儲(chǔ)的內(nèi)容主要是非靜態(tài)屬性。每次用new創(chuàng)建一個(gè)對象實(shí)例后,對象實(shí)例存儲(chǔ)在堆區(qū)域中,這部分空間也被jvm的垃圾回收機(jī)制管理。
3,而Java棧跟大多數(shù)編程語言包括匯編語言的棧功能相似,主要基本類型變量以及方法的輸入輸出參數(shù)。Java程序的每個(gè)線程中都有一個(gè)獨(dú)立的堆棧。
容易發(fā)生內(nèi)存溢出問題的內(nèi)存空間包括:PermanentGeneration space和Heap space。
第一種OutOfMemoryError:PermGenspace
發(fā)生這種問題的原意是程序中使用了大量的jar或class,使java虛擬機(jī)裝載類的空間不夠,與PermanentGeneration space有關(guān)。解決這類問題有以下兩種辦法:
1、增加java虛擬機(jī)中的XX:PermSize和XX:MaxPermSize參數(shù)的大小,其中XX:PermSize是初始永久保存區(qū)域大小,XX:MaxPermSize是最大永久保存區(qū)域大小。如針對tomcat,在catalina.sh或catalina.bat文件中一系列環(huán)境變量名說明結(jié)束處(大約在70行左右) 增加一行:
JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m"
第二種OutOfMemoryError:Java heap space
發(fā)生這種問題的原因是java虛擬機(jī)創(chuàng)建的對象太多,在進(jìn)行垃圾回收之間,虛擬機(jī)分配的到堆內(nèi)存空間已經(jīng)用滿了,與Heapspace有關(guān)。解決這類問題有兩種思路:
1、檢查程序,看是否有死循環(huán)或不必要地重復(fù)創(chuàng)建大量對象。找到原因后,修改程序和算法。
2、增加Java虛擬機(jī)中Xms(初始堆大小)和Xmx(最大堆大?。﹨?shù)的大小。如:set JAVA_OPTS= -Xms256m-Xmx1024m
第三種OutOfMemoryError:unable to create new nativethread
這種錯(cuò)誤在Java線程個(gè)數(shù)很多的情況下容易發(fā)生
GC
垃圾收集(GC)是Java內(nèi)存管理的重要機(jī)制之一。它負(fù)責(zé)自動(dòng)回收不再使用的對象所占用的內(nèi)存,以避免內(nèi)存泄漏和OOM問題的發(fā)生。
GC的工作原理主要涉及到兩個(gè)關(guān)鍵概念:標(biāo)記-清除(Mark-Sweep)和分代收集(Generational)。標(biāo)記-清除算法會(huì)遍歷整個(gè)堆空間,標(biāo)記出仍然被引用的對象,然后清除未被標(biāo)記的對象所占用的內(nèi)存。分代收集則是將堆空間劃分為新生代和老年代兩個(gè)區(qū)域,根據(jù)對象的存活周期采用不同的回收策略。
到此這篇關(guān)于在Spring Boot中淺嘗內(nèi)存泄漏的實(shí)戰(zhàn)記錄的文章就介紹到這了,更多相關(guān)Spring Boot內(nèi)存泄漏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Log4j2代碼方式配置實(shí)現(xiàn)線程級(jí)動(dòng)態(tài)控制
這篇文章主要介紹了使用Log4j2代碼方式配置實(shí)現(xiàn)線程級(jí)動(dòng)態(tài)控制,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
常用數(shù)據(jù)庫的驅(qū)動(dòng)程序及JDBC URL分享
這篇文章主要介紹了常用數(shù)據(jù)庫的驅(qū)動(dòng)程序及 JDBC URL,需要的朋友可以看下2014-01-01
IDEA中l(wèi)og4j 無法輸出到本地 properties配置無效問題
這篇文章主要介紹了IDEA中l(wèi)og4j 無法輸出到本地 properties配置無效問題,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10
Java中Druid連接池連接超時(shí)獲取不到連接的解決
這篇文章主要介紹了Java中Druid連接池連接超時(shí)獲取不到連接的解決,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
Java多線程局域網(wǎng)聊天室的實(shí)現(xiàn)
在學(xué)習(xí)了一個(gè)學(xué)期的java以后,搞了一個(gè)多線程的聊天室,熟悉了一下服務(wù)器和客戶機(jī)的操作。感興趣的小伙伴們可以參考一下2021-06-06
詳解Java如何使用集合來實(shí)現(xiàn)一個(gè)客戶信息管理系統(tǒng)
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用Java 集合實(shí)現(xiàn)一個(gè)客戶信息管理系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2021-11-11

