记一次排查线上OOM详细过程和解决思路

文章目录

OOM起因

前言声明:本人菜鸟一枚,如有地方描述不当望谅解,毕竟本人身处某十八线小城市三十六线小公司。

国庆节前头给项目换了套参数加解密方式,换完之后服务时不时就会出现如下报错,服务停止:

HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1m11s757ms601µs134ns)

翻译后是HikariPool-1-检测到线程不足或时钟跳跃。

搜索了很多文章以为是HikariPool数据库连接池配置不合理导致的,但一番排查后并非该问题,直至后来看见一个清晰的错误,没错那就是OOM(OutOfMemoryError: Java heap space),堆内存溢出了,可惜头并未借由此机会让我去检查,而是选择了重启服务。

头质问我是不是在程序中写了死循环或者大量创建对象的情况。

怎么可能,写代码如此不规范的我是不可能在程序中如此乱搞的,而且在换加解密之前程序是如此健壮,测试做完压测后也并未出现OOM。而在换完加解密方式后,测试发很小的请求就出现了上边线程不足的情况。

后续头让修改启动参数,增大堆内存,也并未有所好转。

正值头要外出培训三天,因此给了我一个千载难逢的好机会。第一天他就让我走查一遍所有代码,不合适的地方全部都改掉。

我心里冷哼一声,就这?我心里还是很大程序怀疑加解密有问题的。

翻来覆去决定查看日志,也并未发现是哪块代码导致的。这就有些为难我月某人了,出此下策我决定:Arthas,启动!

通过Arthas提供的监控面板,我频繁的疯狂触发接口,从监控上不难看到我们的老年代不出意外的越来越大辣!(这里只复原并提供模拟,就不暴露项目内容了)。

在这里插入图片描述
通过面板可以发现频繁的触发了FULL GC,但是老年代还是居高不下,最终导致程序卡死,堆内存溢出~。

排查过程

想要解决问题,就要想办法复现,这样才能对症下药。我在复现时通过dashboard面板发现有多条线程去GC,最终因不能回收老年代内存导致不断调用线程去GC,因此才会报Thread starvation or clock leap detected介个错误。

由于在启动命令中并未指定垃圾收集器,且GC时并行调用线程,所以不出意外现在咱们的垃圾回收器应该是Parallel了。很好,也算是有了个解决思路,因此在查阅多篇资料后我决定换垃圾收集器来try一try。直接更换为了G1收集器

换完G1后,还是老问题,老年代的内存得不到释放,在查阅资料痛定思痛决定通过visualVM分析堆转储文件分析到底是哪里创建了回收不了的对象(肯定还是在加解密这块)。

通过jmap命令生成hprof文件,并拷贝到本地,使用JDK自带的VisualVM工具打开(以下仅供模拟,实际作者在生产中拿到的hprof文件中LinkedHashMap拥有千万个实例):

在这里插入图片描述

通过分析实例的垃圾回收根节点也是找到了罪魁祸首:

在这里插入图片描述

最终发现是工具类中使用了BouncyCastleProvider这个类,在每次加解密时,都创建该对象:

    /**
     * 加密
     *
     * @param content    加密的字符串
     * @param encryptKey key值
     */
    public static String encrypt(String content, String encryptKey) throws Exception {
        //设置Cipher对象
        Cipher cipher = Cipher.getInstance(ALGORITHMS, new BouncyCastleProvider());
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), KEY_ALGORITHM));

        //调用doFinal
        byte[] b = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));

        // 转base64
        return Base64.encodeBase64String(b);

    }

    /**
     * 解密
     *
     * @param encryptStr 解密的字符串
     * @param decryptKey 解密的key值
     */
    public static String decrypt(String encryptStr, String decryptKey) throws Exception {
        //base64格式的key字符串转byte
        byte[] decodeBase64 = Base64.decodeBase64(encryptStr);

        //设置Cipher对象
        Cipher cipher = Cipher.getInstance(ALGORITHMS, new BouncyCastleProvider());
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), KEY_ALGORITHM));

        //调用doFinal解密
        byte[] decryptBytes = cipher.doFinal(decodeBase64);
        return new String(decryptBytes);
    }

