一次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

一次JVM GC长暂停的排查过程

在高并发下,Java程序的GC问题属于很典型的一类问题,带来的影响往往会被进一步放大。不管是「GC频率过快」还是「GC耗时太长」,由于GC期间都存在Stop The World问题,因此很容易导致服务超时,引发性能问题。

一次JVM GC长暂停的排查过程

作者:京东科技 徐传乐 背景 在高并发下,Java程序的GC问题属于很典型的一类问题,带来的影响往往会被进一步放大。不管是「GC频率过快」还是「GC耗时太长」,由于GC期间都存在Stop The World问题,因此很容易导致服务超时,引发性能问题。 事情最初是线上某应用垃圾收集出现Full GC异

[转帖]一次 Java 进程 OOM 的排查分析(glibc 篇)

https://juejin.cn/post/6854573220733911048 遇到了一个 glibc 导致的内存回收问题,查找原因和实验的的过程是比较有意思的,主要会涉及到下面这些: Linux 中典型的大量 64M 内存区域问题 glibc 的内存分配器 ptmalloc2 的底层原理 如

[转帖]一次 Java 进程 OOM 的排查分析(glibc 篇)

https://juejin.cn/post/6854573220733911048 遇到了一个 glibc 导致的内存回收问题,查找原因和实验的的过程是比较有意思的,主要会涉及到下面这些: Linux 中典型的大量 64M 内存区域问题 glibc 的内存分配器 ptmalloc2 的底层原理 如

记一次多个Java Agent同时使用的类增强冲突问题及分析

摘要:Java Agent技术常被用于加载class文件之前进行拦截并修改字节码,以实现对Java应用的无侵入式增强。 本文分享自华为云社区《记一次多个JavaAgent同时使用的类增强冲突问题及分析》,作者:Vansittart。 问题背景 Java Agent技术常被用于加载class文件之前进

[转帖]【JVM】JVM概述

1.JVM定义 JVM 是Java Virtual Machine(JVM )的缩写,Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令进行执行,这样实现了Java“一次编译,到处运行”。 2.JVM组成 JVM由三大部分组成:类加载器(ClassLoader subsystem),执

Java面试题:如果你这样做,你会后悔的,两次启动同一个线程~~~

当一个线程被启动后,如果再次调start()方法,将会抛出IllegalThreadStateException异常。 这是因为Java线程的生命周期只有一次。调用start()方法会导致系统在新线程中运行执行体,但是如果线程已经结束,则不能再次使用,需要重新创建一个新的线程对象并调用start()...

Java CompletableFuture 异步超时实现探索

JDK 8 是一次重大的版本升级,新增了非常多的特性,其中之一便是 CompletableFuture。自此从 JDK 层面真正意义上的支持了基于事件的异步编程范式,弥补了 Future 的缺陷。 在我们的日常优化中,最常用手段便是多线程并行执行。这时候就会涉及到 CompletableFuture 的使用。

Web攻防--Java_SQL注入--XXE注入-- SSTI模板注入--SPEL表达式注入

预编译 编译器在编译sql语句时,会依次进行词法分析、语法分析、语义分析等操作, 预编译技术会让数据库跳过编译阶段,也就无法就进行词法分析,关键字不会被拆开,注入语句也就不会被识别为SQL的关键字,从而防止恶意注入语句改变原有SQL语句本身逻辑。 Java_JDBC注入 在使用JDBC进行数据库操作