在項目開發上線的過程中,最近發現一個加解密服務隔7天左右就會出現以下問題:
Exception in thread “Timer-0” java.lang.OutOfMemoryError: Java heap space
(一開始使用findBugs進行掃描,並未掃描出可用結果)
首先,介紹一個免費開源分析dump的軟件Memory Analyzer,下載地址如下所示:(同事介紹)
http://www.eclipse.org/mat/downloads.php
此工具需要依賴於dump,可以根據以下命令生成JVM的dump文件
ps aux | grep xxx 查詢進程ID
jmap -dump:live,format=b,file=文件名.bin 進程ID
可將生成的dump文件下載到本地,使用Memory Analyzer打開其文件進行分析。
最好在進程剛剛開啓時就生成一個dump文件,在服務使用一段時間後再生成一個dump文件,兩個文件進行對比排除內存漏洞問題。
經過排查發現漏洞問題如下所示:
發現問題出現在了JceSecurity的verificationResults 屬性上,在verificationResults 屬性中存在了過多的BouncyCastleProvider,隨着應用的使用正在不斷的增多,未被GC回收。
對javax.crypto.JceSecurity進行反編譯查看代碼發現verificationResults 是static 類屬性,GC不會自動對其永久代進行回收。
對項目代碼進行排查,發現項目中使用代碼BouncyCastleProvider使用代碼如下所示:
Java代碼 收藏代碼
Cipher ci = Cipher.getInstance("RSA", new BouncyCastleProvider());
發現是坑的代碼,在服務每次使用的時候都會重新創建一個BouncyCastleProvider用來進行初始化密鑰的工具類。
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方法代碼進行了分析,代碼如下所示:
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定義成單例就可以了。
這裏BouncyCastleProvider只需要保證是一個,可以使用代碼Security.addProvider(new BouncyCastleProvider())來保證