卡卷网
当前位置:卡卷网 / 每日看点 / 正文

内存泄漏和内存溢出有啥区别?

作者:卡卷网发布时间:2024-12-15 00:21浏览数量:121次评论数量:0次

昨天线上环境崩了

java堆内存溢出。。。

报错:java.lang.OutOfMemoryError: Java heap space

下面我将我排查问题的思路和过程记录了下来

1. 场景

  1. 客户端跟Java服务端通过websocket连接建立长链接并发送语音数据(text格式)
  2. Java服务端跟听写引擎建立长链接并发送语音数据
  3. 听写引擎将实时转写的内容返回给Java服务端,Java服务端再将数据处理返回给客户端

2. 排查过程

2.1 通过命令查看异常进程

由于是堆内存溢出异常,所以我先检查了一下服务器的负载和堆内存试用情况

通过 top 命令查看服务器进程情况,发现pid为1的一个java进程cpu使用率达到90%多

内存泄漏和内存溢出有啥区别?  第1张

这个进程正好是出现异常的这个java应用

通过 jps 命令也可以查看正在运行的java应用

内存泄漏和内存溢出有啥区别?  第2张


2.2 分析堆内存使用情况

接下来查看这个应用的堆内存使用情况 通过 jmap -heap pid 这个命令

内存泄漏和内存溢出有啥区别?  第3张

简要的对上图 中的内存使用情况做简单的说明:

MinHeapFreeRatio = 40

说明:当堆内存的空闲比例低于这个值时,JVM 会尝试增加堆大小。

影响:设置为 40 表示当堆内存的空闲部分小于 40% 时,JVM 会考虑增加堆大小。

MaxHeapFreeRatio = 70

说明:当堆内存的空闲比例高于这个值时,JVM 会尝试减少堆大小。

影响:设置为 70 表示当堆内存的空闲部分大于 70% 时,JVM 会考虑减少堆大小。

MaxHeapSize = 536,870,912 (512.0 MB)

说明:JVM 最大堆内存大小。

影响:这是 JVM 可以使用的最大内存,当前设置为 512 MB。如果应用程序需要更多内存,可能会遇到 OutOfMemoryError。

NewSize = 11,141,120 (10.625 MB)

说明:新生代(Young Generation)的初始大小。

影响:这是新生代的最小容量,用于存储新创建的对象。

MaxNewSize = 178,913,280 (170.625 MB)

说明:新生代的最大容量。

影响:这是新生代可以增长到的最大大小。

OldSize = 22,413,312 (21.375 MB)

说明:老年代(Old Generation)的初始大小。

影响:这是老年代的最小容量,用于存储晋升对象。

NewRatio = 2

说明:老年代与新生代的比例。例如,NewRatio=2 意味着老年代是新生代的两倍大。

影响:调整新生代和老年代的比例,影响垃圾回收的频率和效率。

SurvivorRatio = 8

说明:Eden 区与 Survivor 区的比例。例如,SurvivorRatio=8 意味着 Eden 区是每个 Survivor 区的八倍大。

影响:调整 Eden 和 Survivor 区的大小,影响对象在年轻代中的存活时间。

MetaspaceSize = 21,807,104 (20.796875 MB)

说明:Metaspace 的初始大小,用于存储类元数据。

影响:Metaspace 的初始分配大小,影响类加载的速度。

CompressedClassSpaceSize = 1,073,741,824 (1024.0 MB)

说明:压缩类空间的大小,用于存储类的内部表示。

影响:限制类元数据的存储空间。

MaxMetaspaceSize = 17,592,186,044,415 MB

说明:Metaspace 的最大大小。实际上,这个值非常大,几乎等于无限制。

影响:理论上没有限制,但实际受限于物理内存和操作系统。

G1HeapRegionSize = 0 (0.0MB)

说明:G1 垃圾收集器的区域大小。这里显示为 0,意味着未使用 G1 收集器。

影响:确认当前使用的不是 G1 收集器。


简单分析: 可以很明显的看到新生代和老年代几乎完全被使用(99.99% 和 100%),这意味着几乎没有可用的堆内存

如果此时出现Java heap space很大可能是因为内存使用完 然后又有对象被分配,这个时候没有空间并且在执行垃圾回收的时候没有回收到对象 所以会溢出

可以通过 jstat -gc <pid> 命令查看垃圾回收信息(由于我当时没有执行 这里就不放截图了)

如果内存一直被占用没有被回收 大概率就是有对象或者数据一直被占用无法执行回收(这里是我们排查问题的关键)

2.3 借助工具分析

由于生产环境不方便排查问题,所以这里借助VisualVM工具

通过如下命令导出快照通过 VisualVM 进行分析

jmap -dump:format=b,file=/tmp/sdc.hprof pid


内存泄漏和内存溢出有啥区别?  第4张


VisualVM 工具就不多介绍了,在jdk的bin目录中就可以启动

将我们从服务器上导出的 .hprof 文件导入到VisualVM中进行分析

导入 -- > 选择文件

内存泄漏和内存溢出有啥区别?  第5张


找到Object类页面

发现char[] 占用极高

内存泄漏和内存溢出有啥区别?  第6张


点开发现里面存着大量的文本信息,这些信息正是客户端通过参数传过来的数据,这很不正常,一般来说客户端传递的参数不属于成员变量或者对象,数据会随着线程或者方法的执行完毕而销毁

内存泄漏和内存溢出有啥区别?  第7张


感觉问题应该是出现在这里,我们在点开一条数据查看 ps:一般堆内存溢出这个异常都是由于对象重复创建或者数组存储数据太大导致的,而我这次查看没有发现有重复创建对象或者大对象,总之,一开始一头雾水

发现确实有对象在引用着他,所以无法被回收

