這是第一次把問題分析的總結記錄下來,一是記錄下做備忘,二是把問題分析的過程和總結梳理下。
一共在兩個系統(tǒng)碰過因為加密導致OOM的問題:
第一次遇到這個問題的時候什么也不懂,只知道渾身發(fā)抖心亂跳……。不知道問題產(chǎn)生的原因更不知道該從何查起,運維同事給打了份dump日志,對我來說什么用都沒有。沒辦法只能請當時組里的牛人幫看。然后他就告訴我把一個變量設置成靜態(tài)的,修改后,發(fā)布到服務器上果然沒有再內(nèi)存飆升直至OOM了。當時也沒有請教下問題的根本原因是什么,只是問題解決就松了一口氣。
第二次是另外一個系統(tǒng),但是那個系統(tǒng)不像第一次碰到的系統(tǒng)那樣發(fā)布上去碰到訪問高峰就OOM。這個系統(tǒng)問題發(fā)現(xiàn)的比較有意思,為什么說有意思呢?因為問題一直都存在,只不過加密工具類調(diào)用的次數(shù)少,再加上這個系統(tǒng)發(fā)布比較頻繁,所以一直沒有OOM。直到有一次半個月沒有更新發(fā)布才報了OOM。
后來開始學習了解jvm,嘗試著去模擬重現(xiàn)當時的場景,然后分析系統(tǒng)OOM的原因。兩次問題的共同點都是多次調(diào)用加密類導致的。所以問題應該就在這個加密類。
模擬的代碼如下:
public static voidencrypt(){ try{ Cipher cipher = Cipher.getInstance("RSA", newBouncyCastleProvider()); // cipher.init(); }catch(NoSuchAlgorithmException e) { e.printStackTrace(); }catch(NoSuchPaddingException e) { e.printStackTrace(); } }
jvm參數(shù)設置:
-Xmx10m -Xms10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/baitianxia/Documents/oom/heapdump.hprof
循環(huán)調(diào)用上面的方法就可以制造OOM了。
既然問題可以重現(xiàn),下面就可以開始分析問題的原因了:
jvm參數(shù)設置的是當OOM的時候打印heap dump到指定目錄。分析heap dump文件常用的工具是MAT(Memory Analyzer Tool)。
1)用MAT打開heapdump.hprof文件的截圖如圖1,看到占用內(nèi)存最大的是餅圖中的深藍色部分,點擊顯示JceSecurity類,那么可以初步斷定問題是由這個類導致的;

2)點擊Actions下的Dominator Tree可以查看占用內(nèi)存最大的對象,點擊后如圖2??梢钥吹接幸粋€IdentityHashMap存儲了大量的BouncyCastleProvider對象;

圖2
3)點擊JceSecurity-->List Objects-->with outgoing references顯示如圖3所示,可以看到是變量名為verificationResults的identityHashMap中存放了大量的BouncyCastleProvier,基本上就已經(jīng)找到導致問題的原因了;

4)查看源碼,可以看到因為verificationResults是靜態(tài)的,不會被GC,所以隨著加密工具類調(diào)用的次數(shù)增加,verificationResults存儲的BouncyCastle也越來越多,最終導致OOM。
public static final Cipher getInstance(String var0, Provider var1) throws NoSuchAlgorithmException, NoSuchPaddingException {
if(var1 == null) {
throw new IllegalArgumentException("Missing provider");
} else {
Exception var2 = null;
List var3 = getTransforms(var0);
boolean var4 = false;
String var5 = null;
Iterator var6 = var3.iterator();
while(true) {
while(true) {
Cipher.Transform var7;
Service var8;
do {
do {
if(!var6.hasNext()) {
if(var2 instanceof NoSuchPaddingException) {
throw (NoSuchPaddingException)var2;
}
if(var5 != null) {
throw new NoSuchPaddingException("Padding not supported: " + var5);
}
throw new NoSuchAlgorithmException("No such algorithm: " + var0, var2);
}
var7 = (Cipher.Transform)var6.next();
var8 = var1.getService("Cipher", var7.transform);
} while(var8 == null);
if(!var4) {
Exception var9 = JceSecurity.getVerificationResult(var1);
if(var9 != null) {
String var12 = "JCE cannot authenticate the provider " + var1.getName();
throw new SecurityException(var12, var9);
}
var4 = true;
}
} while(var7.supportsMode(var8) == 0);
if(var7.supportsPadding(var8) != 0) {
try {
CipherSpi var13 = (CipherSpi)var8.newInstance((Object)null);
var7.setModePadding(var13);
Cipher var10 = new Cipher(var13, var0);
var10.provider = var8.getProvider();
var10.initCryptoPermission();
return var10;
} catch (Exception var11) {
var2 = var11;
}
} else {
var5 = var7.pad;
}
}
}
}
}
private static finalMap verificationResults =newIdentityHashMap();
static synchronized Exception getVerificationResult(Provider var0) {
Object var1 = verificationResults.get(var0);
if(var1 == PROVIDER_VERIFIED) {
return null;
} else if(var1 != null) {
return (Exception)var1;
} else if(verifyingProviders.get(var0) != null) {
return new NoSuchProviderException("Recursion during verification");
} else {
Exception var3;
try {
verifyingProviders.put(var0, Boolean.FALSE);
URL var2 = getCodeBase(var0.getClass());
verifyProviderJar(var2);
verificationResults.put(var0, PROVIDER_VERIFIED);
var3 = null;
return var3;
} catch (Exception var7) {
verificationResults.put(var0, var7);
var3 = var7;
} finally {
verifyingProviders.remove(var0);
}
return var3;
}
}
至此,問題的根本原因已經(jīng)找到了。
解決方法就是將BouncyCastlePrivate 設置一個靜態(tài)的,而不是每次都new一個。
private static BouncyCastleProvider bouncyCastleProvider = new BouncyCastleProvider();
Cipher cipher = Cipher.getInstance("RSA",bouncyCastleProvider);
另外說一點,之所以每次向IdentityHashMap類型verificationResults中put new BouncyCastleProvider會導致OOM是因為IdentityHashMap比較key值是否相等對比的是引用即“==”而HashMap是p.hash== hash &&
((k = p.key) == key || (key !=null&& key.equals(k))),至于為什么verificationResults是IdentityHashMap類型的還要再看看源碼才能知道。
現(xiàn)在回想下第一個系統(tǒng)OOM很好理解,第二個系統(tǒng)之所以一段時間不重啟才會OOM就是因為verificationResults本身是靜態(tài)的再加上應用調(diào)用加密工具類的次數(shù)不多,所以才會有這種現(xiàn)象,比較有趣!我之所以強調(diào)verificationResults是靜態(tài)的,因為只有是靜態(tài)對象才會出現(xiàn)這種系統(tǒng)運行一段時間才會OOM的現(xiàn)象。后面我會再寫一個內(nèi)存溢出的案例。
參考書:
深入理解Java虛擬機:JVM高級特性與最佳實踐
內(nèi)存dump分析工具:
Memory Analyzer (MAT)
參考文檔:
內(nèi)存快照排查OOM,加密時錯誤方法指定provider方式錯誤引起的OOM【原創(chuàng)】
這篇文章寫得詳細,非常推薦,可以說我寫的基本是照抄他的,只是為了加深下自己的印象。