在Spring Boot中浅尝内存泄漏

[复制链接]
发表于 2025-9-23 05:33:00 | 显示全部楼层 |阅读模式
使用静态聚集持有对象引用,制止GC回收

关键点:

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

  1. package io.renren.controller;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RestController;
  5. import org.springframework.web.client.RestTemplate;
  6. import java.util.ArrayList;
  7. import java.util.List;
  8. /**
  9. * author: lj
  10. * date: 2025-4
  11. */
  12. @RestController
  13. public class MemoryLeakController {
  14.     // 静态集合会持续持有对象引用
  15.     private static List<byte[]> LEAKING_LIST = new ArrayList<>();
  16.     // 内存泄漏端点
  17.     @GetMapping("/leak")
  18.     public String leakMemory() {
  19.         // 每次请求添加1MB数据(不会被释放)
  20.         LEAKING_LIST.add(new byte[1024 * 1024]);
  21.         return "已泄漏内存: " + LEAKING_LIST.size() + " MB";
  22.     }
  23.     // 触发OOM的测试方法(快速验证)
  24.     public static void main(String[] args) throws InterruptedException {
  25.         SpringApplication.run(MemoryLeakController.class, args);
  26.         // 通过循环请求快速触发OOM
  27.         while(true) {
  28.             new RestTemplate().getForObject("http://localhost:8080/leak", String.class);
  29.             Thread.sleep(100);
  30.         }
  31.     }
  32. }
复制代码
验证:

1,运行程序(启动时添加JVM参数限定堆巨细):

  1. //在cmd中先cd到jar包所在目录,执行如下命令启动
  2. //-Xmx100m 当程序需要更多内存时,JVM会尝试分配最多100MB的堆内存。如果超过这个限制,可能会抛出OutOfMemoryError
  3. //-Xms100m JVM在启动时分配的最小内存量。如果初始堆内存设置得过低,程序可能在运行过程中频繁扩展堆内存,影响性能
  4. //-XX:+HeapDumpOnOutOfMemoryError 在发生OutOfMemoryError时生成堆转储(Heap Dump)的功能
  5. 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,定期实行聚集清算操作。
  1. @Scheduled(fixedRate = 60_000)
  2. public void cleanLeakingData() {
  3.     LEAKING_LIST.removeIf(data -> /* 清理条件 */);
  4. }
复制代码
--------------------------------------------------更新---------------------------------------------------------
变种实现方式

  1. @SpringBootApplication
  2. @RestController
  3. @EnableCaching // 关键注解:启用缓存
  4. public class CacheLeakDemo {
  5.     // 模拟缓存未正确清理
  6.     @Cacheable("leakyCache")
  7.     @GetMapping("/cache-leak")
  8.     public byte[] cacheLeak() {
  9.         return new byte[1024 * 1024]; // 每次缓存1MB
  10.     }
  11.     public static void main(String[] args) {
  12.         SpringApplication.run(CacheLeakDemo.class, args);
  13.     }
  14. }
复制代码
缓存泄漏原理:
@Cacheable会将每次差别参数的返回结果缓存
由于没有设置逾期时间或巨细限定,缓存会无穷增长
示例中每个哀求天生唯一key(默认基于方法参数),导致缓存不停累积
调优发起

对于缓存使用WeakReference或框架(Caffeine/Ehcache)
  1. // 使用WeakHashMap解决
  2. private static Map<byte[], Boolean> SAFE_MAP =
  3.     Collections.synchronizedMap(new WeakHashMap<>());
复制代码
  1. // 使用Caffeine缓存并设置上限
  2. @Bean
  3. public CacheManager cacheManager() {
  4.     CaffeineCacheManager manager = new CaffeineCacheManager();
  5.     manager.setCaffeine(Caffeine.newBuilder()
  6.             .maximumSize(100)
  7.             .expireAfterWrite(10, TimeUnit.MINUTES));
  8.     return manager;
  9. }
复制代码
由于在 Java 中,WeakHashMap 的设计目标就是通过弱引用(Weak Reference)自动清算不再被使用的键值对,从而避免因对象残留导致的内存泄漏。
引用类型对比表:

引用类型GC活动典范应用场景强引用永不回收(除非显式置为null)平凡对象引用软引用内存不足时回收缓存弱引用下次GC立刻回收WeakHashMap/WeakReference虚引用回收时收到关照资源清算跟踪关键机制:
WeakHashMap 的 键(Key)使用弱引用存储
当键对象不再被其他强引用持偶尔,该键值对会被自动移除
值对象(Value)仍使用强引用,必要特别留意解耦
内存泄漏场景 vs WeakHashMap修复方案

  1. //使用普通HashMap导致泄漏
  2. public class LeakingCache {
  3.     private static Map<byte[], String> CACHE = new HashMap<>();
  4.     // 添加大对象到缓存
  5.     public static void addToCache(byte[] key, String value) {
  6.         CACHE.put(key, value);
  7.     }
  8.     public static void main(String[] args) {
  9.         // 模拟添加后不再使用key
  10.         byte[] key = new byte[1024 * 1024]; // 1MB
  11.         addToCache(key, "大数据");
  12.         
  13.         key = null; // 删除强引用
  14.         
  15.         // 触发GC
  16.         System.gc();
  17.         
  18.         // 缓存仍然持有key的强引用,导致1MB内存无法回收
  19.         System.out.println("缓存大小: " + CACHE.size()); // 输出1
  20.     }
  21. }
