欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

在Spring Boot中淺嘗內(nèi)存泄漏的實(shí)戰(zhàn)記錄

 更新時(shí)間:2025年04月17日 11:36:08   作者:江湖人稱-杰  
本文給大家分享在Spring Boot中淺嘗內(nèi)存泄漏的實(shí)戰(zhàn)記錄,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧

使用靜態(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

2,訪問 http://localhost:8080/leak 觸發(fā)泄漏

請(qǐng)?zhí)砑訄D片描述

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

請(qǐng)?zhí)砑訄D片描述

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

請(qǐng)?zhí)砑訄D片描述

問題定位

使用jvisualvm工具定位問題

在cmd輸入jvisualvm指令

請(qǐng)?zhí)砑訄D片描述

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

請(qǐng)?zhí)砑訄D片描述

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

請(qǐng)?zhí)砑訄D片描述

查找最大對(duì)象

請(qǐng)?zhí)砑訄D片描述

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

請(qǐng)?zhí)砑訄D片描述

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

請(qǐng)?zhí)砑訄D片描述

使用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)表。

請(qǐng)?zhí)砑訄D片描述

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

請(qǐng)?zhí)砑訄D片描述

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

請(qǐng)?zhí)砑訄D片描述

調(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)
HashMap1,000,0001,000,00085.3
WeakHashMap1,000,0003,2146.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ù)的方法

    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-05
  • java實(shí)現(xiàn)斗地主小案例

    java實(shí)現(xiàn)斗地主小案例

    這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)斗地主小案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-02-02
  • Java調(diào)用明華RF讀寫器DLL文件過程解析

    Java調(diào)用明華RF讀寫器DLL文件過程解析

    這篇文章主要介紹了Java調(diào)用明華RF讀寫器DLL文件過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-12-12
  • springmvc Controller方法沒有加@ResponseBody導(dǎo)致api訪問404問題

    springmvc 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ù)封包

    使用通過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-12
  • SpringBoot3.X配置OAuth的代碼實(shí)踐

    SpringBoot3.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-09
  • java 示例講解循環(huán)語(yǔ)句的使用

    java 示例講解循環(huán)語(yǔ)句的使用

    順序結(jié)構(gòu)的程序語(yǔ)句只能被執(zhí)行一次。如果您想要同樣的操作執(zhí)行多次,就需要使用循環(huán)結(jié)構(gòu),循環(huán)結(jié)構(gòu)就是在循環(huán)條件滿足的情況下,反復(fù)執(zhí)行特定代碼
    2022-04-04
  • 詳解怎么用Java的super關(guān)鍵字

    詳解怎么用Java的super關(guān)鍵字

    今天帶大家學(xué)習(xí)Java中super關(guān)鍵字是怎么用的,文中有非常詳細(xì)的介紹,對(duì)正在學(xué)習(xí)的小伙伴們很有幫助,需要的朋友可以參考下
    2021-06-06
  • springboot 單文件上傳的實(shí)現(xiàn)步驟

    springboot 單文件上傳的實(shí)現(xiàn)步驟

    這篇文章主要介紹了springboot實(shí)現(xiàn)單文件上傳的方法,幫助大家更好的理解和使用springboot框架,感興趣的朋友可以了解下
    2021-02-02
  • Java中List遍歷刪除元素remove()的方法

    Java中List遍歷刪除元素remove()的方法

    這篇文章主要介紹了Java中List遍歷刪除元素remove()的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-11-11

最新評(píng)論