内存泄漏和内存溢出有啥区别?  第8张


2.4 本地复现问题

为了验证并且对比问题,我在本地复现了下生产环境的异常

为了快速模拟堆内存溢出,我将我本地程序堆内存故意调小,借助idea工具将堆内存调整为 60 M

内存泄漏和内存溢出有啥区别?  第9张


第二是借助jmeter工具进行压测,由于我的接口是websocket接口,所以还需要借助一个插件 JMeterWebSocketSamplers-1.2.9.jar (大家可以在网上搜一下,如果不能下载可以找我获取, 公主号回复: jw) 将插件放到jmeter的安装目录 /lib/ext/ 下

jmeter的试用这里就不过多介绍了,不熟悉的可以自行查阅一下

我这里设置了 1 个线程,间隔 3 s,一直循环

内存泄漏和内存溢出有啥区别?  第10张


通过我们的VisualVM进行监控

内存泄漏和内存溢出有啥区别?  第11张


压测后观察服务和VisualVM还有压测工具

通过察看结果树发现 17 次之后就异常了

内存泄漏和内存溢出有啥区别?  第12张

服务异常截图:

内存泄漏和内存溢出有啥区别?  第13张


观察压测工具: 可以看到50Mb的时候就溢出了(这里大家可能要留意下,后面分析应该还会用到)

内存泄漏和内存溢出有啥区别?  第14张


点击Heap Dump 生成快照文件 发现和生产环境是同样的问题

内存泄漏和内存溢出有啥区别?  第15张


3. 问题解决过程

接下来就开始排查为啥前端传进来的数据会被一直占用

ps:一开始我为了省事使用http接口压测了一下传输大数据,眼看就把cpu干冒烟了 也没压出异常来

那肯定就是websocket的锅 !!!

3.1 websocket缓冲区分析

上面查看分析快照的时候 也发现了被websocket的类HeapCharBuffer引用

内存泄漏和内存溢出有啥区别?  第16张


那 HeapCharBuffer 中 messageBufferText 到底是干啥的呢?

==messageBufferText== 通常是指用于存储接收到的文本消息的缓冲区。当通过 WebSocket 接受到文本消息时,这些消息会暂时存储在这个缓冲区中,直到被应用程序读取。

说到 messageBufferText 那就不得不提到 maxTextMessageBufferSize

==maxTextMessageBufferSize== 定义了上述缓冲区的最大容量,即可以存储的最大文本消息量或大小。这个值设定了接收缓冲区的界限,防止由于过多未处理的消息导致内存耗尽

这里需要注意⚠️一点,也是引起我们问题的所在:

就是每次建立一个websocket连接都会创建一个最大的缓冲区 maxTextMessageBufferSize

那 maxTextMessageBufferSize 的大小是如何定义的呢,通过websocket的配置类来定义 我之前的定义是 512 k

@Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); // 在此处设置bufferSize container.setMaxTextMessageBufferSize(512000); container.setMaxBinaryMessageBufferSize(512000); container.setMaxSessionIdleTimeout(60 * 1000L); // 60s 超时时间 return container; }

通过上面的分析,可以得出以下结论:

  1. 缓冲区设置的内存偏大导致建立大量连接的时候占用大量内存,而实际每次建立连接的时候缓冲区可能用不了512k
  2. 连接结束之后缓冲区没有被清空

还记得我们上面压测的数据吗,17次之后就异常了,按照我们的结论每次建立连接消耗512k内存,那17次就是8704k 大约8.5mb,我们的堆初始的使用内存大约为40mb多,然后出现溢出的时候使用内存是大约50mb,那正好就是加上我们的8.5mb出现的异常,这里只是一个估算,肯定还有其他地方有内存消耗

为了验证我又将 maxTextMessageBufferSize 的大小调整为了 20k 然后压测 发现300多次才出现异常,那肯定就是因为maxTextMessageBufferSize引起的

但是调整maxTextMessageBufferSize的大小还不能从根本上解决问题,还要找到问题的根源,那就是为啥连接关闭后缓冲区没有被回收。

3.2 GC Root 引用链分析

堆内存都满了,但是数据都没有被垃圾回收只能说明一个问题,就是你的对象还被别人引用着。

接下来我们就通过查看messageBufferText的引用链来寻找问题的根源

我直接截图吧,我们找到有问题的数据(这个在上面使用工具的时候已经讲过了),一开始我们没有往深了挖这个链路,现在通过GC Root这个工具将链路展开

可以很清晰的看到我们的messageBufferText 的大概引用流程: messageBufferText --> ConcurrentHashMap --> ClassLoader(这个是根级) messageBufferText一直在被ConcurrentHashMap引用着 无法被释放


内存泄漏和内存溢出有啥区别?  第17张


最后我排查了下我的代码,连接每次都会正常关闭,问题就出在 ConcurrentHashMap 上,我使用 ConcurrentHashMap 存储了客户端实例,在存储的时候使用 seesionId 作为key,然后加了前缀。。

大概是这样:

内存泄漏和内存溢出有啥区别?  第18张


但是在清除client实例的时候,大概是这样的(当时估计犯神经了):

内存泄漏和内存溢出有啥区别?  第19张


神龙见首不见尾 !!!

虽然是由一个不起眼的小问题引起的,但是整个堆内存溢出的排查思路还是值得我们复盘和学习的。

One more thing

原来时间真的不是一条横跨在你面前的河,有着此岸和彼岸,而是一条挂在悬崖上的瀑布,奔流直下,一去无回。

有兴趣一起学习,加入编程小组的同学可以点击

Java内存泄露生产环境完整排查思路

END

免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。

卡卷网

卡卷网 主页 联系他吧

请记住:卡卷网 Www.Kajuan.Net

欢迎 发表评论:

请填写验证码