在Spring Boot中浅尝内存泄漏的实战记录

2025-04-17 17:50

本文主要是介绍在Spring Boot中浅尝内存泄漏的实战记录,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《在SpringBoot中浅尝内存泄漏的实战记录》本文给大家分享在SpringBoot中浅尝内存泄漏的实战记录,结合实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧...

使用静态集合持有对象引用,阻止GC回收

关键点:

使用static List作为内存泄漏的锚点,其生命周期与ClassLoader一致
每次请求向列表添加1MB字节数组,这些对象会持续占用堆内存
由于集合持有强引用,GC无法回收这些对象
最终会导致OutOfMemoryError: Java heap space

可执行代码:

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 {
    // 静态集合会持续持有对象引用
    private static List<byte[]> LEAKING_LIST = new ArrayList<>();
    // 内存泄漏端点
    @GetMapping("/leak")
    public String leakMemory() {
        // 每次请求添加1MB数据(不会被释放)
        LEAKING_LIST.add(new byte[1024 * 1024]);
        return "已泄漏内存: " + LEAKING_LIST.size() + " MB";
    }
    // 触发OOM的测试方法(快速验证)
    public static void main(String[] args) throws InterruptedException {
        SpringApplication.run(MemoryLeakController.class, args);
        // 通过循环请求快速触发OOM
        while(true) {
            new RestTemplate().getForObject("http://localhost:8080/leak", String.class);
            Thread.sleep(100);
        }
    }
}

验证:

1,运行程序(启动时添加JVM参数限制堆大小):

//在cmd中先cd到jar包所在目录,执行如下命令启动
//-Xmx100m 当程序需要更多内存时,JVM会尝试分配最多100MB的堆内存。如果超过这个限制,可能会抛出OutOfMemoryError
//-Xms100m JVM在启动时分配的最小内存量。如果初始堆内存设置得过低,程序可能在运行过程中频繁扩展堆内存,影响性能。
//-XX:+HeapDumpOnOutOfMemoryError 在发生OutOfMemoryError时生成堆转储(Heap Dump)的功能
java -jar -Xmx100m -Xms100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\Temp renren-generator-1.0.0.jar

2,访问 http://localhost:8080/leak 触发泄漏

在Spring Boot中浅尝内存泄漏的实战记录

日志输出显示了内存泄漏位置。

在Spring Boot中浅尝内存泄漏的实战记录

并且在临时目录中保存了一份堆转储文件,稍后使用MAT(Memory Analyzer Tool)分析。

在Spring Boot中浅尝内存泄漏的实战记录

问题定位

使用jvisualvm工具定位问题

在cmd输入jvisualvm指令

在Spring Boot中浅尝内存泄漏的实战记录

选中应用后,可以监控应用程序的性能。

在Spring Boot中浅尝内存泄漏的实战记录

触发内存泄露后,查看每次GC的持续时间、回收的内存等信息。OOM之后,点击界面右上角的堆Dump,打开应用的堆转储信息。

在Spring Boot中浅尝内存泄漏的实战记录

查找最大对象

在Spring Boot中浅尝内存泄漏的实战记录

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

在Spring Boot中浅尝内存泄漏的实战记录

查看LEAKING_LIST的引用链,至此问题定位完成。

在Spring Boot中浅尝内存泄漏的实战记录

使用MAT(Memory Analyzer Tool)工具定位问题

下载地址:https://eclipse.dev/mat/download/previous/
我的是JDK8,所以我下载了Memory Analyzer 1.10.0 Release版本。下载完成后,直接解压,运行其中的MemoryAnalyzer.exe文件即可启动MAT工具。

用mat工具打开刚编程刚临时目录中保存的堆转储文件,点击Leak Suspects生成内存泄漏报表。

在Spring Boot中浅尝内存泄漏的实战记录

点击details查看java.lang.Object[]的保留堆

在Spring Boot中浅尝内存泄漏的实战记录

查看LEAKING_LIST的引用链,至此问题定位完成。

在Spring Boot中浅尝内存泄漏的实战记录

调优建议

1,避免长时间持有大对象引用。
2,定期执行集合清理操作。

@Scheduled(fixedRate = 60_000)
public void cleanLeakingData() {
    LEAKING_LIST.removeIf(data -> /* 清理条件 */);
}

--------------------------------------------------更新---------------------------------------------------------

变种实现方式

@SpringBootApplication
@RestController
@EnableCaching // 关键注解:启用缓存
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会将每次不同参数的返回结果缓存

因为没有设置过期时间或大小限制,缓存会无限增长

示例中每个请求生成唯一key(默认基于方法参数),导致缓存不断累积

调优建议

对于缓存使用WeakReference或框架(Caffeine/Ehcache)

// 使用WeakHashMap解决
private static Map<byte[], Boolean> SAFE_MAP = 
    Collections.synchronizedMap(new WeakHashMap<>());
// 使用Caffeine缓存并设置上限
@Bean
public CacheManager cacheManager() {
    CaffeineCacheManager manager = new CaffeineCacheManager();
    manager.setCaffeine(androidCaffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(10, TimeUnit.MINUTES));
    return manager;
}

