美文网首页
加密工具类导致内存溢出分析总结

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

作者: 牛逼的二进制 | 来源:发表于2017-03-02 07:13 被阅读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【原创】
    这篇文章写得详细,非常推荐,可以说我写的基本是照抄他的,只是为了加深下自己的印象。

    相关文章

      网友评论

          本文标题:加密工具类导致内存溢出分析总结

          本文链接:https://www.haomeiwen.com/subject/whfhgttx.html