一次Java服务内存过高的分析过程

一次,java,服务,内存,分析,过程 · 浏览次数 : 684

小编点评

**内存分析报告** | 对象类型 | 数量 | 占用内存大小 | |---|---|---| | Object[] | 700M | 700M | | org.springframework.boot.loader.LaunchedURLClassLoader | 240M | 240M | | TagCateSimilarityUtils | 100M | 100M | | shallow | 450M | 450M | **分析结果** * **堆内内存分析**显示 700M 的对象占用 700M 的内存,其中包含 Spring Boot 类的 240M 对象。 * **堆外内存分析**显示,由于对象没有被回收,导致其数量超过了 1G,这可能导致内存泄漏。 * **Xms 设置**设置为 8g,这意味着 Java程序尝试分配 8g 的虚拟内存给进程。 * **内存占用**分析显示,即使申请了 10 MB 的实际内存,也只增长了 1 MB,这与虚拟内存分配的延迟分配策略有关。 **可能导致内存泄漏的原因** * **Spring Boot 类的对象**可能因某些原因无法被垃圾回收。 * **对象在内存中保持引用**,导致无法被垃圾回收。 * **线程对对象的锁定**会导致无法被垃圾回收。 * **网络问题**可能导致对象无法被及时释放。 **解决方案** * **设置合理的堆外内存大小**,以避免内存泄漏。 * **优化对象的生命周期**,以便更早回收垃圾对象。 * **解决线程的竞争问题**,以确保对象被正确回收。 * **监控网络连接状态**,并采取措施以避免网络阻塞。

正文

现象

年前,收到了短信报警,显示A服务的某台机器内存过高,超过80%

image.png

如上图所示,内存会阶段性增加。奇怪的是,十多台机器中只有这一台有这个问题

堆内内存分析

最先怀疑是内存泄漏的问题,所以首先使用jmap命令把堆dump下来

jmap -dump:format=b,file=service.hprof 1948

用MAT分析堆文件发现了一个奇怪的问题,下载下来7G的文件MAT显示的Size只有700M

image.png

后来知道不加live选项会把堆中所有的对象dump下来,即使是已经是垃圾的对象

参考 what are "live" objects in java heap? (heap dump with jmap)

live 选项会在讲堆内容dump到文件时,强制做一次fullGC,剩下的就是live对象,也就是从GC Root可以寻达的对象

为什么有时不能用live呢,因为fullGC可能会让应用卡主,不能接受这种情况适用不增加live选项

后来我重新使用了live选项dump下来有900M

jmap -dump:live,file=live-dump.bin <pid>

MAT的内存泄漏报告

首先用MAT的Leak Suspect看一下

image.png

看到了org.springframework.boot.loader.LaunchedURLClassLoader这个对象有240M

因为一直不太清楚live选项的原因,所以就想用其他工具看看这7G到底都是什么

IDEA自带工具分析

image.png

把hprof拖入IDEA,就可以看到上图,上面分析了所有的对象,从占用的大小就可以看出来

其中的Shallow表示的是:

对象本身占用内存的大小,也就是对象头加成员变量(不是成员变量的值)的总和

Retained表示的是:

如果一个对象被释放掉,那会因为该对象的释放而减少引用进而被释放的所有的对象(包括被递归释放的)所占用的heap大小,即对象被垃圾回收器回收后能被GC从内存中移除的所有对象之和。

具体参考一文让你理解什么是shallow heap及retained heap

我们把前几名的Shallow加起来也有好几G了

也可以点进Object[]查看这700多万的对象数组都是什么
image.png

可以看到一个占用450M的Object[10240]属于LaunchedURLClassLoader

image.png

上图中可以看到这450M中TagCateSimilarityUtils占了200M, 这是一个本地缓存,虽然占的多了一点,通过比对正常服务器的堆转储,是没有问题的

查看int[]对象时,发现了很多Retained是0的对象,也就是说这些对象已经是不可达对象,只不过还没有被回收,那是不是就是只要让这些对象回收了,内存占用就下来了
image.png

解决垃圾回收问题

上面分析中看到了很多对象没有被回收,怀疑是没有FullGC(Mixed)所以老年代中的垃圾对象,一直都没回收,看了一下JVM参数

