Loading [MathJax]/jax/output/CommonHTML/jax.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一次艰难的内存泄露排查,BeanUtils的锅

一次艰难的内存泄露排查,BeanUtils的锅

作者头像
烂猪皮
发布于 2020-11-02 06:32:23
发布于 2020-11-02 06:32:23
1.3K00
代码可运行
举报
文章被收录于专栏:JAVA烂猪皮JAVA烂猪皮
运行总次数:0
代码可运行

现象

通过jstat -gcutil pid 5000 ,发现fgc次数很多而且频繁,此时老年代占比已经大约70%左右,且已经回收不了内存,我们这边设置的fgc阈值是老年代的70%。此时因为还有30%的老年空间,所以整体内存相对还算稳定,CPU也比较稳定,但是有很大的潜在的风险,就是内存一直上涨,不释放。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[service@ZQ-SE-331-V05 ~]$ jstat -gcutil 1087 5000
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00  55.09  88.41  72.10  92.64  85.22   9223 1169.442   435  168.866 1338.307
 57.54   0.00  82.24  72.31  92.64  85.22   9224 1169.542   436  168.877 1338.418
  0.00  63.75   5.33  72.50  92.64  85.22   9225 1169.642   436  168.877 1338.519
  0.00  63.75  34.02  72.50  92.64  85.22   9225 1169.642   436  168.877 1338.519
  0.00  63.75  59.26  72.50  92.64  85.22   9225 1169.642   436  168.877 1338.519
  0.00  63.75  81.37  72.50  92.64  85.22   9225 1169.642   436  168.877 1338.519
 55.60   0.00  11.75  72.71  92.64  85.22   9226 1169.742   436  168.877 1338.619
 55.60   0.00  40.07  72.71  92.64  85.22   9226 1169.742   436  168.877 1338.619
 55.60   0.00  67.86  72.70  92.64  85.22   9226 1169.742   437  169.541 1339.284
  0.00  56.04   4.21  72.59  92.64  85.22   9227 1169.838   437  169.541 1339.379
  0.00  56.04  30.01  71.73  92.64  85.22   9227 1169.838   438  169.649 1339.487
  0.00  56.04  57.75  71.73  92.64  85.22   9227 1169.838   438  169.649 1339.487
  0.00  56.04  79.01  71.73  92.64  85.22   9227 1169.838   438  169.649 1339.487
 55.39   0.00   2.54  71.92  92.64  85.22   9228 1169.988   438  169.649 1339.638
 55.39   0.00  24.70  71.92  92.64  85.22   9228 1169.988   438  169.649 1339.638
 55.39   0.00  47.89  71.92  92.64  85.22   9228 1169.988   438  169.649 1339.638
 55.39   0.00  82.01  71.89  92.64  85.22   9228 1169.988   439  170.207 1340.196

初步猜测是出现了内存泄露,通过jmap -histo/-histo:live pid >> log导出fgc前后的histo对比,发现了一个实例数很大的对象CarnivalOneDayInfo,达到了2kw级别,而且一直在增加

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
num     #instances         #bytes  class name
----------------------------------------------
   1:      28906840     1387528320  java.util.HashMap
   2:      38675870     1237627840  java.util.HashMap$Node
   3:      18631826      745273040  xxx.CarnivalOneDayInfo

    num     #instances         #bytes  class name
----------------------------------------------
   1:      31092889     1492458672  java.util.HashMap
   2:      35749328     1143978496  java.util.HashMap$Node
   3:      20355334      814213360  xxx.CarnivalOneDayInfo

排查

直接看CarnivalOneDayInfo(嘉年华)相关代码,因为之前的测试大概知道这个问题,所以很快的定位到是在每分钟的MinuteJob中检查所有在线玩家的时间活动的时候,相关逻辑会克隆一个CarnivalOneDayInfo。所以初步定位是CarnivalOneDayInfo不断clone的问题

后端y同学看了一下相关逻辑,本地把相关clone逻辑注释后,再次本地观察histo,发现该对象实例不在增加,再次确认1的推论,但看了一下活动相关代码,所有活动的检查都是clone了一份,为什么只有嘉年华活动泄露?看相关代码,发现clone出来的对象都是临时对象,应该会被fgc的,如何泄露呢?