阅读了该类的源码后(部分):

public final class BouncyCastleProvider extends Provider
    implements ConfigurableProvider
{
    private static final Map keyInfoConverters = new HashMap();

    public void addKeyInfoConverter(ASN1ObjectIdentifier oid, AsymmetricKeyInfoConverter keyInfoConverter)
    {
        synchronized (keyInfoConverters)
        {
            keyInfoConverters.put(oid, keyInfoConverter);
        }
    }

    private void loadPQCKeys()
    {
        addKeyInfoConverter(PQCObjectIdentifiers.sphincs256, new Sphincs256KeyFactorySpi());
        addKeyInfoConverter(PQCObjectIdentifiers.newHope, new NHKeyFactorySpi());
        addKeyInfoConverter(PQCObjectIdentifiers.xmss, new XMSSKeyFactorySpi());
        addKeyInfoConverter(IsaraObjectIdentifiers.id_alg_xmss, new XMSSKeyFactorySpi());
        addKeyInfoConverter(PQCObjectIdentifiers.xmss_mt, new XMSSMTKeyFactorySpi());
        addKeyInfoConverter(IsaraObjectIdentifiers.id_alg_xmssmt, new XMSSMTKeyFactorySpi());
        addKeyInfoConverter(PQCObjectIdentifiers.mcEliece, new McElieceKeyFactorySpi());
        addKeyInfoConverter(PQCObjectIdentifiers.mcElieceCca2, new McElieceCCA2KeyFactorySpi());
        addKeyInfoConverter(PQCObjectIdentifiers.rainbow, new RainbowKeyFactorySpi());
        addKeyInfoConverter(PQCObjectIdentifiers.qTESLA_p_I, new QTESLAKeyFactorySpi());
        addKeyInfoConverter(PQCObjectIdentifiers.qTESLA_p_III, new QTESLAKeyFactorySpi());
        addKeyInfoConverter(PKCSObjectIdentifiers.id_alg_hss_lms_hashsig, new LMSKeyFactorySpi());
    }
    
    private static AsymmetricKeyInfoConverter getAsymmetricKeyInfoConverter(ASN1ObjectIdentifier algorithm)
    {
        synchronized (keyInfoConverters)
        {
            return (AsymmetricKeyInfoConverter)keyInfoConverters.get(algorithm);
        }
    }
}

所以只需在外部创建对象时保持单例或者说是保持只有一个实例即可,我在这里直接在外部创建了一个静态变量,用于解决这个问题。解决完再未出现老年代不释放内存的问题,如此,便大功告成了。

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当遇到内存不足(OOM)的问题时,可以采取以下步骤进行排查解决: 1. 确认OOM错误:查看系统日志或应用程序日志,确认是否发生了OOM错误。通常,OOM错误会在日志中显示为"Out of memory"或"java.lang.OutOfMemoryError"等。 2. 分析内存使用情况:使用监控工具(如top、htop)或分析工具(如jstat、jmap)来观察系统或应用程序的内存使用情况。检查是否存在内存泄漏或者内存使用过高的情况。 3. 调整JVM参数:如果是Java应用程序发生OOM,可以尝试调整JVM参数来增加可用内存。常见的参数包括-Xmx(最大堆内存大小)和-Xms(初始堆内存大小),可以根据应用程序的需求进行调整。 4. 优化代码:检查应用程序的代码,确保没有存在内存泄漏或者不合理的内存使用。可以通过使用合适的数据结构、及时释放资源、避免大对象等方式来优化代码。 5. 增加服务器资源:如果以上方法无法解决OOM问题,可以考虑增加服务器的物理内存或者升级到更高配置的服务器。 6. 使用分布式系统:如果单台服务器无法满足应用程序的内存需求,可以考虑使用分布式系统,将应用程序分散到多台服务器上,从而充分利用集群的内存资源。 7. 调整应用程序逻辑:如果应用程序需要处理大量数据或者复杂计算,可以考虑优化算法或者分批处理数据,以减少内存的使用。 在解决OOM问题时,需要根据具体情况进行分析和调整。如果问题比较复杂,可以借助性能分析工具或者咨询专业的开发人员来进行排查解决
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值