因为在 Java 中,WeakHashMap 的设计目的就是通过弱引用(Weak Reference)自动清理不再被使用的键值对,从而避免因对象残留导致的内存泄漏。

引用类型对比表:

引用类型GC行为典型应用场景
强引用永不回收(除非显式置为null)普通对象引用
软引用内存不足时回收缓存
弱引用下次GC立即回收WeakHashMap/WeakReference
虚引用回收时收到通知资源清理跟踪

关键机制:
WeakHashMap 的 键(Key)使用弱引用存储
当键对象不再被其他强引用持有时,该键值对会被自动移除
值对象(Value)仍使用强引用,需要特别注意解耦

内存泄漏场景 vs WeakHashMap修复方案

//使用普通HashMap导致泄漏
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, "大数据");
        key = null; // 删除强引用
        // 触发GC
        Systandroidem.gc();
        // 缓存仍然持有key的强引用,导致1MB内存无法回收
        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, "安全数据");
        key = null; // 删除最后一个强引用
        // 强制GC(生产环境不要主动调用System.gc())
        System.gc();
        // 给GC一点时间执行
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        System.out.println("缓存大小: " + SAFE_CACHE.size()); // 输出0
    }
}

实战应用

场景:设备连接会话管理

@RestController
public class DeviceController {
    // 使用WeakHashMap管理临时会话
    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";
    }
    // 当Device对象不再被外部引用时,自动清理会话
}

配置验证端点

@GetMapping("/session-count")
public int getSessionCount() {
    return deviceSessions.size();
}

测试方法

1,发送连接请求
curl -X POST http://localhost:8080/connect -d '{"id":"device1"}'
2,立即调用/session-count查看数量
3,停止持有Device对象引用后触发GC
4,再次检查会话数量

增强版缓存实现(带自动清理)

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) {
            // 这里可以触发回调清理相关资源
            System.out.println("清理条目: " + ref);
        }
    }
}

代码测试片段

// 测试插入100万条数据
IntStream.range(0, 1_000_000).forEach(i -> {
    Object key = new Object();
    map.put(key, "Value-" + i);
});
// 强制GC后统计剩余条目
System.gc();
Thread.sleep(1000);
System.out.println("剩余条目: " + map.size());

测试结果:

Map类型初始条目GC后剩余条目内存占用(MB)
HashMap1,000,0001,000,00085.3
WeakHashMap1,000,0003,2146.7

场景:设备状态临时缓存

public class DeviceStateManager {
    // Key: 设备对象,Value: 最后上报时间
    private final WeakHashMap<Device, Long> lastReportTime = 
        new WeakHashMap<>();
    // 更新状态
    public void updateState(Device device) {
        lastReportTime.put(device, System.currentTimeMillis());
    }
    // 获取在线设备列表(需配合ReferenceQueue清理)
    public List<Device> getOnlineDevices() {
        return new ArrayList<>(lastReportTime.keySet());
    }
}

优势分析:
当设备断开连接且不再被其他模块引用时,自动清理状态
避免因设备频繁上下线导致的内存增长
适合作为二级缓存,配合持久化存储使用

综上:
WeakHashMap 是解决特定类型内存泄漏的有效工具,但需要充分理解其工作原理和适用场景。在实际物联网系统中,通常需要结合软引用、引用队列等机制构建更健壮的缓存系统。

----------------------------------------------基础信息补充--------------------------------------------------------
除了上方方法,也能通过JDK自带的工具jmap,jconsole来获得一个堆转储文件。

jvm(java虚拟机)管理的内存大致包括三种不同类型http://www.chinasem.cn的内存区域:

PermanentGeneration space(永久保存区域)、Heap space(堆区域)、JavaStacks(Java栈)。
1,其中永久保存区域主要存放Class(类)和Meta的信息,Class第一次被Load的时候被放入PermGenspace区域,Class需要存储的内容主要包括方法和静态属性。
2,堆区域用来存放Class的实例(即对象),对象需要存储的内容主要是非静态属性。每次用new创建一个对象实例后,对象实例存储在堆区域中,这部分空间也被jvm的垃圾回收机制管理。
3,而Java栈跟大多数编程语言包括汇编语言的栈功能相似,主要基本类型变量以及方法的输入输出参数。Java程序的每个线程中都有一个独立的堆栈。
容易发生内存溢出问题的内存空间包括:PermanentGeneration space和Heap space。

第一种OutOfMemoryError:PermGenspace

发生这种问题的原意是程序中使用了大量的jar或class,使java虚拟机装载类的空间不够,与PermanentGeneration space有关。解决这类问题有以下两种办法:

1、增加java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小,其中XX:PermSize是初始永久保存区域大小,XX:MaxPermSize是最大永久保存区域大小。如针对tomcat,在catalina.sh或catalina.BAT文件中一系列环境变量名说明结束处(大约在70行左右) 增android加一行:

JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m"

第二种OutOfMemoryError:Java heap space

发生这种问题的原因是java虚拟机创建的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了,与Heapspace有关。解决这类问题有两种思路:

1、检查程序,看是否有死循环或不必要地重复创建大量对象。找到原因后,修改程序和算法。

