在Spring Boot中浅尝内存泄漏的实战记录
目录
- 使用静态集合持有对象引用,阻止GC回收
- 关键点:
- 可执行代码:
- 验证:
- 1,运行程序(启动时添加JVM参数限制堆大小):
- 2,访问 http://localhost:8080/leak 触发泄漏
- 问题定位
- 使用jvisualvm工具定位问题
- 使用MAT(Memory Analyzer Tool)工具定位问题
- 调优建议
- 变种实现方式
- 调优建议
- 引用类型对比表:
- 内存泄漏场景 vs WeakHashMap修复方案
- 实战应用
- 场景:设备连接会话管理
- 增强版缓存实现(带自动清理)
- 场景:设备状态临时缓存
- jvm(Java虚拟机)管理的内存大致包括三种不同类型的内存区域:
- 第一种OutOfMemoryError:PermGenspace
- 第二种OutOfMemoryError:Java heap space
- 第三种OutOfMemoryError:unable to create new nativethread
使用静态集合持有对象引用,阻止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 触发泄漏
日志输出显示了内存泄漏位置。
并且在临时目录中保存了一份堆转储文件,稍后使用MAT(Memory Analyzer Tool)分析。
问题定位
使用jvisualvm工具定位问题
在cmd输入jvisualvm指令
选中应用后,可以监控应用程序的性能。
触发内存泄露后,查看每次GC的持续时间、回收的内存等信息。OOM之后,点击界面右上角的堆Dump,打开应用的堆转储信息。
查找最大对象
打开java.lang.Object[]的保留堆
查看LEAKING_LIST的引用链,至此问题定位完成。
使用MAT(Memory Analyzer Tool)工具定位问题
下载地址:https://eclipse.dev/mat/download/previous/
我的是JDK8,所以我下载了Memory Analyzer 1.10.0 Release版本。下载完成后,直接解压,运行其中的MemoryAnalyzer.exe文件即可启动MAT工具。用mat工具打开刚编程刚临时目录中保存的堆转储文件,点击Leak Suspects生成内存泄漏报表。
点击details查看java.lang.Object[]的保留堆
查看LEAKING_LIST的引用链,至此问题定位完成。
调优建议
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) |
---|---|---|---|
HashMap | 1,000,000 | 1,000,000 | 85.3 |
WeakHashMap | 1,000,000 | 3,214 | 6.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.devze.com的内存区域:
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内存泄漏内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论