-Xmx8g -Xms8g -Xmn4g -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=32m -Dfastjson.parser.safeMode=true

调整为,删除了一些默认和不生效的参数,移除了Xmn,交给G1自己调整

-Xmx8g -Xms8g  -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=32m -Dfastjson.parser.safeMode=true

不过并没有解决问题,此时我发现了新的问题:

  1. 按理GC完只有不到1G的对象,为什么监控中会显示内存很高呢
  2. 设置了Xms=8g,按理一启动就会占用至少8G内存,监控为什么是从6G开始增长的

这个问题和虚拟内存和实际内存有关,可以参考linux top命令 实存(RES)与虚存(VIRT)详解

VIRT:

1、进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据,以及malloc、new分配的堆空间和分配的栈空间等;
2、假如进程新申请10MB的内存,但实际只使用了1MB,那么它会增长10MB,而不是实际的1MB使用量。

RES:

1、进程当前使用的内存大小
2. 如果申请10MB的内存,实际使用1MB,它只增长1MB,与VIRT相反;

监控显示的内存是实际占用的内存,Xms设置的是虚拟内存,参考降低 Java 程序的“虚拟内存地址”占用

可以看出 Java Heap 与 Metaspace 紧挨着分配,两块一共占用了 3GB 的 Size(虚拟内存地址空间),而表征物理内存占用的 Rss 却只有 673288KB。也就是说,mmap 只是给进程分配一个线性区域(虚拟内存),并没有分配物理内存,只有当进程访问这块内存时,操作系统才会分配具体的内存页给进程,这就是 Linux 内存管理的延迟分配策略

内存一旦被分配给Java程序,就不会还给操作系统了,所以监控看起来内存就是只增不减,个人理解这样对于Java程序也是有好处的,不用频繁申请内存,对于垃圾的区域标记然后就可以重新写了

Java也存在JVM参数-XX:+AlwaysPreTouch 来实现启动就把堆内存全都申请到

为了抵消延迟分配策略,在进程启动时强制分配好 Java Heap 的物理内存,虽然增加了启动延时,但是可以减少进程运行时由于分配内存造成的延时

总结

堆内内存是正常的,不存在内存泄漏,只是正常的内存使用增长

堆外内存分析

在搜索LaunchedURLClassLoader内存泄漏问题时,看到了Spring Boot引起的“堆外内存泄漏”排查及经验总结,学到了如何进行堆外内存分析

一次完整的JVM堆外内存泄漏java故障排查记录也是一篇值得学习的如何分析堆外内存的文章

不过我们问题产生的原因和上面的不一致

使用pmap分析应用的内存占用,但不太容易看出是什么占用了内存

pmap -x 1927 | sort -k3 -r -n

image.png

JVM的NativeMemoryTracking参数会看到更详细,不过需要重启,而且会有5%-10%的性能损耗

// 写在启动参数上面
-XX:NativeMemoryTracking=detail

// 生成内容在下图中
jcmd 1927 VM.native_memory detail scale=MB > temp3.txt

image.png

经过对堆外内存的观察,发现确实有一些比较高,例如线程占700M,发现了有很多不必要的线程,但不是随着时间不断增加的

上图中的Total一行的commited=9788MB,表示Java应用程序堆内加堆外内存最大可达这么多,这么一算12 * 0.8 = 9.6G,确实超了报警阈值

这样看来,堆外内存也没有问题,是Xmx和内存阈值设置的不匹配,导致内存正常使用的情况下报警了,也是没有合理预估堆外内存占用的原因,堆外占了快2G的内存

将报警阈值调到85%,发现内存在周期性的增长和减少,并不会超过阈值,如下图

image.png

总结

服务的内存使用情况是正常的,无论是堆内还是堆外内存,需要做的是设置合理的堆内存大小,预估堆外内存大小,合理设置报警阈值

通过这次分析也学习了如何对Java程序内存进行分析,也学习了很多工具

也意思到监控工具的重要性,在看别人分析的过程中,一般都会对JVM资源进行监控,这样就能很明显看出资源的动态,因为我们目前没有这样的工具,所以分析起来就比较麻烦

前面提到其中只有一台机器有这个现象,还没有找到原因,不过有几个现象:

  1. 内存增长快
  2. 部分日志大小比其他机器大
  3. 耗时每隔一段时间会增加