2、增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。如:set JAVA_OPTS= -Xms256m-Xmx1024m

第三种OutOfMemoryError:unable to create new nativethread

这种错误在Java线程个数很多的情况下容易发生

GC

垃圾收集(GC)是Java内存管理的重要机制之一。它负责自动回收不再使用的对象所占用的内存,以避免内存泄漏和OOM问题的发生。
GC的工作原理主要涉及到两个关键概念:标记-清除(Mark-Sweep)和分代收集(Generational)。标记-清除算法会遍历整个堆空间,标记出仍然被引用的对象,然后清除未被标记的对象所占用的内存。分代收集则是将堆空间划分为新生代和老年代两个区域,根据对象的存活周期采用不同的回收策略。

到此这篇关于在Spring Boot中浅尝内存泄漏的实战记录的文章就介绍到这了,更多相关Spring Boot内存泄漏内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持China编程(www.chinasem.cn)!

这篇关于在Spring Boot中浅尝内存泄漏的实战记录的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154261

相关文章

Python办公自动化实战之打造智能邮件发送工具

《Python办公自动化实战之打造智能邮件发送工具》在数字化办公场景中,邮件自动化是提升工作效率的关键技能,本文将演示如何使用Python的smtplib和email库构建一个支持图文混排,多附件,多... 目录前言一、基础配置:搭建邮件发送框架1.1 邮箱服务准备1.2 核心库导入1.3 基础发送函数二、

java使用protobuf-maven-plugin的插件编译proto文件详解

《java使用protobuf-maven-plugin的插件编译proto文件详解》:本文主要介绍java使用protobuf-maven-plugin的插件编译proto文件,具有很好的参考价... 目录protobuf文件作为数据传输和存储的协议主要介绍在Java使用maven编译proto文件的插件

Java中的数组与集合基本用法详解

《Java中的数组与集合基本用法详解》本文介绍了Java数组和集合框架的基础知识,数组部分涵盖了一维、二维及多维数组的声明、初始化、访问与遍历方法,以及Arrays类的常用操作,对Java数组与集合相... 目录一、Java数组基础1.1 数组结构概述1.2 一维数组1.2.1 声明与初始化1.2.2 访问

Javaee多线程之进程和线程之间的区别和联系(最新整理)

《Javaee多线程之进程和线程之间的区别和联系(最新整理)》进程是资源分配单位,线程是调度执行单位,共享资源更高效,创建线程五种方式:继承Thread、Runnable接口、匿名类、lambda,r... 目录进程和线程进程线程进程和线程的区别创建线程的五种写法继承Thread,重写run实现Runnab

Java 方法重载Overload常见误区及注意事项

《Java方法重载Overload常见误区及注意事项》Java方法重载允许同一类中同名方法通过参数类型、数量、顺序差异实现功能扩展,提升代码灵活性,核心条件为参数列表不同,不涉及返回类型、访问修饰符... 目录Java 方法重载(Overload)详解一、方法重载的核心条件二、构成方法重载的具体情况三、不构

PowerShell中15个提升运维效率关键命令实战指南

《PowerShell中15个提升运维效率关键命令实战指南》作为网络安全专业人员的必备技能,PowerShell在系统管理、日志分析、威胁检测和自动化响应方面展现出强大能力,下面我们就来看看15个提升... 目录一、PowerShell在网络安全中的战略价值二、网络安全关键场景命令实战1. 系统安全基线核查

Java通过驱动包(jar包)连接MySQL数据库的步骤总结及验证方式

《Java通过驱动包(jar包)连接MySQL数据库的步骤总结及验证方式》本文详细介绍如何使用Java通过JDBC连接MySQL数据库,包括下载驱动、配置Eclipse环境、检测数据库连接等关键步骤,... 目录一、下载驱动包二、放jar包三、检测数据库连接JavaJava 如何使用 JDBC 连接 mys

SpringBoot线程池配置使用示例详解

《SpringBoot线程池配置使用示例详解》SpringBoot集成@Async注解,支持线程池参数配置(核心数、队列容量、拒绝策略等)及生命周期管理,结合监控与任务装饰器,提升异步处理效率与系统... 目录一、核心特性二、添加依赖三、参数详解四、配置线程池五、应用实践代码说明拒绝策略(Rejected

一文详解SpringBoot中控制器的动态注册与卸载

《一文详解SpringBoot中控制器的动态注册与卸载》在项目开发中,通过动态注册和卸载控制器功能,可以根据业务场景和项目需要实现功能的动态增加、删除,提高系统的灵活性和可扩展性,下面我们就来看看Sp... 目录项目结构1. 创建 Spring Boot 启动类2. 创建一个测试控制器3. 创建动态控制器注

Java操作Word文档的全面指南

《Java操作Word文档的全面指南》在Java开发中,操作Word文档是常见的业务需求,广泛应用于合同生成、报表输出、通知发布、法律文书生成、病历模板填写等场景,本文将全面介绍Java操作Word文... 目录简介段落页头与页脚页码表格图片批注文本框目录图表简介Word编程最重要的类是org.apach