4.生产内存溢出问题

一、内存溢出场景

线上某次促销活动,监控到内存持续增长,接口响应时间越来越长,到最后无响应,内存溢出OOM

1.紧急解决方案

扩容,重启机器增加堆内存分配

2.原因分析

由于jvm参数中有配置jvm参数

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/aliyun/logs/jvm/
该参数配置代表发生OOM时,生成一份堆快照文件

从服务器中获取到堆快照文件java_pid3088.hprof
使用jvisualvm打开该快照,如图:

oom时产生的堆快照文件

如图所示,除一些基本数据类型外,内存溢出时快照中存在较多的java.security.Provider ServiceKey对象与java.security.Provider Service对象。
Provider主要用于加密解密时用到,项目中原本相关的代码就很少,所以这里已经对部分问题接口有了猜测。然后观测内存增长比较高的时间段,查询日志该时间段接口访问比较频繁的接口,发现有一个单点登录接口比较符合上边的情况,对该接口重点压测分析,如没问题对其余相关接口压测分析。

注:相关的截图为后续复现截图

3.jemter压测问题接口

线下复现问题,对单点登录接口进行压测,发现该接口请求响应时间越来越长,直到最后完全没有响应,发生OOM,与线上情况基本吻合。

(1).jemter压测响应情况
(第一张刚刚压测截图为后续重新压测补充的,所以时间不一致,但是效果一致)
压测整体响应情况如下:通过刚刚压测阶段->压测一小段时间->压测一段时间->压测最后阶段,可以发现响应时间越来越长,到最后完全无响应,对应报错OOM。此时基本可以确认问题出在该接口。

刚刚开始压测
压测了一小段时间后
压测一段时间后
压测最后阶段

2.jvisualvm观测内存与垃圾回收情况
压测时打开jdk自带jvisualvm工具,查看其内存与垃圾回收情况。
整体监视情况如下

整体监视情况

垃圾回收情况如下:刚刚开始压测一小段时间:老年代已经在持续增长了,eden已经出现了多次minorGC。

刚刚开始压测一小段时间

压测一段时间后,eden执行了非常多次minorGC,而老年代一直在持续增长,已经到达了最大内存。

压测一段时间后

压测较长后,eden执行了非常多次,而老年代在到达了最大内存后也执行了一次fullGC。

压测较长时间后

压测很久很久后,可以老年代在一次fullGC后,并没有缓解情况,后续还在持续增长,探后再次打满内存,导致OOM。

压测最后阶段

3.jvisualvm观测堆快照--定位内存泄漏的是哪个对象

运行一小段时间的抽样器堆快照
运行了一段时间后的抽样器堆快照
运行了一段时间后的分析器堆快照

可以发现java.security.Provider ServiceKey对象与java.security.Provider Service对象在经历了无数次gc仍然存活,截图时为75次,占用了大量堆空间,其依赖的基本数据类型同样占用大量堆空间,所以这里再次断定Provider对象出现了问题。

4.jvisualvm观测线程快照--定位内存溢出的是哪个线程方法

运行了很久的抽样器线程快照
运行了一段时间的分析器线程快照

通过线程快照可以定位到具体类与方法,查看该方法为:

Cipher.getInstance("RSA", new BouncyCastleProvider());

由此得出结论,单点登录接口,做加密操作时,创建了大量的BouncyCastleProvider对象,且该对象一直不能被回收,导致内存溢出。
下面对代码详细分析

4.代码分析
简化demo,通过上述定位到了接口/oom/test,方法定位到了buildParam的Cipher.getInstance方法,对象则定位到了Provider,下面分析下demo。

@RestController
public class OOMController {

   @RequestMapping("/oom/test")
    public String testOOM() {
        JSONObject data = new JSONObject();
        data.put("AppId", "APPID");
        data.put("Secret", "HASHKEY");
        data.put("TransCode", "TRANSCODETOKEN");
        String rsaContent = buildParam(data.toString());
        return rsaContent;
    }


    /**
     * 这是个实际遇到的问题,关于api调用加密解密的参数拼接导致内存泄漏
     * 简化了业务,只保留了问题关注点代码,也就是new BouncyCastleProvider()
     *
     * @param data
     * @return
     */
    private String buildParam(String data) {

        try {
            // 怀疑这里new BouncyCastleProvider可能会有问题
            Cipher cipher = Cipher.getInstance("RSA", new BouncyCastleProvider());
        } catch (Exception e) {
            e.printStackTrace();
        }

        JSONObject value = new JSONObject();
        value.put("Data", data);
        value.put("Access_Token", "token");
        JSONObject content = new JSONObject();
        content.put("content", value);
        return content.toString();
    }
}

对Cipher.getInstance("RSA", new BouncyCastleProvider());进行深层次的分析,创建Cipher时,会验证Provider是否合法,验证Provider合法过程中,会将Provider放到一个全局map-verificationResults中,所以一直存在引用,即Provider和全局verificationResults不会被回收,到这里问题基本就定位到了,下边有一些debug时verificationResults的一些截图。

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) { 
                            // 验证Provider是否支持该算法 
                            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 final Map verificationResults = new IdentityHashMap();
    
    /**
     * 每次将Provider放到全局变量verificationResults map中,所以会一直被引用,不会被垃圾回收
     */
    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);
                // 这是一个全局变量,并且每次调用该方法都会新增,所以会一直在增加,
                // 且属于不能被回收的对象
                ![image.png](https://upload-images.jianshu.io/upload_images/15829707-77445ee4e469d6b6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
.put(var0, PROVIDER_VERIFIED);
                var3 = null;
                return var3;
            } catch (Exception var7) {
                verificationResults.put(var0, var7);
                var3 = var7;
            } finally {
                verifyingProviders.remove(var0);
            }

            return var3;
        }
    }

通过对verificationResults截图可以发现该map中BouncyCastleProvide作为key存在,且BouncyCastleProvide对象本身属于键值对且占用内存比较多

压测截图-verificationResults对象信息

5.最终解决
问题发现了,解决方案则比较简单,这里其实不需要每次都创建一个BouncyCastleProvide对象,只需要创建一个全局BouncyCastleProvide对象就可以。
解决后相关内存与垃圾回收截图献上。

运行一段时间的整体监控情况
运行一段时间的垃圾回收情况
运行一段时间的堆快照
运行一段时间的线程快照

注:文中相关截图都是后续复现中所截,且并非同一次压测截图完成。

库存导出频繁fullgc与内存泄露问题
1.poi替换easyexcel,一次捞所有数据替换为分页查 解决内存泄露
2.metaspace达到阈值导致fullgc

其他:
文档推荐,g1fullgc排查思路,gc日志解读
https://blog.csdn.net/liaomingwu/article/details/124813934
https://segmentfault.com/a/1190000021875196
https://zhuanlan.zhihu.com/p/267388951
https://www.jianshu.com/p/ac1ba3479c08

你可能感兴趣的:(4.生产内存溢出问题)