一次项目上内存溢出分析实践(javax.crypto.JceSecurity)

一、现象描述

公司在xx项目的单点登录服务器内存持续升高,无法回收,造成内存溢出

二、问题分析

(1). 生成dump文件

    ps aux | grep cas 查询单点登录服务器进程ID jmap -dump:live,format=b,file=cas.bin 进程ID

(2). 内存分析工具(MAT)进行分析

1. 查看概览视图

一次项目上内存溢出分析实践(javax.crypto.JceSecurity) 可以看到javax.crypto.JceSecurity 占用了全部内存(1.9GB)的90%左右

2. Dominator Tree视图

Shallow heap & Retained heap概念详解

Shallow Size 对象自身占用的内存大小,不包括它引用的对象。 针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。当然这里面还会包括一些java语言特性的数据存储单元。 针对数组类型的对象,它的大小是数组元素对象的大小总和。 Retained Size Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C, C就是间接引用) 换句话说,Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存。 不过,释放的时候还要排除被GC Roots直接或间接引用的对象。他们暂时不会被被当做Garbage。 一次项目上内存溢出分析实践(javax.crypto.JceSecurity)

(3). 视图分析

通过该视图可知class javax.crypto.JceSecurity @ 0x5f4a2af48 这个一个实列占用了大量内存,而这个实例下面有两个java.util.IdentityHashMap的属性,其中 @ 0x5f4f38dd8(即verificationResults) 占用了91%的内存,继续展开这个内存可以看到java.util.IdentityHashMap @ 0x5f4f38dd8之所以占用大量内存是由于该实例下面存在10189个org.bouncycastle.jce.provider.BouncyCastleProvider类的实例, 所以该问题的直接原因是因为进程中构造了大量的BouncyCastleProvider实例存储在JceSecurity.verificationResults中,而JceSecurity.verificationResults又是静态的,造成GC无法回收。

(4). 调用链分析

调用链查找默认遵循一个原则:jdk以及开源源码没有缺陷(虽然有时候不一定正确),调用链最终跟踪到我们自己写的代码

    通过分析JceSecurity类源码可知调用verificationResults.put的代码只有JceSecurity.getVerificationResult方法,而调用链上游如下几个地方,都是jdk里面的,可能一下子无从查找: 一次项目上内存溢出分析实践(javax.crypto.JceSecurity)

    回到造成内存溢出的直接原因“大量的BouncyCastleProvider实例无法回收” 一次项目上内存溢出分析实践(javax.crypto.JceSecurity) 再开发工具中根据关键字“new BouncyCastleProvider” 搜索相关代码发现有7处代码构造BouncyCastleProvider实例,但继续向上寻找调用链发现最终只有 DesHelper() 是有效的,并且此时的DesHelper已经是我们自定义的代码,DesHelper.encrypt 会调用Cipher.getInstance 方法,与步骤1中的调用链衔接上。

    对DesHelper()上游调用链树进行分析 一次项目上内存溢出分析实践(javax.crypto.JceSecurity) 又可引出很多业务代码,所以需要尽量保持DesHelper的构造方法签命不变(如果对上游业务代码熟悉,改变方法签命也可以)

三、解决方案

所以解决问题的实现方案之一可以如下: 一次项目上内存溢出分析实践(javax.crypto.JceSecurity)

来源url
栏目