所以直接开始操作第一步,准备查CarnivalOneDayInfo是被谁持有引用,查这个问题的话,必须要把内存堆快照dump出来,然后利用工具检查,如mat。但是线上玩家很多,而且堆内存很多,导出一次很花费时间,会stw,所以直接连开发服务器,查了一下,一样存在CarnivalOneDayInfo泄露的问题,所以直接利用jmap -dump:live,format=b,file=2388_heap.bin 2388,导出开发服务器java进程的堆内存快照。

直接利用mat打开,对mat使用有经验的话,操作步骤是

  1. 选择dominator_tree
  2. 搜索CarnivalOneDayInfo
  3. List Objects
  4. 选择With incoming references,谁持有了它的引用

直接发现是被

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
queue-executor-handler-5

java.lang.ThreadLocal$ThreadLocalMap @ 0x8104eec0

java.lang.ThreadLocal$ThreadLocalMap$Entry[64] @ 0x866710f0

java.lang.ThreadLocal$ThreadLocalMap$Entry @ 0x86671608

java.util.IdentityHashMap @ 0x86671628

java.lang.Object[],通过查看这个属性,可以看到其大量持有了CarnivalOneDayInfo和HashSet从上面可以很确定的是,CarnivalOneDayInfo是直接被逻辑线程的threadlocal持有。

艰难的分析之路

逻辑线程的threadlocal怎么会持有CarnivalOneDayInfo,从代码和想法上感觉不可思议,我这边的第一步是直接在ide中查所有ThreadLocal的引用,发现

logback protobuf 业务自己的threadlocal BeanUtils的threadlocal

排查后,简单测试了一下,和查了一下相关代码,发现没有什么思路和投诉。觉得beanutils和logback有点问题,但是感觉和CarnivalOneDayInfo关系也不大,于是下面我的重点在于想从mat入手,看看能不能从中找出threadlocal的名字,但最终是被证明是徒劳的,只有引用地址,不过我们z同学从是debug的排查思路出发的在ThreadLocal的set和setInitialValue打断点,然后跑游戏,重点断点在MinuteJob,向逻辑线程投递消息检查活动状态这块每次调用checkTimeActivity,都去观察每个ThreadLocalEntry然后重点观察是否出现IdentityHashMap,因为上面mat分析到了是这个map终于定位到堆栈。

堆栈

MinuJob -> 遍历在线所有玩家,向逻辑线程投递消息 -> ActivityManager#checkTimeActivity 遍历所有个人活动 -> CarnivalActivityInfo#checkActivityState CarnivalActivityInfo diff = playerInfo.clone() // 在这里clone了一份 checkIsSameState -> activityBaseInfo.getCarnivalDaysMap().equals(carnivalDaysMap) // 调用equals比较 去依次比较CarnivalDaysMap中每一个CarnivalOneDayInfo 调用CarnivalOneDayInfo -> BaseCarnivalOneDayInfo # equals BeanUtils.isDirty

具体代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private static Set<String> getDirtyNamesByBean(AugmentedBean augmentedBean) {
        IdentityHashMap<AugmentedBean, Set<String>> dirtyNamesMap = dirtyNames.get();
        if (dirtyNamesMap == null) {
            dirtyNamesMap = new IdentityHashMap<>();
            dirtyNames.set(dirtyNamesMap);
        }
        return dirtyNamesMap.computeIfAbsent(augmentedBean, k -> Sets.newHashSet());
    }

    public static boolean isDirty(AugmentedBean augmentedBean) {
        return getDirtyNamesByBean(augmentedBean).size() > 0;
    }

从上面可以非常容易的看到是这里创建了一个IdentityHashMap,然后set到了threadlocal中和之前的分析如出一辙至此完全定位问题是,是BeanUtils的锅。

复盘和总结

其实非常怀疑过beanutils的问题,但是没有太仔细注意,现在一看,和上面的分析一模一样,分析内存泄露,思路:

  • 对比fgc前后的histo,确认那些对象实例数一直在增加,而且明显偏大
  • 分析代码,如果直接定位问题,最好
  • 如果不能直接定位,需要确认是谁持有该对象引用,那么需要dump堆内存快照
  • 但是不能在线上dump,需要在开发服务器复现(内存泄露通常比较容易复现),然后在开发服务器dump
  • dump出来后,利用mat工具分析泄露,List Objects With incoming references,找到引用
  • 下一步是需要确认为什么这里会引用,可以分析代码,解决问题,最好
  • 如果不能,那么可以使用debug的方式,在上一步引用相关对象的代码出加断点,确认线程堆栈

