在项目开发上线的过程当中,最近发现一个Dubbo服务隔7天左右就会出现如下问题:php
Exception in thread "Timer-0" java.lang.OutOfMemoryError: Java heap spacejava
(一开始使用findBugs进行扫描,并未扫描出可用结果)eclipse
首先,介绍一个免费开源分析dump的软件Memory Analyzer,下载地址以下所示:(同事介绍)ide
http://www.eclipse.org/mat/downloads.php
此工具须要依赖于dump,能够根据如下命令生成JVM的dump文件工具
ps aux | grep xxx 查询进程IDspa
jmap -dump:live,format=b,file=文件名.bin 进程IDcode
可将生成的dump文件下载到本地,使用Memory Analyzer打开其文件进行分析。orm
最好在进程刚刚开启时就生成一个dump文件,在服务使用一段时间后再生成一个dump文件,两个文件进行对比排除内存漏洞问题。对象
通过排查发现漏洞问题以下所示:进程
发现问题出如今了JceSecurity的verificationResults 属性上,在verificationResults 属性中存在了过多的BouncyCastleProvider,随着应用的使用正在不断的增多,未被GC回收。
对javax.crypto.JceSecurity进行反编译查看代码发现verificationResults 是static 类属性,GC不会自动对其永久代进行回收。
对项目代码进行排查,发现项目中使用代码BouncyCastleProvider使用代码以下所示:
Java代码
Cipher ci = Cipher.getInstance("RSA", new BouncyCastleProvider());
发现是坑张的代码,在服务每次使用的时候都会从新建立一个BouncyCastleProvider用来进行初始化密钥的工具类。
Java代码
public static final Cipher getInstance(String paramString, Provider paramProvider) throws NoSuchAlgorithmException, NoSuchPaddingException{ if (paramProvider == null) { throw new IllegalArgumentException("Missing provider"); } Object localObject1 = null; List localList = getTransforms(paramString); int i = 0; String str = null; for (Iterator localIterator = localList.iterator(); localIterator.hasNext(); ) { Transform localTransform = (Transform)localIterator.next(); Provider.Service localService = paramProvider.getService("Cipher", localTransform.transform); if (localService == null) continue; Object localObject2; Object localObject3; if (i == 0){ localObject2 = JceSecurity.getVerificationResult(paramProvider); if (localObject2 != null) { localObject3 = "JCE cannot authenticate the provider " + paramProvider.getName(); throw new SecurityException((String)localObject3, (Throwable)localObject2); } i = 1; } if (localTransform.supportsMode(localService) == 0) { continue; } if (localTransform.supportsPadding(localService) == 0) { str = localTransform.pad; } try{ localObject2 = (CipherSpi)localService.newInstance(null); localTransform.setModePadding((CipherSpi)localObject2); localObject3 = new Cipher((CipherSpi)localObject2, paramString); ((Cipher)localObject3).provider = localService.getProvider(); ((Cipher)localObject3).initCryptoPermission(); return localObject3; } catch (Exception localException) { localObject1 = localException; } } if (localObject1 instanceof NoSuchPaddingException) { throw ((NoSuchPaddingException)localObject1); } if (str != null) { throw new NoSuchPaddingException("Padding not supported: " + str); } throw new NoSuchAlgorithmException("No such algorithm: " + paramString, localObject1); }
可查看BouncyCastleProvider代码发现此类进行过特殊处理,每次new出的实例hashCode是相同的。又对JceSecurity.getVerificationResult方法代码进行了分析,代码以下所示:
Java代码
static synchronized Exception getVerificationResult(Provider paramProvider){ Object localObject1 = verificationResults.get(paramProvider); if (localObject1 == PROVIDER_VERIFIED) return null; if (localObject1 != null) { return (Exception)localObject1; } if (verifyingProviders.get(paramProvider) != null) { return new NoSuchProviderException("Recursion during verification"); }Exception localException2; try { verifyingProviders.put(paramProvider, Boolean.FALSE); URL localURL = getCodeBase(paramProvider.getClass()); verifyProviderJar(localURL); verificationResults.put(paramProvider, PROVIDER_VERIFIED); localException2 = null; return localException2; } catch (Exception localException1){ verificationResults.put(paramProvider, localException1); localException2 = localException1; return localException2; } finally { verifyingProviders.remove(paramProvider); } }
查找到这里发现本身愈来愈矛盾,每次new出来的BouncyCastleProvider具备相同的hashCode,放在verificationResults 属性Map中怎么会愈来愈多,后一个应当会将前一个覆盖才对,怎么会致使内存溢出。
最终实在无头绪请教同事,发现一个verificationResults属性定义的竟然是IdentityHashMap,此Map在存储类的时候并非使用类的equals方法来判断是否Key已经存在,而是使用==来判断是否Key已经存在的。换句话说就是当两个对象不==那此Map就会将两个对象都存进去。
找到这里问题的解决方案就已经很是明了了,只要给BouncyCastleProvider定义成单例就能够了。