性能文章>BitArray虽好,但请不要滥用,一次线上内存暴增排查>

BitArray虽好,但请不要滥用,一次线上内存暴增排查原创

6994010

背景

讲故事

前天写了一篇大内存排查在园子里挺火,这是做自媒体最开心的事拉,干脆再来一篇满足大家胃口,上个月我写了一篇博客提到过使用bitmap对原来的List<CustomerID>进行高强度压缩,将原来的List内存压缩了将近106倍,但是bitmap不是一味的好,你必须在正确的场景中使用,而不是闭着眼睛滥用,bitmap在C#中对应的集合是BitArray。

好像剧透了😄😄😄,结果就是BitArray的滥用导致内存小10G的涨跌,不过这种东西重点还是看解决思路,写给以后的自己,可不能让这难得的实践经验蒸发啦~~~

解决思路

一看托管堆

看托管堆虽然是一个好主意,但也不是每次都凑效,毕竟造成内存暴涨暴跌的原因各种各样,就像人感冒有风寒,风热和病毒性,对吧😁,还是使用老命令: !dumpheap -stat -min 102400 ,在托管堆上找大于100M的对象。

0:030> !dumpheap -stat -min 102400
Statistics:
              MT    Count    TotalSize Class Name
00007ffe094ec988        1      1438413 System.Byte[]
00007ffdab934c48        1      1810368 System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.Collections.Generic.HashSet`1[[System.Int64, mscorlib]], System.Core]][]
00007ffe094e6948        1      2527996 System.String
00007ffdab9ace78        4     29499552 System.Collections.Generic.Dictionary`2+Entry[[System.Int64, mscorlib],[System.DateTime, mscorlib]][]
00007ffe094e4078        4    267342240 System.String[]
00007ffe094e9220      135    452683336 System.Int32[]
00007ffdab8cd620      123   1207931808 System.Collections.Generic.HashSet`1+Slot[[System.Int64, mscorlib]][]
00007ffe094c8510      185   1579292760 System.Int64[]
00007ffdab9516b0      154   1934622720 System.Linq.Set`1+Slot[[System.Int64, mscorlib]][]
000001cc882de970      347   3660623866      Free
Total 1371 objects

去掉一些敏感类后,再观察好像没有特别显眼的集合,像 System.Int64[] ,System.Linq.Set1+Slot[[System.Int64, mscorlib]][] 一般都是用作其他集合的内存存储,很多时候用!gcroort 抓不出来,最大的反而是Free列,有347个碎片,高达 3.5G,说明此时的大对象堆是一塌糊涂啊,要是GC能帮忙压缩一下该多好😄。

查看每一个线程的调用栈

先惯性的偷窥一下程序中有多少个线程。

0:000> !threads
ThreadCount:      74
UnstartedThread:  0
BackgroundThread: 72
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                                                        Lock  
       ID OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
   0    1 2958 000001cc882e5a40    2a020 Preemptive  0000000000000000:0000000000000000 000001cc882d8db0 1     MTA 
   2    2 2358 000001cc883122c0    2b220 Preemptive  000001D41B132930:000001D41B1348A0 000001cc882d8db0 0     MTA (Finalizer) 
   3    4 2204 000001cc883ae5d0  102a220 Preemptive  0000000000000000:0000000000000000 000001cc882d8db0 0     MTA (Threadpool Worker) 
   5    7 278c 000001cca29d8ef0  202b220 Preemptive  000001D41AB53A98:000001D41AB55A58 000001cc882d8db0 1     MTA 
   6   40 2a64 000001cca3048f10  1020220 Preemptive  0000000000000000:0000000000000000 000001cc882d8db0 0     Ukn (Threadpool Worker) 
   7   46  e34 000001cca311c390  202b220 Preemptive  0000000000000000:0000000000000000 000001cc882d8db0 0     MTA 
   8   47 27d8 000001cca3115e00    2b220 Preemptive  0000000000000000:0000000000000000 000001cc882d8db0 0     MTA 

...

可以看到当前有74个线程,后台线程有72个,接下来用 ~*e !clrstack 查看每个托管线程都在做什么,由于内容太多,我就节选一下了哈。

0:000> ~*e !clrstack
OS Thread Id: 0x2d64 (29)
        Child SP               IP Call Site
