内存碎片分析

背景

在上篇文章中
进程状态可视化方案
搭建了可视化界面,对于目前运行环境下的几百个服务器状态就非常清晰了,比如服务的虚拟内存,这时就发现一个服务存在问题:

虚拟内存、提交内存

红色线为virtual Bytes ,大概在1.4G左右
绿色线为Private Bytes , 大概在350M左右

那么应该是出现了严重的内存碎片(Fragmentation)。

往事

在众多疑难问题定位过程中,我一直对刚参加工作时分析的一个内存碎片问题印象非常深刻,因为这个问题锻炼了我坚韧的耐力和大幅度技能的提升。

刚工作的时候被分配到一个视频组件组,主要功能就是网络收流、报文解析、音视频解码、视频渲染、音频同步播放,和当时的视频播放软件最大的区别就是多窗口,最大支持128个视频流同时播放。

当时一个大版本发布之后,长时间运行(1天)组件就会出现内存申请失败,这个问题刚好就交给我处理,当时整个公司都没有人会用分析dump,大家都是用vs进行调试,还有代码的回滚是不可能的,这个版本合入太多功能,其他团队、客户都等着这个版本呢。
所以我的思路

  1. 第一步是减少问题复现的时间,通过3天左右的观察、验证,找到了业务流程,写了一个测试demo,将原来需要1天才复现的问题,减少到只需要2个小时左右。

  2. 在观察、验证的3天时间,查资料,找原因,把《windows核心编程》 内存方面仔细研读,上网找资料。大概可以锁定是内存碎片导致,而不是内存泄漏。

  3. 进行二分法对模块进行分解测试。所以短时间内我又要把整个组件的模块设计、流程学习一遍。

  4. 建立execl对测试结果数据进行分析

  5. 3~4 步骤坚持了1周左右。

  6. 大家可能都觉得应该能找到问题了吧? 结论是 no, 没找到原因。

我们可以梳理下内存碎片的原因,大致就是 a、b 两个大小内存随机申请,最后会导致一个连续内存会被a、b大小内存随机分裂,最后有一个更大的C就无法申请到内存。如果采用二分划分,那么很有可能我们把C给排除了,把a、b 模块保留,这时候程序当然稳定运行,execl表格体现出来的就是这个a、b模块正常,但实际是存在问题的。

现实的程序远比上面的例子复杂的多,模块、开源库、第三方依赖都得考虑,这也是当时整理了1周左右的数据仍然定位不到原因。

问题的分析已经过去了1周半,却没有任何进展,手上堆积的事情也越来越多,这1周半几乎都是7~23点,身体、精神也吃不消,所以就换了一个同事重新整理execl;一周过后也是没有头绪。在这一周中,我赶完落后的项目进度,然后继续查找资料,在网上看到了一本《软件调试》的书籍,学习堆的知识,这一周客户、boss都催的紧,粗略的看完“堆”章节,里面有句话吸引到了我:

1
2
3
4
5
6
7
8
9
Heap中有两个参数
1、HeapDeCommitFreeBlockThreshold
2、HeapDecommitTotalFreeThreshold

考虑到应用程序很可能还会立刻申请内存并减少与内存管理器的交互次数,内存释放时需要同时满足两个条件才能被立即解除提交
1、释放的堆块超过HeapDeCommitFreeBlockThreshold的值(比如4KB)
2、堆上的总空闲空间达到HeapDecommitTotalFreeThreshold(比如64K)

否则,堆管理器会将这个块加到空闲块列表中,并更新堆管理器的总空闲值。

刚好我们的组件中有一个大小申请65535大小的空间,会不会因为刚好没到64K导致的? 立刻把模块中的65535修改为65535+1, 经过验证后,程序还真稳定了……….其实也没根本解决问题,只是怀疑,所以内心一直觉的有所欠缺。

工作实在太忙,能力也有限,解决之后也没时间总结这个问题,还有好多事情要加班干呢………

