记一次内存泄露的排查过程
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、问题浮出水面
8、核心问题分析
orika是一个对象映射工具。由于这里没有用单例导致了内存的泄露
项目中大量使用了orika作为对象拷贝工具,但是都是在每个serviceImpl中去注册映射方式,其实整个项目中的方式也就5个,但是放到实现类中就会重复注册,而每次注册就会生成class信息,导致Metaspace占用空间增长。最终就出现了本项目中的问题。
解决办法
1、集中单例注册所有的orika的注册方式 2、优化jvm参数设置
9、后续
更多精彩,敬请关注, 程序员导航网 https://chenzhuofan.top