000000d908cfe698 00007ffe28646bf4 [GCFrame: 000000d908cfe698] 
000000d908cfe768 00007ffe28646bf4 [HelperMethodFrame_1OBJ: 000000d908cfe768] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)

OS Thread Id: 0x214c (30)
        Child SP               IP Call Site
000000d90957e6e8 00007ffe28646bf4 [GCFrame: 000000d90957e6e8] 
000000d90957e7b8 00007ffe28646bf4 [HelperMethodFrame_1OBJ: 000000d90957e7b8] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)

OS Thread Id: 0x1dc0 (40)
        Child SP               IP Call Site
000000d950ebe878 00007ffe28646bf4 [GCFrame: 000000d950ebe878] 
000000d950ebe948 00007ffe28646bf4 [HelperMethodFrame_1OBJ: 000000d950ebe948] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)

OS Thread Id: 0x274c (53)
        Child SP               IP Call Site
000000d9693fe518 00007ffe28646bf4 [GCFrame: 000000d9693fe518] 
000000d9693fe5e8 00007ffe28646bf4 [HelperMethodFrame_1OBJ: 000000d9693fe5e8] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)
000000d9693fe700 00007ffe09314d05 System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken)
000000d9693fe790 00007ffe0930d996 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken)
000000d9693fe800 00007ffe09c9b7a1 System.Threading.Tasks.Task.InternalWait(Int32, System.Threading.CancellationToken)

image.png

发现一个奇怪的现象,有4个线程 29,30,40,53 在 Monitor.ObjWait 处卡住了,从调用栈来看这四个家伙正在准备向Mongodb批量插入数据[InsertBatch],此时应该有其他的一个线程先行获取到了lock正在做InsertBatch,这四个线程在等待,如何觉得抽象了,我画一张图:

image.png

寻找insertbatch处的集合

这里我就拿 30号线程说事,从上图调用栈中你应该看到一个System.Collections.Generic.IEnumerable1<System.__Canon>,从IEnumerable中可以猜测实现类应该是List或者HashSet这样的集合,接下来用 !dso 把30号线程栈上的对象全部dump出来。

image.png

从图中看应该就是这个 List<xxx.Common.GroupConditionCustomerIDCacheModel>,然后用!objsize ➕!do 给List量个尺寸并且dump一下。