复制代码
  1. //使用WeakHashMap
  2. public class SafeCache {
  3.     // 使用WeakHashMap + 同步包装(线程安全
  4.     private static Map<byte[], String> SAFE_CACHE =
  5.         Collections.synchronizedMap(new WeakHashMap<>());
  6.     public static void addToCache(byte[] key, String value) {
  7.         SAFE_CACHE.put(key, value);
  8.     }
  9.     public static void main(String[] args) {
  10.         byte[] key = new byte[1024 * 1024];
  11.         addToCache(key, "安全数据");
  12.         
  13.         key = null; // 删除最后一个强引用
  14.         
  15.         // 强制GC(生产环境不要主动调用System.gc())
  16.         System.gc();
  17.         
  18.         // 给GC一点时间执行
  19.         try { Thread.sleep(1000); } catch (InterruptedException e) {}
  20.         
  21.         System.out.println("缓存大小: " + SAFE_CACHE.size()); // 输出0
  22.     }
  23. }
复制代码
实战应用

场景:装备毗连会话管理

  1. @RestController
  2. public class DeviceController {
  3.     // 使用WeakHashMap管理临时会话
  4.     private static Map<Device, Session> deviceSessions =
  5.         Collections.synchronizedMap(new WeakHashMap<>());
  6.     @PostMapping("/connect")
  7.     public String connect(@RequestBody Device device) {
  8.         Session session = new Session(device);
  9.         deviceSessions.put(device, session);
  10.         return "Connected";
  11.     }
  12.     // 当Device对象不再被外部引用时,自动清理会话
  13. }
复制代码
设置验证端点
  1. @GetMapping("/session-count")
  2. public int getSessionCount() {
  3.     return deviceSessions.size();
  4. }
复制代码
测试方法
  1. 1,发送连接请求
  2. curl -X POST http://localhost:8080/connect -d '{"id":"device1"}'
  3. 2,立即调用/session-count查看数量
  4. 3,停止持有Device对象引用后触发GC
  5. 4,再次检查会话数量
复制代码
增强版缓存实现(带自动清算)

  1. public class AdvancedCache<K, V> {
  2.     private final Map<K, V> cache =
  3.         new WeakHashMap<>();
  4.     private final ReferenceQueue<K> queue =
  5.         new ReferenceQueue<>();
  6.     public void put(K key, V value) {
  7.         // 清理已回收的条目
  8.         processQueue();
  9.         cache.put(key, value);
  10.     }
  11.     private void processQueue() {
  12.         Reference<? extends K> ref;
  13.         while ((ref = queue.poll()) != null) {
  14.             // 这里可以触发回调清理相关资源
  15.             System.out.println("清理条目: " + ref);
  16.         }
  17.     }
  18. }
复制代码
代码测试片断
  1. // 测试插入100万条数据
  2. IntStream.range(0, 1_000_000).forEach(i -> {
  3.     Object key = new Object();
  4.     map.put(key, "Value-" + i);
  5. });
  6. // 强制GC后统计剩余条目
  7. System.gc();
  8. Thread.sleep(1000);
  9. System.out.println("剩余条目: " + map.size());
复制代码
测试结果:
Map类型初始条目GC后剩余条目内存占用(MB)HashMap1,000,0001,000,00085.3WeakHashMap1,000,0003,2146.7场景:装备状态临时缓存

  1. public class DeviceStateManager {
  2.     // Key: 设备对象,Value: 最后上报时间
  3.     private final WeakHashMap<Device, Long> lastReportTime =
  4.         new WeakHashMap<>();
  5.     // 更新状态
  6.     public void updateState(Device device) {
  7.         lastReportTime.put(device, System.currentTimeMillis());
  8.     }
  9.     // 获取在线设备列表(需配合ReferenceQueue清理)
  10.     public List<Device> getOnlineDevices() {
  11.         return new ArrayList<>(lastReportTime.keySet());
  12.     }
  13. }
复制代码
优势分析:
当装备断开毗连且不再被其他模块引用时,自动清算状态
避免因装备频仍上下线导致的内存增长
适看成为二级缓存,共同恒久化存储使用
综上:
WeakHashMap 是办理特定类型内存泄漏的有用工具,但必要充实明白其工作原理和实用场景。在实际物联网物联网体系中,通常必要联合软引用、引用队列等机制构建更结实的缓存体系。
----------------------------------------------基础信息补充--------------------------------------------------------
除了上方方法,也能通过JDK自带的工具jmap,jconsole来得到一个堆转储文件。
jvm(java捏造机)管理的内存大致包罗三种差别类型的内存地域:

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捏造机中的XXermSize和XX:MaxPermSize参数的巨细,此中XXermSize是初始永久生存地域巨细,XX:MaxPermSize是最大永久生存地域巨细。如针对tomcat,在catalina.sh或catalina.bat文件中一系列情况变量名阐明竣事处(约莫在70行左右) 增长一行:
JAVA_OPTS=" -XXermSize=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)。标记-扫除算法会遍历整个堆空间,标记出仍然被引用的对象,然后扫除未被标记的对象所占用的内存。分代网络则是将堆空间分别为新生代和老年代两个地域,根据对象的存活周期采用差别的回收计谋。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

×
登录参与点评抽奖,加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表