这是第一次把问题分析的总结记录下来,一是记录下做备忘,二是把问题分析的过程和总结梳理下。
一共在两个系统碰过因为加密导致OOM的问题:
第一次遇到这个问题的时候什么也不懂,只知道浑身发抖心乱跳……。不知道问题产生的原因更不知道该从何查起,运维同事给打了份dump日志,对我来说什么用都没有。没办法只能请当时组里的牛人帮看。然后他就告诉我把一个变量设置成静态的,修改后,发布到服务器上果然没有再内存飙升直至OOM了。当时也没有请教下问题的根本原因是什么,只是问题解决就松了一口气。
第二次是另外一个系统,但是那个系统不像第一次碰到的系统那样发布上去碰到访问高峰就OOM。这个系统问题发现的比较有意思,为什么说有意思呢?因为问题一直都存在,只不过加密工具类调用的次数少,再加上这个系统发布比较频繁,所以一直没有OOM。直到有一次半个月没有更新发布才报了OOM。
后来开始学习了解jvm,尝试着去模拟重现当时的场景,然后分析系统OOM的原因。两次问题的共同点都是多次调用加密类导致的。所以问题应该就在这个加密类。
模拟的代码如下:
public static voidencrypt(){ try{ Cipher cipher = Cipher.getInstance("RSA", newBouncyCastleProvider()); // cipher.init(); }catch(NoSuchAlgorithmException e) { e.printStackTrace(); }catch(NoSuchPaddingException e) { e.printStackTrace(); } }
jvm参数设置:
-Xmx10m -Xms10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/baitianxia/Documents/oom/heapdump.hprof
循环调用上面的方法就可以制造OOM了。
既然问题可以重现,下面就可以开始分析问题的原因了:
jvm参数设置的是当OOM的时候打印heap dump到指定目录。分析heap dump文件常用的工具是MAT(Memory Analyzer Tool)。
1)用MAT打开heapdump.hprof文件的截图如图1,看到占用内存最大的是饼图中的深蓝色部分,点击显示JceSecurity类,那么可以初步断定问题是由这个类导致的;
2)点击Actions下的Dominator Tree可以查看占用内存最大的对象,点击后如图2。可以看到有一个IdentityHashMap存储了大量的BouncyCastleProvider对象;
图2
3)点击JceSecurity-->List Objects-->with outgoing references显示如图3所示,可以看到是变量名为verificationResults的identityHashMap中存放了大量的BouncyCastleProvier,基本上就已经找到导致问题的原因了;
图34)查看源码,可以看到因为verificationResults是静态的,不会被GC,所以随着加密工具类调用的次数增加,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;
}
}
至此,问题的根本原因已经找到了。
解决方法就是将BouncyCastlePrivate 设置一个静态的,而不是每次都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类型的还要再看看源码才能知道。
现在回想下第一个系统OOM很好理解,第二个系统之所以一段时间不重启才会OOM就是因为verificationResults本身是静态的再加上应用调用加密工具类的次数不多,所以才会有这种现象,比较有趣!我之所以强调verificationResults是静态的,因为只有是静态对象才会出现这种系统运行一段时间才会OOM的现象。后面我会再写一个内存溢出的案例。
参考书:
深入理解Java虚拟机:JVM高级特性与最佳实践
内存dump分析工具:
Memory Analyzer (MAT)
参考文档:
内存快照排查OOM,加密时错误方法指定provider方式错误引起的OOM【原创】
这篇文章写得详细,非常推荐,可以说我写的基本是照抄他的,只是为了加深下自己的印象。
网友评论