问题现象
最近有一个同事负责的项目,运行一断时间就OOM了,让我帮忙排查一下是什么原因,首先我查看项目启动命令配置了dump文件,看到dump文件居然有3.4G,肯定是代码有问题导致内存没有回收。
-XX:+HeapDumpBeforeFullGC -XX:+HeapDumpAfterFullGC -XX:HeapDumpPath=\dump
排查过程
选择Biggest Objects,看到javax.crypto.JceSecurity这个对象居然有1.9G
选择javax.crypto.JceSecurity 然后 选择Use Selected Objects,References 里选择Outgoing references
然后可以看到verificationResults 这个HasMap里居然有14349个BouncyCastleProvider对象,看来就是这个对象没有释放导致内存泄露了。
e
在项目里搜索了一下BouncyCastleProvider,这个类是在RSAUtill被实例化了,看来是在做RSA加解密时出现的问题
RSA解密代码如下,应该就是 Cipher.getInstance(“RSA”,new org.bouncycastle.jce.provider.BouncyCastleProvider()) 这行代码导致的
public static byte[] decrypt(PrivateKey pk, byte[] raw) throws Exception {
try {
//这里是RSA解密,每解密一次创建了一个对象
Cipher cipher = Cipher.getInstance("RSA",
new org.bouncycastle.jce.provider.BouncyCastleProvider());
cipher.init(cipher.DECRYPT_MODE, pk);
int blockSize = cipher.getBlockSize();
ByteArrayOutputStream bout = new ByteArrayOutputStream(64);
int j = 0;
while (raw.length - j * blockSize > 0) {
bout.write(cipher.doFinal(raw, j * blockSize, blockSize));
j++;
}
return bout.toByteArray();
} catch (Exception e) {
throw new Exception(e.getMessage());
}
}
进入 Cipher.getInstance()方法中,关键有一行代码 Exception ve = JceSecurity.getVerificationResult(provider);
public static final Cipher getInstance(String transformation,
Provider provider)
throws NoSuchAlgorithmException, NoSuchPaddingException
{
if (provider == null) {
throw new IllegalArgumentException("Missing provider");
}
Exception failure = null;
List<Transform> transforms = getTransforms(transformation);
boolean providerChecked = false;
String paddingError = null;
for (Transform tr : transforms) {
Service s = provider.getService("Cipher", tr.transform);
if (s == null) {
continue;
}
if (providerChecked == false) {
//此处获取了provider
Exception ve = JceSecurity.getVerificationResult(provider);
if (ve != null) {
String msg = "JCE cannot authenticate the provider "
+ provider.getName();
throw new SecurityException(msg, ve);
}
providerChecked = true;
}
if (tr.supportsMode(s) == S_NO) {
continue;
}
if (tr.supportsPadding(s) == S_NO) {
paddingError = tr.pad;
continue;
}
try {
CipherSpi spi = (CipherSpi)s.newInstance(null);
tr.setModePadding(spi);
Cipher cipher = new Cipher(spi, transformation);
cipher.provider = s.getProvider();
cipher.initCryptoPermission();
return cipher;
} catch (Exception e) {
failure = e;
}
}
// throw NoSuchPaddingException if the problem is with padding
if (failure instanceof NoSuchPaddingException) {
throw (NoSuchPaddingException)failure;
}
if (paddingError != null) {
throw new NoSuchPaddingException
("Padding not supported: " + paddingError);
}
throw new NoSuchAlgorithmException
("No such algorithm: " + transformation, failure);
}
在JceSecurity.getVerificationResult(provider) 找到了 verificationResults.put(p, PROVIDER_VERIFIED),每解密一次,就会像verificationResults对象中put一次provider对象,怪不得verificationResults 这个HasMap里居然有14349个BouncyCastleProvider对象。原来都找到了。
解决方法
将 Cipher 改写成单例就好了
private static Cipher cipher;
static {
try {
cipher = Cipher.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
} catch (Exception e) {
e.printStackTrace();
}
}
提示
javax.crypto.Cipher 自带的JDK没有原码 ,经查阅资料发现,jdk加密相关的类javax.crypto.Cipher不提供源码文档,原因是受美国政府强加密算法出口管制限制而不提供源代码。美国政府对涉及到加密算法和强加密的软件技术产品进行出口管制和限制,基于这个法律的要求,JDK 不能提供有关强加密的源代码。不过可以从 https://jdk.java.net/java-se-ri/8-MR3 下载