也解释了为什么只有嘉年华活动有泄露,因为只有它调用了beanutils生成的basexx的equals方法,其他都没有调用,beanutils是当初clone对象的一个解决方案,用来回滚和diff,增量更新,后来该方案废弃,因为会随着对象的复杂度提高而导致clone成本高,但是遗留了一大部分生成的代码,而这次的bug也是因为调用了废弃的生成代码的方法。所以下一个版本一定将所有生成的废弃代码清理一遍 后续解决办法。

发现了beanutils的这个问题后,那么很容易解决泄露问题了。我写了一个beanshell脚本,向逻辑线程投递了消息,调用 BeanUtils.clean,清理所有threadlocals

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import x.BeanUtils;
import y.HandlerModule;

for (int i = 1; i <= 16; i++) {
    HandlerModule.instance.addQueueTask(i, new Runnable() {
        public void run() {
            BeanUtils.clean();
        }
    });
}

在用jstat看了一下,CarnivalOneDayInfo经过fgc后,从kw到了百万,老年代占用也从70到了30

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ jstat -gcutil 1087 5000

74.73   0.00  16.02  72.48  92.61  85.04  10156 1313.117   575  271.355 1584.472
 74.73   0.00  34.71  72.48  92.61  85.04  10156 1313.117   575  271.355 1584.472
 74.73   0.00  54.42  72.48  92.61  85.04  10156 1313.117   575  271.355 1584.472
 74.73   0.00  73.29  72.48  92.61  85.04  10156 1313.117   575  271.355 1584.472
 74.73   0.00  89.41  72.48  92.61  85.04  10156 1313.117   575  271.355 1584.472
  0.00  71.54   9.25  72.74  92.64  85.06  10157 1313.303   576  272.188 1585.492
  0.00  71.54  28.30  72.73  92.64  85.06  10157 1313.303   577  272.188 1585.492
  0.00  71.54  55.85  72.73  92.64  85.06  10157 1313.303   577  272.463 1585.766
  0.00  71.54  78.05  72.73  92.64  85.06  10157 1313.303   577  272.463 1585.766
 69.21   0.00   1.70  70.98  92.64  85.06  10158 1313.438   578  273.320 1586.758
 69.21   0.00  19.97  63.09  92.64  85.06  10158 1313.438   578  273.320 1586.758
 69.21   0.00  39.82  53.33  92.64  85.06  10158 1313.438   578  273.320 1586.758
 69.21   0.00  59.75  41.61  92.64  85.06  10158 1313.438   578  273.320 1586.758
 69.21   0.00  75.12  31.79  92.64  85.06  10158 1313.438   578  273.320 1586.758
 69.21   0.00  94.13  31.79  92.64  85.06  10158 1313.438   578  273.320 1586.758
  0.00  86.02  15.60  32.07  92.64  85.06  10159 1313.761   578  273.320 1587.081
  0.00  86.02  94.86  32.07  92.64  85.06  10159 1313.761   578  273.320 1587.081

  [service@ZQ-SE-331-V05 config]$  jmap -histo 1087 | grep CarnivalOneDayInfo
  10:       1408627       56345080  xxx.CarnivalOneDayInfo

另外后面的优化方案是重构活动代码,另外即使用equals,也不用之前beanutils生成的类的equals比较,避免beanutils threadlocals的泄露问题。

