在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無法回收這些對(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); // 通過循環(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ù)限制堆大?。?/h4>
//在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
//在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ǔ)信息。
查找最大對(duì)象
打開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,避免長(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)闆]有設(shè)置過期時(shí)間或大小限制,緩存會(huì)無限增長(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ì)目的就是通過弱引用(Weak Reference)自動(dòng)清理不再被使用的鍵值對(duì),從而避免因?qū)ο髿埩魧?dǎo)致的內(nèi)存泄漏。
引用類型對(duì)比表:
引用類型 | 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)存無法回收 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類型 | 初始條目 | 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à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è)備斷開連接且不再被其他模塊引用時(shí),自動(dòng)清理狀態(tài)
避免因設(shè)備頻繁上下線導(dǎo)致的內(nèi)存增長(zhǎng)
適合作為二級(jí)緩存,配合持久化存儲(chǔ)使用
綜上:
WeakHashMap 是解決特定類型內(nèi)存泄漏的有效工具,但需要充分理解其工作原理和適用場(chǎng)景。在實(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í)例(即對(duì)象),對(duì)象需要存儲(chǔ)的內(nèi)容主要是非靜態(tài)屬性。每次用new創(chuàng)建一個(gè)對(duì)象實(shí)例后,對(duì)象實(shí)例存儲(chǔ)在堆區(qū)域中,這部分空間也被jvm的垃圾回收機(jī)制管理。
3,而Java棧跟大多數(shù)編程語(yǔ)言包括匯編語(yǔ)言的棧功能相似,主要基本類型變量以及方法的輸入輸出參數(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ū)域大小。如針對(duì)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)建的對(duì)象太多,在進(jìn)行垃圾回收之間,虛擬機(jī)分配的到堆內(nèi)存空間已經(jīng)用滿了,與Heapspace有關(guā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問題的發(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接收form-data和x-www-form-urlencoded數(shù)據(jù)的方法
form-data和x-www-form-urlencoded是兩種不同的HTTP請(qǐng)求體格式,本文主要介紹了SpringBoot接收form-data和x-www-form-urlencoded數(shù)據(jù)的方法,具有一定的參考價(jià)值,感興趣的可以了解一下2024-05-05springmvc Controller方法沒有加@ResponseBody導(dǎo)致api訪問404問題
這篇文章主要介紹了springmvc Controller方法沒有加@ResponseBody導(dǎo)致api訪問404問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01使用通過ARP類似P2P終結(jié)者實(shí)現(xiàn)數(shù)據(jù)封包
目前網(wǎng)絡(luò)上類似P2P終結(jié)者這類軟件,主要都是基于ARP欺騙實(shí)現(xiàn)的,網(wǎng)絡(luò)上到處都有關(guān)于ARP的介紹,不過為了本文讀者不需要再去查找,我就在這里大概講解一下2012-12-12SpringBoot3.X配置OAuth的代碼實(shí)踐
在進(jìn)行Java后端技術(shù)框架版本升級(jí)時(shí),特別是將SpringBoot從2.X升級(jí)到3.X,發(fā)現(xiàn)對(duì)OAuth的配置有大幅變更,新版本中刪除了多個(gè)常用配置類,本文給大家介紹SpringBoot3.X配置OAuth的相關(guān)知識(shí),感興趣的朋友一起看看吧2024-09-09springboot 單文件上傳的實(shí)現(xiàn)步驟
這篇文章主要介紹了springboot實(shí)現(xiàn)單文件上傳的方法,幫助大家更好的理解和使用springboot框架,感興趣的朋友可以了解下2021-02-02