加密工具类导致内存溢出分析总结

2017-03-02  本文已影响0人  牛逼的二进制

这是第一次把问题分析的总结记录下来,一是记录下做备忘,二是把问题分析的过程和总结梳理下。

一共在两个系统碰过因为加密导致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类,那么可以初步断定问题是由这个类导致的;

图1

2)点击Actions下的Dominator Tree可以查看占用内存最大的对象,点击后如图2。可以看到有一个IdentityHashMap存储了大量的BouncyCastleProvider对象;


图2

3)点击JceSecurity-->List Objects-->with outgoing references显示如图3所示,可以看到是变量名为verificationResults的identityHashMap中存放了大量的BouncyCastleProvier,基本上就已经找到导致问题的原因了;

图3

4)查看源码,可以看到因为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【原创】
这篇文章写得详细,非常推荐,可以说我写的基本是照抄他的,只是为了加深下自己的印象。

上一篇 下一篇

猜你喜欢

热点阅读