关于内存问题,在正式上线之前,一定要可根据同时在线人数,dau等准确预估整体占用内存,如一个player的实际的占用内存,全局静态数据如排行榜的实际占用内存等。可以通过代码和工具获取。这样能快速确认是否是出现了内存泄露还是真的比较占内存。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-10-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 JAVA烂猪皮 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
阿里二面:ThreadLocal内存泄露灵魂四问,人麻了!
ThreadLocal能够在线程本地存储对应的变量,从而有效的避免线程安全问题。但是使用ThreadLocal时,稍微不注意就有可能造成内存泄露的问题。那么ThreadLocal在哪些场景下会出现内存泄露?哪些场景下不会出现内存泄露?出现内存泄露的根本原因又是什么呢?如何真正避免内存泄露?
冰河
2023/11/06
10.2K1
阿里二面:ThreadLocal内存泄露灵魂四问,人麻了!
一次恐怖的 Java 内存泄漏排查实战
最近在看《深入理解Java虚拟机:JVM高级特性与最佳实践》(第二版)这本书,理论+实践结合,深入浅出,强烈推荐给大家。 这两天对JVM内容进行了一个讨论,讨论的内容主要包括如下几个方面。 1)内存溢出和内存泄露的介绍? 2)如何排查和处理内存泄露? 一、内存溢出和内存泄露 一种通俗的说法。 1、内存溢出:你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数据,出现溢出。 2、内存泄漏:你用new申请了一块内存,后来很长时间都不再使用了(按理应该释放),但是因为一直被某个或某些实例所持
精讲java
2018/07/05
2.5K3
关于MySQL内存泄露如何排查的一些思路
MySQL使用内存上升90%!在运维过程中50%的几率,会碰到这样的问题。算是比较普遍的现象。
数据和云
2021/08/27
2.9K0
关于MySQL内存泄露如何排查的一些思路
记一次调试python内存泄露的问题
这两天由于公司需要, 自己编写了一个用于接收dicom文件(医学图像文件)的server. 经过各种coding-debuging-coding-debuging之后, 终于上线了, 上线后心里美滋滋
小小科
2018/05/04
3.5K0
记一次调试python内存泄露的问题
BlackHole开发日记-一次压力测试及JVM调优的经过
BlackHole开发很久了,目前稳定性、性能都还可以了,但是作为一个Java程序,内存开销一直是硬伤,动不动100M内存下去了,对于单机用户实在是不太友好。
九州暮云
2019/08/21
6940
关于ThreadLocal内存泄露的备忘
还记得第一次接触到ThreadLocal可能导致内存泄露的问题是有一次面试的时候被问到了ThreadLocal的缺陷是什么。当然由于后来没有面试官的联系方式很遗憾也一直没能确认所谓的缺陷是不是就是可能导致内存泄漏,不过后来发现虽然当时弄明白了可是过段时间又搞忘记了这个问题,所以特别记录下来做个备忘吧。
哲洛不闹
2018/10/25
6330
个人谈谈对ThreadLocal内存泄露的理解
平时我们会使用ThreadLocal来存放当前线程的副本数据,让当前线程执行流中各个位置,都可以从ThreadLocal中获取到想要的线程副本数据,而无需通过方法参数逐级传递,减少了代码的耦合。
大忽悠爱学习
2023/02/26
7300
个人谈谈对ThreadLocal内存泄露的理解
一次 Netty 堆外内存泄露问题排查
最近在做一个基于 websocket 的长连中间件,服务端使用实现了 socket.io 协议(基于websocket协议,提供长轮询降级能力) 的 netty-socketio 框架,该框架为 netty 实现,鉴于本人对 netty 比较熟,并且对比同样实现了 socket.io 协议的其他框架,这个框架的口碑要更好一些,因此选择这个框架作为底层核心。
涤生
2019/05/14
2.7K0
一次 Netty 堆外内存泄露问题排查
记一次 JAVA 的内存泄露分析
前不久,上线了一个新项目,这个项目是一个压测系统,可以简单的看做通过回放词表(http请求数据),不断地向服务发送请求,以达到压测服务的目的。在测试过程中,一切还算顺利,修复了几个小bug后,就上线了。在上线后给到第一个业务方使用时,就发现来一个严重的问题,应用大概跑了10多分钟,就收到了大量的 Full GC 的告警。
芋道源码
2020/10/27
2.7K0
记一次 JAVA 的内存泄露分析
什么是内存溢出(Out Of Memory---OOM)和内存泄露 (Memory Leak)
系统已经不能再分配出你所需要的空间,比如系统现在只有1G的空间,但是你偏偏要2个G空间,这就叫内存溢出 例子:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。
小小鱼儿小小林
2020/10/10
7.1K0
来来来,聊聊7种内存泄露场景和13种解决方案
Java通过垃圾回收机制,可以自动的管理内存,这对开发人员来说是多么美好的事啊。但垃圾回收器并不是万能的,它能够处理大部分场景下的内存清理、内存泄露以及内存优化。但它也并不是万能的。
程序新视界
2021/12/07
2.8K0
来来来,聊聊7种内存泄露场景和13种解决方案
【干货】JVM 优化、内存泄露排查、gc.log 分析方法等
本文讲解了 JVM 的内存划分和分配策略,并以截图和脚本展示常用可视化和命令行工具的使用方法,完整演示了 JVM 优化、内存泄露排查、gc.log 分析方法等。
IT技术小咖
2019/06/26
5.9K0
【干货】JVM 优化、内存泄露排查、gc.log 分析方法等
详细解读ThreadLocal的内存泄露
说到内存溢出,我相信各位都知道是什么,但是说到内存泄露,而且还是 ThreadLocal ,阿粉就得来说一下这个了,毕竟如果面试的时候被问到 ThreadLocal 的内存泄露,是不是有可能不太了解了呢,今天阿粉来说一下这个 ThreadLocal 的内存泄露的原因,以及如何从开发中去避免这个问题。
Java极客技术
2022/12/04
1.5K0
详细解读ThreadLocal的内存泄露
如何使用Eclipse内存分析工具定位内存泄露
本文以我司生产环境Java应用内存泄露为案例进行分析,讲解如何使用Eclipse的MAT分析定位问题
Bug开发工程师
2021/03/19
2.5K0
如何使用Eclipse内存分析工具定位内存泄露
解决ThreadLocal的内存泄露问题
ThreadLocal的内存泄露问题   根据上面Entry方法的源码,我们知道ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:
好派笔记
2021/10/08
1.1K0
记一次内存泄露调试
首先介绍一下相关背景。最近在测试一个程序时发现,在任务执行完成之后,从任务管理器上来看,内存并没有下降到理论值上。程序在启动完成之后会占用一定的内存,在执行任务的时候,会动态创建一些内存,用于存储任务的执行状态,比如扫描了哪些页面,在扫描过程中一些收发包的记录等等信息。这些中间信息在任务结束之后会被清理掉。任务结束之后,程序只会保存执行过的任务列表,从理论上讲,任务结束之后,程序此时所占内存应该与程序刚启动时占用内存接近,但是实际观察的结果就是任务结束之后,与刚启动之时内存占用差距在100M以上,这很明显不正常,当时我的第一反应是有内存泄露
Masimaro
2021/01/13
9236
排查Java的内存问题
核心要点 排查Java的内存问题可能会非常困难,但是正确的方法和适当的工具能够极大地简化这一过程; Java HotSpot JVM会报告各种OutOfMemoryError信息,清晰地理解这些错误信息非常重要,在我们的工具箱中有各种诊断和排查问题的工具,它们能够帮助我们诊断并找到这些问题的根本原因; 在本文中,我们会介绍各种诊断工具,在解决内存问题的时候,它们是非常有用的,包括: HeapDumpOnOutOfMemoryError和PrintClassHistogram JVM选项 Eclipse MA
用户1263954
2018/04/08
2.9K0
排查Java的内存问题
关于 Java 内存泄露的错误认知,你所应该了解的
在本篇博文中,我们将了解什么是 Java 中的内存泄漏,以及关于 Java 内存泄漏场景的错误认知进行简要解析。
Luga Lee
2023/08/11
1.4K1
关于 Java 内存泄露的错误认知,你所应该了解的
探究ThreadLocal原理(基于JAVA8源码分析)线程封闭ThreadLocal是什么ThreadLoalMaphash冲突内存泄露避免内存泄露题外小话ThreadLocal的应用场合
线程封闭 实现好的并发是一件困难的事情,所以很多时候我们都想躲避并发。避免并发最简单的方法就是线程封闭。什么是线程封闭呢? 就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。 使用ThreadLocal是实现线程封闭的最好方法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。 Thre
JavaEdge
2018/05/16
1.6K1
记一次golang内存泄露
最近在QA环境上验证功能时,发现机器特别卡,查看系统内存,发现可用(available)内存仅剩200多M,通过对进程耗用内存进行排序,发现有一个名为application-manager的容器服务的内存占用达到700多M,该服务使用Gin框架对外提供操作k8s资源的简单功能,解析客户端请求,并将结果返回给客户端。由于是测试环境,访问量极少,但内存一直只增不减,从最初的10M,一直增加到700多M,最终由于OOM而被重启(Pod)。
charlieroro
2021/06/02
1.6K0
相关推荐
阿里二面:ThreadLocal内存泄露灵魂四问,人麻了!
更多 >
交个朋友
加入[架构及运维] 腾讯云技术交流站
云架构设计 云运维最佳实践
加入架构与运维学习入门群
系统架构设计入门 运维体系构建指南
加入架构与运维工作实战群
高并发系统设计 运维自动化实践
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档