在項目開發上線的過程當中,最近發現一個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定義成單例就能夠了。