打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
C#:.NET陷阱之五:奇怪的OutOfMemoryException

 我们在开发过程中曾经遇到过一个奇怪的问题:当软件加载了很多比较大规模的数据后,会偶尔出现OutOfMemoryException异常,但通过内存检查工具却发现还有很多可用内存。于是我们怀疑是可用内存总量充足,但却没有足够的连续内存了----也就是说存在很多未分配的内存空隙。但不是说.NET运行时的垃圾收集器会压缩使用中的内存,从而使已经释放的内存空隙连成一片吗?于是我深入研究了一下垃圾回收相关的内容,最终明确的了问题所在----大对象堆(LOH)的使用。如果你也遇到过类似的问题或者对相关的细节有兴趣的话,就继续读读吧。

如果没有特殊说明,后面的叙述都是针对32位系统。

首先我们来探讨另外一个问题:不考虑非托管内存的使用,在最坏情况下,当系统出现OutOfMemoryException异常时,有效的内存(程序中有GCRoot的对象所占用的内存)使用量会是多大呢?2G?1G? 500M?50M?或者更小(是不是以为我在开玩笑)?来看下面这段代码(参考 https://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/)。

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
publicclass Program
{
    staticvoid Main(string[]args)
    {
        varsmallBlockSize = 90000;
        varlargeBlockSize = 1 <<24;
        varcount = 0;
        varbigBlock = newbyte[0];
        try
        {
            varsmallBlocks = newList<byte[]>();
            while(true)
            {
                GC.Collect();
                bigBlock= newbyte[largeBlockSize];
                largeBlockSize++;
                smallBlocks.Add(newbyte[smallBlockSize]);
                count++;
            }
        }
        catch(OutOfMemoryException)
        {
            bigBlock= null;
            GC.Collect();
            Console.WriteLine("{0}Mb allocated",
                (count* smallBlockSize) / (1024 * 1024));
        }
         
        Console.ReadLine();
    }
}

这段代码不断的交替分配一个较小的数组和一个较大的数组,其中较小数组的大小为90,000字节,而较大数组的大小从16M字节开始,每次增加一个字节。如代码第15行所示,在每一次循环中bigBlock都会引用新分配的大数组,从而使之前的大数组变成可以被垃圾回收的对象。在发生OutOfMemoryException时,实际上代码会有count个小数组和一个大小为16M + count的大数组处于有效状态。最后代码输出了异常发生时小数组所占用的内存总量。

下面是在我的机器上的运行结果----和你的预测有多大差别?提醒一下,如果你要亲自测试这段代码,而你的机器是64位的话,一定要把生成目标改为x86。

23 Mb allocated

考虑到32位程序有2G的可用内存,这里实现的使用率只有1%!

下面即介绍个中原因。需要说明的是,我只是想以最简单的方式阐明问题,所以有些语言可能并不精确,可以参考http://msdn.microsoft.com/en-us/magazine/cc534993.aspx以获得更详细的说明。

.NET的垃圾回收机制基于“Generation”的概念,并且一共有G0,G1,G2三个Generation。一般情况下,每个新创建的对象都属于于G0,对象每经历一次垃圾回收过程而未被回收时,就会进入下一个Generation(G0-> G1 ->G2),但如果对象已经处于G2,则它仍然会处于G2中。

软件开始运行时,运行时会为每一个Generation预留一块连续的内存(这样说并不严格,但不影响此问题的描述),同时会保持一个指向此内存区域中尚未使用部分的指针P,当需要为对象分配空间时,直接返回P所在的地址,并将P做相应的调整即可,如下图所示。【顺便说一句,也正是因为这一技术,在.NET中创建一个对象要比在C或C++的堆中创建对象要快很多----当然,是在后者不使用额外的内存管理模块的情况下。】

在对某个Generation进行垃圾回收时,运行时会先标记所有可以从有效引用到达的对象,然后压缩内存空间,将有效对象集中到一起,而合并已回收的对象占用的空间,如下图所示。

但是,问题就出在上面特别标出的“一般情况”之外。.NET会将对象分成两种情况区别对象,一种是大小小于85,000字节的对象,称之为小对象,它就对应于前面描述的一般情况;另外一种是大小在85,000之上的对象,称之为大对象,就是它造成了前面示例代码中内存使用率的问题。在.NET中,所有大对象都是分配在另外一个特别的连续内存(LOH,Large ObjectHeap)中的,而且,每个大对象在创建时即属于G2,也就是说只有在进行Generation2的垃圾回收时,才会处理LOH。而且在对LOH进行垃圾回收时不会压缩内存!更进一步,LOH上空间的使用方式也很特殊----当分配一个大对象时,运行时会优先尝试在LOH的尾部进行分配,如果尾部空间不足,就会尝试向操作系统请求更多的内存空间,只有在这一步也失败时,才会重新搜索之前无效对象留下的内存空隙。如下图所示:

从上到下看

  1. LOH中已经存在一个大小为85K的对象和一个大小为16M对象,当需要分配另外一个大小为85K的对象时,会在尾部分配空间;

  2. 此时发生了一次垃圾回收,大小为16M的对象被回收,其占用的空间为未使用状态,但运行时并没有对LOH进行压缩;

  3. 此时再分配一个大小为16.1M的对象时,分尝试在LOH尾部分配,但尾部空间不足。所以,

  4. 运行时向操作系统请求额外的内存,并将对象分配在尾部;

  5. 此时如果再需要分配一个大小为85K的对象,则优先使用尾部的空间。

所以前面的示例代码会造成LOH变成下面这个样子,当最后要分配16M+N的内存时,因为前面已经没有任何一块连续区域满足要求时,所以就会引发OutOfMemoryExceptiojn异常。

 

要解决这一问题其实并不容易,但可以考虑下面的策略。 

  1. 将比较大的对象分割成较小的对象,使每个小对象大小小于85,000字节,从而不再分配在LOH上;

  2. 尽量“重用”少量的大对象,而不是分配很多大对象;

  3. 每隔一段时间就重启一下程序。

最终我们发现,我们的软件中使用数组(List<float>)保存了一些曲线数据,而这些曲线的大小很可能会超过了85,000字节,同时曲线对象的个数也非常多,从而对LOH造成了很大的压力,甚至出现了文章开头所描述的情况。针对这一情况,我们采用了策略1的方法,定义了一个类似C++中deque的数据结构,它以分块内存的方式存储数据,而且保证每一块的大小都小于85,000,从而解决了这一问题。

此外要说的是,不要以为64位环境中可以忽略这一问题。虽然64位环境下有更大的内存空间,但对于操作系统来说,.NET中的LOH会提交很大范围的内存区域,所以当存在大量的内存空隙时,即使不会出现OutOfMemoryException异常,也会使得内页页面交换的频率不断上升,从而使软件运行的越来越慢。

最后分享我们定义的分块列表,它对IList<T>接口的实现行为与List<T>相同,代码中只给出了比较重要的几个方法。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
publicclass BlockList<T>: IList<T>
{
    privatestatic intmaxAllocSize;
    privatestatic intinitAllocSize;
    privateT[][] blocks;
    privateint blockCount;
    privateint[] blockSizes;
    privateint version;
    privateint countCache;
    privateint countCacheVersion;
 
    staticBlockList()
    {
        vartype = typeof(T);
        varsize = type.IsValueType ?Marshal.SizeOf(default(T)) :IntPtr.Size;
        maxAllocSize= 80000 / size;
        initAllocSize= 8;
    }
 
    publicBlockList()
    {
        blocks= new T[8][];
        blockSizes= new int[8];
        blockCount= 0;
    }
 
    publicvoid Add(T item)
    {
        intblockId = 0, blockSize = 0;
        if(blockCount == 0)
        {
            UseNewBlock();
        }
        else
        {
            blockId= blockCount - 1;
            blockSize= blockSizes[blockId];
            if(blockSize == blocks[blockId].Length)
            {
                if(!ExpandBlock(blockId))
                {
                    UseNewBlock();
                    ++blockId;
                    blockSize= 0;
                }
            }
        }
 
        blocks[blockId][blockSize]= item;
        ++blockSizes[blockId];
        ++version;
    }
 
    publicvoid Insert(int index,T item)
    {
        if(index > Count)
        {
            thrownewArgumentOutOfRangeException("index");
        }
 
        if(blockCount == 0)
        {
            UseNewBlock();
            blocks[0][0]= item;
            blockSizes[0]= 1;
            ++version;
            return;
        }
 
        for(int i = 0; i <blockCount; ++i)
        {
            if(index >= blockSizes[i])
            {
                index-= blockSizes[i];
                continue;
            }
 
            if(blockSizes[i] < blocks[i].Length ||ExpandBlock(i))
            {
                for(var j = blockSizes[i]; j > index;--j)
                {
                    blocks[i][j]= blocks[i][j - 1];
                }
 
                blocks[i][index]= item;
                ++blockSizes[i];
                break;
            }
 
            if(i == blockCount - 1)
            {
                UseNewBlock();
            }
 
            if(blockSizes[i + 1] == blocks[i + 1].Length
                &&!ExpandBlock(i + 1))
            {
                UseNewBlock();
                varnewBlock = blocks[blockCount - 1];
                for(int j = blockCount - 1; j> i + 1; --j)
                {
                    blocks[j]= blocks[j - 1];
                    blockSizes[j]= blockSizes[j - 1];
                }
 
                blocks[i+ 1] = newBlock;
                blockSizes[i+ 1] = 0;
            }
 
            varnextBlock = blocks[i + 1];
            varnextBlockSize = blockSizes[i + 1];
            for(var j = nextBlockSize; j > 0;--j)
            {
                nextBlock[j]= nextBlock[j - 1];
            }
 
            nextBlock[0]= blocks[i][blockSizes[i] - 1];
            ++blockSizes[i+ 1];
 
            for(var j = blockSizes[i] - 1; j > index;--j)
            {
                blocks[i][j]= blocks[i][j - 1];
            }
 
            blocks[i][index]= item;
            break;
        }
 
        ++version;
    }
 
    publicvoid RemoveAt(intindex)
    {
        if(index < 0 || index >=Count)
        {
            thrownewArgumentOutOfRangeException("index");
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
Box2D 内存管理
OutOfMemoryException问题的处理
.Net内存分配笔记
Java数组中常见的面试题
从GC的角度看性能优化
【原创】构建高性能ASP.NET站点 第七章 如何解决内存的问题(前篇)—托管资源优化—垃...
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服