记一次内存泄露的排查过程

记一次内存泄露的排查过程

1、现象分析

新产品上线后,流量导入过来,服务的内存持续升高,且有增无减。随着流量的升高,内存增加的速度也是线性增涨。

2、准备知识

2.1、java memory结构

2.1.1、分代概念

对于垃圾收集算法来说,分代回收是高级算法之一。对象按照生成时间进行分代,刚刚生成不久的年轻对象划为新生代(Young
gen-eration),而存活了较长时间的对象划为老生代(Old
generation)。根据具体实现方式的不同,可能还会划分更多的代。比如有的把永久代也算做一个代。

2.1.2、memory划分

java memory主要分heap memory 和 non-heap memory,其计算公式如下:

Max memory = [-Xmx] + [-XX:MaxPermSize] + number_of_threads * [-Xss]
2.1.3、heap结构

按分代,分young-eden,young-survivor,old
用-Xmn,-Xms,-Xmx来指定

2.1.4、non-heap结构

包括metaspace,thread stacks,compiled native code,memory allocated by native code

-XX:PermSize或-XX:MetaspceSize,-Xss或-XX:ThreadStackSize

2.2 、PermGen与Metaspace

2.2.1、字符串常量池的变化
  • 在java7的时候将字符串常量池则移到java heap

所有的被intern的String被存储在PermGen区.PermGen区使用-XX:MaxPermSize=N来设置最大大小,但是由于应用程序string.intern通常是不可预测和不可控的,因此不好设置这个大小。设置不好的话,常常会引起

java.lang.OutOfMemoryError: PermGen space
  • java7,8的字符串常量池在堆中实现

字符串常量池被限制在整个应用的堆内存中,在运行时调用String.intern()增加字符串常量不会使永久代OOM了。

2.2.2、方法区的变化
  • java8的时候去除PermGen,将其中的方法区移到non-heap中的Metaspace
  • Metaspace属于non-heap
  • OOM异常
如果类元数据的空间占用达到MaxMetaspaceSize设置的值,将会触发对象和类加载器的垃圾回收。
java.lang.OutOfMemoryError: Metaspace space
JVM从Metaspace在捕获一个一个内存分配失败后抛出。

2.3、Metaspace相关参数

  • -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

2.4 、小结

将常量池从PermGen剥离到heap中,将元数据从PermGen剥离到元数据区,去除PermGen的好处如下:

  • 将字符串常量池从PermGen分离出来,与类元数据分开,提升类元数据的独立性
  • 将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。

在PermGen中元数据可能会随着每一次Full GC发生而进行移动。HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理PermGen中的元数据,分离出来以后可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化。

  • 为后续将HotSpot与JRockit合二为一做准备。

PermGen是HotSpot的实现特有的,JRockit并没有PermGen一说

3、工具准备

  • jvisualvm.exe
  • javamelody
  • druid
  • arthas
  • jmeter

4、初步分析

2.1、内存泄露

通过javamelody分析发现堆内存并未出现大的泄露问题

5、怀疑排查

3.1、druid连接池的问题

​ 第一、本人对druid连接池做了拦截处理

​ 第二、druid的默认监控数据是保存在内存中

3.2、spring-retry框架

spring-retry与springboot整合不当可能导致类重复加载

3.3、orika对象映射

多例使用orika的mapper对象可能导致堆外内存溢出

6、猜测验证

4.1、本地junit单元测试

4.2、jmeter压力测试

4.3、arthas在线观察内存使用情况

7、问题浮出水面

记一次内存泄露的排查过程_第1张图片

8、核心问题分析

orika是一个对象映射工具。由于这里没有用单例导致了内存的泄露
项目中大量使用了orika作为对象拷贝工具,但是都是在每个serviceImpl中去注册映射方式,其实整个项目中的方式也就5个,但是放到实现类中就会重复注册,而每次注册就会生成class信息,导致Metaspace占用空间增长。最终就出现了本项目中的问题。
  • 解决办法

    1、集中单例注册所有的orika的注册方式
    2、优化jvm参数设置
    

9、后续

更多精彩,敬请关注, 程序员导航网 https://chenzhuofan.top

记一次内存泄露的排查过程_第2张图片

你可能感兴趣的:(性能优化)