之后的windows上项目基本就很少出现内存碎片的问题,所以就没有这方面的分析经验。

问题再次分析

刚好有程序又出现了类似问题,所以试着用dump来分析。首先我对出现问题的程序代码一点不熟悉,所以我很难去构建复现条件,只能暂时通过dump来分析。

上面截图显示了7天的内存变化,很稳定,有查看了30天的内存,基本也是一样,virtual size 还足够,Private Bytes也不高,所以问题的紧急程度不高,估摸着程序再继续运行一个月也没问题。保存了fulldump。

基础知识

我们大致要了解内存申请的过程,commit、reserv、virtual size、Private Bytes的概念。
堆的三个重要结构体:HEAP、Segment、Enty。
不了解的话,可以先照猫画虎 来按步骤分析一遍。

dump下载

下载
提取码:ykgb

windbg

heap -s

加载dump信息,输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
0:000> !heap -s
SEGMENT HEAP ERROR: failed to initialize the extention
LFH Key : 0x58b8f7f8
Termination on corruption : DISABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-----------------------------------------------------------------------------
002a0000 00000002 1246976 510132 1246976 261431 2450 1382 0 5d6 LFH
External fragmentation 51 % (2450 free blocks)
Virtual address fragmentation 59 % (1382 uncommited ranges)
00590000 00001002 3136 1636 3136 860 6 3 0 471b LFH
External fragmentation 52 % (6 free blocks)
00490000 00001002 256 4 256 1 1 1 0 0
004d0000 00001002 1280 420 1280 122 50 2 0 0 LFH
Virtual block: 032f0000 - 032f0000 (size 00000000)
Virtual block: 05310000 - 05310000 (size 00000000)
00910000 00001002 1280 556 1280 136 4 2 2 0 LFH
00b20000 00001002 64 12 64 4 2 1 0 0
00110000 00001002 64 4 64 2 1 1 0 0
03120000 00001002 64 4 64 2 1 1 0 0
00520000 00011002 256 4 256 1 2 1 0 0
032b0000 00001002 256 4 256 1 2 1 0 0
034d0000 00001002 3328 1996 3328 1050 49 3 0 6 LFH
External fragmentation 52 % (49 free blocks)
05420000 00001002 64 16 64 13 1 1 0 0
00960000 00001002 1088 212 1088 77 2 2 0 0
-----------------------------------------------------------------------------

也可以看出 002a0000 的resevr 比 commit 大了很多。

ps:里面有“Virtual block:” 代表的是超过508K的内存是使用HeapVirtualAlloc申请的,其中(size 00000000)指的是找不到HEAP_VIRTUAL_ALLOC_ENTRY的结构体定义(不知道怎么解决)。

!heap -a 002a0000

