在Spring Boot中淺嘗內(nèi)存泄漏的實(shí)戰(zhàn)記錄
使用靜態(tài)集合持有對(duì)象引用,阻止GC回收
關(guān)鍵點(diǎn):
使用static List作為內(nèi)存泄漏的錨點(diǎn),其生命周期與ClassLoader一致
每次請(qǐng)求向列表添加1MB字節(jié)數(shù)組,這些對(duì)象會(huì)持續(xù)占用堆內(nèi)存
由于集合持有強(qiáng)引用,GC無(wú)法回收這些對(duì)象
最終會(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ù)持有對(duì)象引用
private static List<byte[]> LEAKING_LIST = new ArrayList<>();
// 內(nèi)存泄漏端點(diǎn)
@GetMapping("/leak")
public String leakMemory() {
// 每次請(qǐng)求添加1MB數(shù)據(jù)(不會(huì)被釋放)
LEAKING_LIST.add(new byte[1024 * 1024]);
return "已泄漏內(nèi)存: " + LEAKING_LIST.size() + " MB";
}
// 觸發(fā)OOM的測(cè)試方法(快速驗(yàn)證)
public static void main(String[] args) throws InterruptedException {
SpringApplication.run(MemoryLeakController.class, args);
// 通過(guò)循環(huán)請(qǐng)求快速觸發(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)存。如果超過(guò)這個(gè)限制,可能會(huì)拋出OutOfMemoryError //-Xms100m JVM在啟動(dòng)時(shí)分配的最小內(nèi)存量。如果初始堆內(nèi)存設(shè)置得過(guò)低,程序可能在運(yùn)行過(guò)程中頻繁擴(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,訪問(wèn) http://localhost:8080/leak 觸發(fā)泄漏

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

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

問(wèn)題定位
使用jvisualvm工具定位問(wèn)題
在cmd輸入jvisualvm指令

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

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

查找最大對(duì)象

打開(kāi)java.lang.Object[]的保留堆

查看LEAKING_LIST的引用鏈,至此問(wèn)題定位完成。

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

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

查看LEAKING_LIST的引用鏈,至此問(wèn)題定位完成。

調(diào)優(yōu)建議
1,避免長(zhǎng)時(shí)間持有大對(duì)象引用。
2,定期執(zhí)行集合清理操作。
@Scheduled(fixedRate = 60_000)
public void cleanLeakingData() {
LEAKING_LIST.removeIf(data -> /* 清理?xiàng)l件 */);
}--------------------------------------------------更新---------------------------------------------------------
變種實(shí)現(xiàn)方式
@SpringBootApplication
@RestController
@EnableCaching // 關(guān)鍵注解:?jiǎ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)闆](méi)有設(shè)置過(guò)期時(shí)間或大小限制,緩存會(huì)無(wú)限增長(zhǎng)
示例中每個(gè)請(qǐng)求生成唯一key(默認(rèn)基于方法參數(shù)),導(dǎo)致緩存不斷累積
調(diào)優(yōu)建議
對(duì)于緩存使用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ì)目的就是通過(guò)弱引用(Weak Reference)自動(dòng)清理不再被使用的鍵值對(duì),從而避免因?qū)ο髿埩魧?dǎo)致的內(nèi)存泄漏。
引用類(lèi)型對(duì)比表:
| 引用類(lèi)型 | GC行為 | 典型應(yīng)用場(chǎng)景 |
|---|---|---|
| 強(qiáng)引用 | 永不回收(除非顯式置為null) | 普通對(duì)象引用 |
| 軟引用 | 內(nèi)存不足時(shí)回收 | 緩存 |
| 弱引用 | 下次GC立即回收 | WeakHashMap/WeakReference |
| 虛引用 | 回收時(shí)收到通知 | 資源清理跟蹤 |
關(guān)鍵機(jī)制:
WeakHashMap 的 鍵(Key)使用弱引用存儲(chǔ)
當(dāng)鍵對(duì)象不再被其他強(qiáng)引用持有時(shí),該鍵值對(duì)會(huì)被自動(dòng)移除
值對(duì)象(Value)仍使用強(qiáng)引用,需要特別注意解耦
內(nèi)存泄漏場(chǎng)景 vs WeakHashMap修復(fù)方案
//使用普通HashMap導(dǎo)致泄漏
public class LeakingCache {
private static Map<byte[], String> CACHE = new HashMap<>();
// 添加大對(duì)象到緩存
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)存無(wú)法回收
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)用
場(chǎ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對(duì)象不再被外部引用時(shí),自動(dòng)清理會(huì)話
}配置驗(yàn)證端點(diǎn)
@GetMapping("/session-count")
public int getSessionCount() {
return deviceSessions.size();
}測(cè)試方法
1,發(fā)送連接請(qǐng)求
curl -X POST http://localhost:8080/connect -d '{"id":"device1"}'
2,立即調(diào)用/session-count查看數(shù)量
3,停止持有Device對(duì)象引用后觸發(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);
}
}
}代碼測(cè)試片段
// 測(cè)試插入100萬(wàn)條數(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());測(cè)試結(jié)果:
| Map類(lèi)型 | 初始條目 | GC后剩余條目 | 內(nèi)存占用(MB) |
|---|---|---|---|
| HashMap | 1,000,000 | 1,000,000 | 85.3 |
| WeakHashMap | 1,000,000 | 3,214 | 6.7 |
場(chǎng)景:設(shè)備狀態(tài)臨時(shí)緩存
public class DeviceStateManager {
// Key: 設(shè)備對(duì)象,Value: 最后上報(bào)時(shí)間
private final WeakHashMap<Device, Long> lastReportTime =
new WeakHashMap<>();
// 更新?tīng)顟B(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)勢(shì)分析:
當(dāng)設(shè)備斷開(kāi)連接且不再被其他模塊引用時(shí),自動(dòng)清理狀態(tài)
避免因設(shè)備頻繁上下線導(dǎo)致的內(nèi)存增長(zhǎng)
適合作為二級(jí)緩存,配合持久化存儲(chǔ)使用
綜上:
WeakHashMap 是解決特定類(lèi)型內(nèi)存泄漏的有效工具,但需要充分理解其工作原理和適用場(chǎng)景。在實(shí)際物聯(lián)網(wǎng)系統(tǒng)中,通常需要結(jié)合軟引用、引用隊(duì)列等機(jī)制構(gòu)建更健壯的緩存系統(tǒng)。
----------------------------------------------基礎(chǔ)信息補(bǔ)充--------------------------------------------------------
除了上方方法,也能通過(guò)JDK自帶的工具jmap,jconsole來(lái)獲得一個(gè)堆轉(zhuǎn)儲(chǔ)文件。
jvm(java虛擬機(jī))管理的內(nèi)存大致包括三種不同類(lèi)型的內(nèi)存區(qū)域:
PermanentGeneration space(永久保存區(qū)域)、Heap space(堆區(qū)域)、JavaStacks(Java棧)。
1,其中永久保存區(qū)域主要存放Class(類(lèi))和Meta的信息,Class第一次被Load的時(shí)候被放入PermGenspace區(qū)域,Class需要存儲(chǔ)的內(nèi)容主要包括方法和靜態(tài)屬性。
2,堆區(qū)域用來(lái)存放Class的實(shí)例(即對(duì)象),對(duì)象需要存儲(chǔ)的內(nèi)容主要是非靜態(tài)屬性。每次用new創(chuàng)建一個(gè)對(duì)象實(shí)例后,對(duì)象實(shí)例存儲(chǔ)在堆區(qū)域中,這部分空間也被jvm的垃圾回收機(jī)制管理。
3,而Java棧跟大多數(shù)編程語(yǔ)言包括匯編語(yǔ)言的棧功能相似,主要基本類(lèi)型變量以及方法的輸入輸出參數(shù)。Java程序的每個(gè)線程中都有一個(gè)獨(dú)立的堆棧。
容易發(fā)生內(nèi)存溢出問(wèn)題的內(nèi)存空間包括:PermanentGeneration space和Heap space。
第一種OutOfMemoryError:PermGenspace
發(fā)生這種問(wèn)題的原意是程序中使用了大量的jar或class,使java虛擬機(jī)裝載類(lèi)的空間不夠,與PermanentGeneration space有關(guān)。解決這類(lèi)問(wèn)題有以下兩種辦法:
1、增加java虛擬機(jī)中的XX:PermSize和XX:MaxPermSize參數(shù)的大小,其中XX:PermSize是初始永久保存區(qū)域大小,XX:MaxPermSize是最大永久保存區(qū)域大小。如針對(duì)tomcat,在catalina.sh或catalina.bat文件中一系列環(huán)境變量名說(shuō)明結(jié)束處(大約在70行左右) 增加一行:
JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m"
第二種OutOfMemoryError:Java heap space
發(fā)生這種問(wèn)題的原因是java虛擬機(jī)創(chuàng)建的對(duì)象太多,在進(jìn)行垃圾回收之間,虛擬機(jī)分配的到堆內(nèi)存空間已經(jīng)用滿了,與Heapspace有關(guān)。解決這類(lèi)問(wèn)題有兩種思路:
1、檢查程序,看是否有死循環(huán)或不必要地重復(fù)創(chuàng)建大量對(duì)象。找到原因后,修改程序和算法。
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)回收不再使用的對(duì)象所占用的內(nèi)存,以避免內(nèi)存泄漏和OOM問(wèn)題的發(fā)生。
GC的工作原理主要涉及到兩個(gè)關(guān)鍵概念:標(biāo)記-清除(Mark-Sweep)和分代收集(Generational)。標(biāo)記-清除算法會(huì)遍歷整個(gè)堆空間,標(biāo)記出仍然被引用的對(duì)象,然后清除未被標(biāo)記的對(duì)象所占用的內(nèi)存。分代收集則是將堆空間劃分為新生代和老年代兩個(gè)區(qū)域,根據(jù)對(duì)象的存活周期采用不同的回收策略。
到此這篇關(guān)于在Spring Boot中淺嘗內(nèi)存泄漏的實(shí)戰(zhàn)記錄的文章就介紹到這了,更多相關(guān)Spring Boot內(nèi)存泄漏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot接收f(shuō)orm-data和x-www-form-urlencoded數(shù)據(jù)的方法
form-data和x-www-form-urlencoded是兩種不同的HTTP請(qǐng)求體格式,本文主要介紹了SpringBoot接收f(shuō)orm-data和x-www-form-urlencoded數(shù)據(jù)的方法,具有一定的參考價(jià)值,感興趣的可以了解一下2024-05-05
Java調(diào)用明華RF讀寫(xiě)器DLL文件過(guò)程解析
這篇文章主要介紹了Java調(diào)用明華RF讀寫(xiě)器DLL文件過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
springmvc Controller方法沒(méi)有加@ResponseBody導(dǎo)致api訪問(wèn)404問(wèn)題
這篇文章主要介紹了springmvc Controller方法沒(méi)有加@ResponseBody導(dǎo)致api訪問(wèn)404問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
使用通過(guò)ARP類(lèi)似P2P終結(jié)者實(shí)現(xiàn)數(shù)據(jù)封包
目前網(wǎng)絡(luò)上類(lèi)似P2P終結(jié)者這類(lèi)軟件,主要都是基于ARP欺騙實(shí)現(xiàn)的,網(wǎng)絡(luò)上到處都有關(guān)于ARP的介紹,不過(guò)為了本文讀者不需要再去查找,我就在這里大概講解一下2012-12-12
SpringBoot3.X配置OAuth的代碼實(shí)踐
在進(jìn)行Java后端技術(shù)框架版本升級(jí)時(shí),特別是將SpringBoot從2.X升級(jí)到3.X,發(fā)現(xiàn)對(duì)OAuth的配置有大幅變更,新版本中刪除了多個(gè)常用配置類(lèi),本文給大家介紹SpringBoot3.X配置OAuth的相關(guān)知識(shí),感興趣的朋友一起看看吧2024-09-09
springboot 單文件上傳的實(shí)現(xiàn)步驟
這篇文章主要介紹了springboot實(shí)現(xiàn)單文件上傳的方法,幫助大家更好的理解和使用springboot框架,感興趣的朋友可以了解下2021-02-02