image.png

对比日志发现是Redis超时比较多,所以怀疑可能是机器网络的问题,不过目前还无结论

参考

[1] 一文让你理解什么是shallow heap及retained heap
[2] linux top命令 实存(RES)与虚存(VIRT)详解
[3] 降低 Java 程序的“虚拟内存地址”占用
[4] 一次完整的JVM堆外内存泄漏java故障排查记录
[5] Spring Boot引起的“堆外内存泄漏”排查及经验总结

与一次Java服务内存过高的分析过程相似的内容:

一次Java服务内存过高的分析过程

现象 年前,收到了短信报警,显示A服务的某台机器内存过高,超过80% 如上图所示,内存会阶段性增加。奇怪的是,十多台机器中只有这一台有这个问题 堆内内存分析 最先怀疑是内存泄漏的问题,所以首先使用jmap命令把堆dump下来 jmap -dump:format=b,file=service.hpro

[转帖]java获取到heapdump文件后,如何快速分析?

https://www.jianshu.com/p/aaf56385766d 简介 在之前的OOM问题复盘之后,本周,又一Java服务出现了内存问题,这次问题不严重,只会触发堆内存占用高报警,没有触发OOM,但好在之前的复盘中总结了dump脚本,会在堆占用高时自动执行jstack与jmap,使得我们

[转帖]G1收集器基本介绍(-XX:+UseG1GC)

概述G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC 停顿时间要求的同时,还具备高吞吐量性能特征. 停顿时间要求的同时,还具备高吞吐量性能特征. G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可

为什么 java 容器推荐使用 ExitOnOutOfMemoryError 而非 HeapDumpOnOutOfMemoryError ?

前言 好久没写文章了, 今天之所以突然心血来潮, 是因为昨天出现了这样一个情况: 我们公司的某个手机APP后端的用户(customer)微服务出现内存泄露, 导致OutOfMemoryError, 但是因为经过我们精心优化的openjdk容器参数, 这次故障对用户完全无感知. :muscle::mu

KylinV10升级部分软件的简单方法

背景 2022-12-26有同事晚上在群里反馈客户现场的测试环境内存紧张. 我这边第一反应是进程重复了,导致内存使用量飙升. 告知现场使用 ps -ef |grep java |grep caf 发现只有一个进程. 然后使用 top 然后输入 M 使用内存排序: 发现除了java主服务之后还有 au

[转帖]fastJson与一起堆内存溢出'血案'

https://www.jianshu.com/p/876d443c2162 现象 QA同学反映登录不上服务器 排查问题1--日志级别 查看log,发现玩家登录的时候抛出了一个java.lang.OutOfMemoryError 大概代码是向Redis序列化一个PlayerMirror镜像数据,但是

[转帖]某游戏海外版本堆外内存泄露排查

https://www.jianshu.com/p/cae00d9c99fe 某游戏海外版本堆外内存泄露排查 现象 线上有部分服务器用top发现Java进程内存占用占比达到99,而且出现了有一个服务器被Linux OOM Kill 排查 选择了110服,该机器的Java进程最大堆内存设置的是9710

【Azure 存储服务】Java Storage SDK 调用 uploadWithResponse 代码示例(询问ChatGTP得代码原型后人力验证)

问题描述 查看Java Storage SDK,想找一个 uploadWithResponse 的示例代码,但是通过全网搜索,结果没有任何有帮助的代码。使用最近ChatGPT来寻求答案,得到非常有格式的内容: 问:java azure storage account to use uploadWit

Java单元测试及常用语句

编写Java单元测试用例,即把一段复杂的代码拆解成一系列简单的单元测试用例,并且无需启动服务,在短时间内测试代码中的处理逻辑。写好Java单元测试用例,其实就是把“复杂问题简单化,建单问题深入化“。在编写的过程中, 我们也可以对自己的代码进行一个二次检查。

【解决方案】Java 互联网项目中消息通知系统的设计与实现(上)

消息通知系统(notification-system)作为一个独立的微服务,完整地负责了 App 端内所有消息通知相关的后端功能实现。该系统既需要与文章系统、订单系统、会员系统等相关联,也需要和其它业务系统相关联,是一个偏底层的通用服务系统。