打印该堆段所有的内存信息,(截取一部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Segment80 at 54790000:
Flags: 00000000
Base: 54790000
First Entry: 54790040
Last Entry: 55760000
Total Pages: 00000fd0
Total UnCommit: 00000b0f
Largest UnCommit:00000000
UnCommitted Ranges: (9)

Heap entries for Segment80 in Heap 002a0000
address: psize . size flags state (requested size)
54790000: 00000 . 00040 [101] - busy (3f)
54790040: 00040 . 40000 [101] - busy (3fff8) Internal
547d0040: 40000 . 40000 [101] - busy (3fff8) Internal
54810040: 40000 . 40000 [101] - busy (3fff8) Internal
54850040: 40000 . 00fa0 [100]
54850fe0: 00fa0 . 00020 [111] - busy (1d)
54851000: 0063d000 - uncommitted bytes.
54e8e000: 00000 . 01000 [100]
54e8f000: 01000 . 40000 [101] - busy (3fff8) Internal
54ecf000: 40000 . 40000 [101] - busy (3fff8) Internal
54f0f000: 40000 . 40000 [101] - busy (3fff8) Internal
54f4f000: 40000 . 40fe0 [100]
54f8ffe0: 40fe0 . 00020 [111] - busy (1d)
54f90000: 000fe000 - uncommitted bytes.
5508e000: 00000 . 40000 [100]
550ce000: 40000 . 01000 [101] - busy (ff8) Internal
550cf000: 01000 . 40000 [100]
5510f000: 40000 . 20000 [101] - busy (1fff8) Internal
5512f000: 20000 . 10000 [101] - busy (fff8) Internal
5513f000: 10000 . 03fe0 [100]
55142fe0: 03fe0 . 00020 [111] - busy (1d)
55143000: 0000b000 - uncommitted bytes.
5514e000: 00000 . 40000 [100]
5518e000: 40000 . 40000 [101] - busy (3fff8) Internal
551ce000: 40000 . 00fe0 [100]
551cefe0: 00fe0 . 00020 [111] - busy (1d)
551cf000: 001ff000 - uncommitted bytes.
553ce000: 00000 . 40840 [100]
5540e840: 40840 . 40000 [101] - busy (3fff8) Internal
5544e840: 40000 . 00400 [101] - busy (3f8) Internal
5544ec40: 00400 . 003a0 [100]
5544efe0: 003a0 . 00020 [111] - busy (1d)
5544f000: 000be000 - uncommitted bytes.
5550d000: 00000 . 01000 [100]
5550e000: 01000 . 40000 [101] - busy (3fff8) Internal
5554e000: 40000 . 40fe0 [100]
5558efe0: 40fe0 . 00020 [111] - busy (1d)
5558f000: 0000e000 - uncommitted bytes.
5559d000: 00000 . 00800 [100]
5559d800: 00800 . 10000 [101] - busy (fff8) Internal
555ad800: 10000 . 10000 [101] - busy (fff8) Internal
555bd800: 10000 . 407e0 [100]
555fdfe0: 407e0 . 00020 [111] - busy (1d)

简单的含义

1
2
3
4
5
6
7
8
9
Segment80 at 54790000:                ----- 第80个堆块链表
Flags: 00000000
Base: 54790000
First Entry: 54790040
Last Entry: 55760000
Total Pages: 00000fd0
Total UnCommit: 00000b0f
Largest UnCommit:00000000
UnCommitted Ranges: (9) ------ 有9个未提交的内存

1
2
3
4
5
6
7
8
9
address  : psize .      size         [flags]   state    (requested size)    <debug flags>
堆块地址 前一堆块大小 当前堆块大小 状态 状态描述 用户实际分配的大小 哪种堆调试支持

5544efe0: 003a0 . 00020 [111] - busy (1d)
5544efe0: enty 的地址: 5544efe0+8 就是内存实际地址
003a0: 上一个内存块的大小
0020: 当前堆块大小
busy: 使用中
1d: 实际分配的大小

那么把所有链表统计一下大概出现了 1300多次的uncommitted bytes,其中大致浏览存在着1000多次的40000内存busy状态,基本上每个uncommitted 的内存附件都有一个40000的内存,那么是否可以怀疑是40000导致的呢?
40000的堆块大小实际用户申请大小是 3fff8,刚好是256K-8 。逐一查看内存内容也没有找到任何线索。

至此dump的分析导致为此,剩下的过程就需要查看是哪里申请了这么大的内存?

  1. 代码review
  2. 用gflags打开栈回溯,问题复现后,通过内存地址进行栈回溯找到内存申请的代码

总结

问题其实没有被定位,后续有时间会继续跟踪此问题。
事实上即使找到内存申请的地方,也不一定能解决。实际对于这种大块(>4KB)的内存申请应该需要谨慎,写代码中也应该尽量避免大块内存的申请,可以考虑使用内存池;
windows本身是有低块堆来解决小内存的申请,但申请的内存大小有限;
偷懒可以直接使用jemalloc/tcmalloc库
本文只是提供了一种分析思路。

后续分析原因 内存碎片分析(2)