0:030> !objsize 000001d3fa581518 
sizeof(000001d3fa581518) = 1487587080 (0x58aac708) bytes (System.Collections.Generic.List`1[[DataMipCRM.Common.GroupConditionCustomerIDCacheModel, DataMipCRM.Common]])
0:030> !do 000001d3fa581518
Name:        System.Collections.Generic.List`1[[DataMipCRM.Common.GroupConditionCustomerIDCacheModel, DataMipCRM.Common]]
MethodTable: 00007ffdab9557d0
EEClass:     00007ffe08eb22a0
Size:        40(0x28) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffe09478740  4001871        8     System.__Canon[]  0 instance 000001d3fa5b9bf8 _items
00007ffe094e9288  4001872       18         System.Int32  1 instance             1520 _size
00007ffe094e9288  4001873       1c         System.Int32  1 instance             1520 _version
00007ffe094e6f28  4001874       10        System.Object  0 instance 0000000000000000 _syncRoot
00007ffe09478740  4001875        8     System.__Canon[]  0   static  <no information>

可以看出list占用 1487587080/1024/1024=1.4G,尼玛这么大,吓人哈,从_size看也就1520个,说明重点都在 _items 数组里啦,接下里用 da 把第一个item拿出来解剖下。

0:030> !da -length 1 -details 000001d3fa5b9bf8
Name:        DataMipCRM.Common.GroupConditionCustomerIDCacheModel[]
MethodTable: 00007ffdab955e10
EEClass:     00007ffe08eaaa00
Size:        16408(0x4018) bytes
Array:       Rank 1, Number of elements 2048, Type CLASS
Element Methodtable: 00007ffdab955740
[0] 000001d3fa581540
    Name:        DataMipCRM.Common.GroupConditionCustomerIDCacheModel
    MethodTable: 00007ffdab955740
    EEClass:     00007ffdab94b9e8
    Size:        64(0x40) bytes
    File:        D:\LuneceService\DataMipCRM.Common.dll
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffdaac69258  4000589       28     ...oDB.Bson.ObjectId      1     instance     000001d3fa581568     <_id>k__BackingField
        00007ffe094e9288  400058a       20             System.Int32      1     instance                 1901     <ShopId>k__BackingField
        00007ffe094e6948  400058b        8            System.String      0     instance     000001d3f7154070     <GroupConditionHasCode>k__BackingField
        00007ffe094e6948  400058c       10            System.String      0     instance     000001cca7b46ac0     <unit>k__BackingField
        00007ffe094f1cb0  400058d       18     ...lections.BitArray      0     instance     000001d3fa581580     <customeridArray>k__BackingField

从最后一行的Type列可以看到有一个 BitArray 类,还是一样,先量个尺寸再打出来。

0:030> !objsize 000001d3fa581580     
sizeof(000001d3fa581580) = 956008 (0xe9668) bytes (System.Collections.BitArray)
0:030> !do 000001d3fa581580     
Name:        System.Collections.BitArray
MethodTable: 00007ffe094f1cb0
EEClass:     00007ffe08ead968
Size:        40(0x28) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffe094e9220  40017e2        8       System.Int32[]  0 instance 000001d5320c6d18 m_array
00007ffe094e9288  40017e3       18         System.Int32  1 instance          7647524 m_length
00007ffe094e9288  40017e4       1c         System.Int32  1 instance                2 _version
00007ffe094e6f28  40017e5       10        System.Object  0 instance 0000000000000000 _syncRoot

从output中看,这个bitarray占用 956008/1024/1024 = 0.91M,真tmd的大,看bit位有 764w 个,说明有一个CustomerID=7647524-1放在这里面,问题基本上就算找到了,现在终于知道内存为什么这么大,算一下就出来了。

image.png

总结

最后去问了下搬砖的为什么这么写,是因为给客户展示的一张报表,为了加速。。。将每一个点上的人群保存起来放在BitArray上然后进入Monogdb缓存,这样客户后续选择某一个点进行 下钻 操作的话,可以直接从mongodb中将BitArray的人群复原出来,免去程序重复计算之苦,因为每个点的人群具有排他性,落在每个点上的人可能只有几十,几百,几千,而偏偏这家客户有800w之多,自然这个CustomerID就特别大了,更不巧的就用了bitArray存少量的大数字,这些赶在一起之后,就是一个典型的滥用bitarray,您说的?

本文作者:一线码农聊技术

请先登录,再评论

暂无回复,快来写下第一个回复吧~

为你推荐

关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
谨防JDK8重复类定义造成的内存泄漏
概述 如今JDK8成了主流,大家都紧锣密鼓地进行着升级,享受着JDK8带来的各种便利,然而有时候升级并没有那么顺利?比如说今天要说的这个问题。我们都知道JDK8在内存模型上最大的改变是,放弃了Perm
JVM菜鸟进阶高手之路九(解惑)
关于MAT工具相关知识解惑MAT 不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如 Sun, HP, SAP 所采用的 HPROF 二进制堆存储文件,以及 IBM 的
JVM垃圾回收与一次线上内存泄露问题分析和解决过程
本文转载自:花椒技术微信公众号 前言内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。Ja
强如 Disruptor 也发生内存溢出?
前言```OutOfMemoryError ```问题相信很多朋友都遇到过,相对于常见的业务异常(数组越界、空指针等)来说这类问题是很难定位和解决的。本文以最近碰到的一次线上内存溢出的定位、解决问题的
spring boot 引起的 “堆外内存泄漏”
背景组内一个项目最近一直报swap区域使用过高异常,笔者被叫去帮忙查看原因。发现配置的4G堆内内存,但是实际使用的物理内存高达7G,确实有点不正常,JVM参数配置是:```java-XX:Metasp
实战:一次疑似内存泄漏的问题排查
问题背景最近服务器到期等因素,进行了迁移。租了其它的外国厂商,但是由于资费问题,购买了1.5G 内存的服务器(现)。因为原本用惯了4G内存的服务器(原),现在压缩成这样,似乎不太能支持我的使用,囧!现
记录一次Flink作业异常的排查过程
最近2周开始接手apache flink全链路监控数据的作业,包括指标统计,业务规则匹配等逻辑,计算结果实时写入elasticsearch. 昨天遇到生产环境有作业无法正常重启的问题,我负责对这个问题