您的位置:首页 > 编程语言 > C#

C# 大型对象堆学习总结

2017-04-11 18:14 141 查看

大型对象堆揭秘

http://blog.csdn.net/jfkidear/article/details/18358551

CLR 全面透彻解析

大型对象堆揭秘

Maoni Stephens

 目录

大型对象堆和 GC 

何时回收大型对象 

LOH 性能意义 

回收 LOH 的性能数据 

使用调试器 

CLR 垃圾回收器 (GC) 将对象分为大型、小型两类。如果是大型对象,与其相关的一些属性将比对象较小时显

得更为重要。例如,压缩大型对象(将内存复制到堆上的其他位置)的费用相当高。在本月的专栏中,我将深

入探讨大型对象堆。我将讨论符合什么条件的对象才能称之为大型对象,如何回收这些大型对象,以及大型对

象具备哪些性能意义。

大型对象堆和 GC

在 Microsoft® .NET Framework 1.1 和 2.0 中,如果对象大于或等于 85,000 字节,将被视为大型对象。此

数字根据性能优化的结果确定。当对象分配请求传入后,如果符合该大小阈值,便会将此对象分配给大型对象

堆。这究竟是什么意思呢?要理解这些内容,先了解一些关于 .NET 垃圾回收器的基础知识可能会有所帮助。

众所周知,.NET 垃圾回收器是分代回收器。它包含三代:第 0 代、第 1 代和第 2 代。之所以分代,是因为

在良好调优的应用程序中,您可以在第 0 代清除大部分对象。例如,在服务器应用程序中,与每个请求关联的

分配将在完成请求后清除。仍存在的分配请求将转到第 1 代,并在那里进行清除。从本质上讲,第 1 代是新

对象区域与生存期较长的对象区域之间的缓冲区。

从分代的角度来说,大型对象属于第 2 代,因为只有在第 2 代回收过程中才能回收它们。回收一代时,同时

也会回收所有前面的代。例如,执行第 1 代垃圾回收时,将同时回收第 1 代和第 0 代。执行第 2 代垃圾回

收时,将回收整个堆。因此,第 2 代垃圾回收也称为完整垃圾回收。在本专栏中,我将使用术语“第 2 代垃

圾回收”而不是“完整垃圾回收”,但它们可以互换。

垃圾回收器堆的各代是按逻辑划分的。实际上,对象存在于托管堆栈段上。托管堆栈段是垃圾回收器通过调用 

VirtualAlloc 代表托管代码在操作系统上保留的内存块。加载 CLR 时,将分配两个初始堆栈段(一个用于小

型对象,另一个用于大型对象),我将它们分别称为小型对象堆 (SOH) 和大型对象堆 (LOH)。

然后,通过将托管对象置于任一托管堆栈段上来满足分配请求。如果对象小于 85,000 字节,则将其放在 SOH 

段上;否则将其放在 LOH 段上。随着分配到各段上的对象越来越多,会以较小块的形式提交这些段。

对于 SOH,垃圾回收未处理的对象将进入下一代;由此第 0 代回收未处理的对象将被视为第 1 代对象,依此

类推。但是,最后一代回收未处理的对象仍会被视为最后一代中的对象。也就是说,第 2 代垃圾回收未处理的

对象仍是第 2 代对象;LOH 未处理的对象仍是 LOH 对象(由第 2 代回收)。用户代码只能在第 0 代(小型

对象)或 LOH(大型对象)中分配。只有垃圾回收器可以在第 1 代(通过提升第 0 代回收未处理的对象)和

第 2 代(通过提升第 1 代和第 2 代回收未处理的对象)中“分配”对象。

触发垃圾回收后,垃圾回收器将寻找存在的对象并将它们压缩。不过对于 LOH,由于压缩费用很高,CLR 团队

会选择扫过所有对象,列出没有被清除的对象列表以供以后重新使用,从而满足大型对象的分配请求。相邻的

被清除对象将组成一个自由对象。

有一点必须注意,虽然目前我们不会压缩 LOH,但将来可能会进行压缩。因此,如果您分配了大型对象并希望

确保它们不被移动,则应将其固定起来。

请注意,下面的图仅用于说明。我使用了很少的对象,只为说明堆上发生的事件。实际上,还存在许多对象。

图 1 说明了一种情况,在第一次第 0 代 GC 后形成了第 1 代,其中 Obj1 和 Obj3 被清除;在第一次第 1 

代 GC 后形成了第 2 代,其中 Obj2 和 Obj5 被清除。

图 1 SOH 分配和垃圾回收(单击图像可查看大图)

图 2 说明在第 2 代垃圾回收后,您将看到 Obj1 和 Obj2 被清除,内存中原来存放 Obj1 和 Obj2 的空间将

成为一个可用空间,随后可用于满足 Obj4 的分配请求。从最后一个对象 Obj3 到此段末尾的空间仍可用于以

后的分配请求。

图 2 LOH 分配和垃圾回收(单击图像可查看大图)

如果没有足够的可用空间来容纳大型对象分配请求,我会先尝试从操作系统获取更多段。如果失败,我将触发

第 2 代垃圾回收以便释放一些空间。

在第 2 代垃圾回收期间,我会把握时机将不包含任何活动对象的段释放回操作系统(通过调用 VirtualFree)

。从最后一个存在的对象到该段末尾的内存将退回。而且,尽管已重置可用空间,但仍会提交它们,这意味着

操作系统无需将其中的数据重新写入磁盘。图 3 说明了一种情况,我将一个段(段 2)释放回操作系统,并在

剩下的段中退回了更多空间。如果需要使用该段末尾的已退回空间来满足新的大型对象分配请求,我可以再次

提交该内存。

图 3 垃圾回收期间在 LOH 上释放的已消除段(单击图像可查看大图)

有关提交/退回的说明,请参阅有关 VirtualAlloc 的 MSDN® 文档,网址为 go.microsoft.com/fwlink/?

LinkId=116041。

何时回收大型对象

要确定何时回收大型对象,我们首先讨论一下通常何时会执行垃圾回收。如果发生下列情况之一,将执行垃圾

回收:

分配超出第 0 代或大型对象阈值 大部分 GC 都是由于需在托管堆上进行分配而执行(这是最典型的情况)。

调用 System.GC.Collect 如果对第 2 代调用 GC.Collect(通过不向 GC.Collect 传递参数或将 

GC.MaxGeneration 作为参数传递),将立即回收 LOH 及其他托管堆。

系统内存太低 收到来自操作系统的高内存通知时会发生此情况。如果我认为执行第 2 代垃圾回收会有所帮助

,就会触发一个垃圾回收。

阈值是各代的属性。将对象分配给某代时,会增加该代的内存量,使之接近该代的阈值。当超出某代的阈值时

,便会在该代触发垃圾回收。因此,当您分配小型或大型对象时,需要分别使用第 0 代和 LOH 的阈值。当垃

圾回收器分配到第 1 代和第 2 代中时,将使用第 1 代的阈值。运行此程序时,会动态调整这些阈值。

LOH 性能意义

下面,我们来看一下分配成本。CLR 确保清除了我提供的每个新对象的内存。这意味着大型对象的分配成本完

全由清理的内存(除非触发了垃圾回收)决定。如果需要两轮才能清除 1 个字节,则意味着需要 170,000 轮

才能清除最小的大型对象。这对于分配较大的大型对象的人们来说很平常。对于 2GHz 计算机上的 16MB 对象

,大约需要 16ms 才能清除内存。这些成本相当大。

现在我们来看一下回收成本。前面曾提到,LOH 和第 2 代将一起回收。如果超过两者中任何一个的阈值,都会

触发第 2 代回收。如果由于第 2 代为 LOH 而触发了第 2 代回收,则第 2 代本身在垃圾回收后不一定会变得

更小。因此,如果第 2 代中的数据不多,这将不是问题。但是,如果第 2 代很大,则触发多次第 2 代垃圾回

收可能会产生性能问题。如果要临时分配许多大型对象,并且您拥有一个大型 SOH,则运行垃圾回收可能会花

费很长时间;毫无疑问,如果仍继续分配和处理真正的大型对象,分配成本肯定会大幅增加。

LOH 上的特大对象通常是数组(很少会有非常大的实例对象)。如果数组元素包含很多引用,则成本将会很高

。如果元素不包含任何引用,则根本无需处理此数组。例如,如果使用数组存储二进制树中的节点,一种实现

方法是按实际节点引用某个节点的左侧节点和右侧节点:

 class Node

{

    Data d;

    Node left;

    Node right;

};

Node[] binary_tr = new Node [num_nodes];

如果 num_nodes 很大,则意味着至少需要对每个元素处理两个引用。另一种方法是存储左侧节点和右侧节点的

索引:

 class Node

{

    Data d;

    uint left_index;

    uint right_index;

};

这样,您可将左侧节点的数据作为 binary_tr[left_index].d 引用,而非作为 left.d 引用;而垃圾回收器无

需查看左侧节点和右侧节点的任何引用。

在这三个回收原因中,通常前两个比第三个出现得多。因此,最好能够分配一个大型对象池并重新使用这些对

象,而不是分配临时对象。Yun Jin 在其博客日志 (go.microsoft.com/fwlink/?LinkId=115870) 中介绍了一

个此类缓冲池的示例。当然,您可能希望增加缓冲区大小。

回收 LOH 的性能数据

可以通过某些方法来回收与 LOH 相关的性能数据。不过,在介绍它们之前,我们先谈论一下为什么要进行回收



在开始回收特定区域的性能数据前,希望您已经找到需查看此区域的原因,或您已查看了其他已知区域但未发

现任何问题可解释您需要解决的性能问题。

有关详细解释,建议您阅读我的博客日志(请参见 go.microsoft.com/fwlink/?LinkId=116467)。在日志中,

我介绍了内存和 CPU 的基础知识。另外,2006 年 11 月期刊中的“CLR 全面透彻解析”针对内存问题进行了

调查,介绍了在托管过程中诊断可能与托管堆相关的性能问题涉及的步骤(请参见

msdn2.microsoft.com/magazine/cc163528)。

.NET CLR 内存性能计数器通常是调查性能问题的第一步。与 LOH 相关的计数器显示第 2 代回收的数目和大型

对象堆的大小。第 2 代回收的数目显示了自回收过程开始执行第 2 代垃圾回收的次数。计数器会在第 2 代垃

圾回收(也称为完整垃圾回收)结束时递增。此计数器显示最后看到的值。

大型对象堆大小指的是大型对象堆的当前大小(以字节为单位,包括可用空间)。此计数器将在垃圾回收结束

时更新,而不是在每次分配时更新。

查看性能计数器的常用方法是使用性能监视器 (PerfMon.exe)。使用“添加计数器”可为您关注的过程添加感

兴趣的计数器,如图 4 所示。

图 4 在性能监视器中添加计数器(单击图像可查看大图)

您可以将性能计数器数据保存在性能监视器的日志文件中,也可以编程方式查询性能计数器。大部分人在例行

测试过程中都采用此方式进行收集。如果发现计数器显示的值不正常,则可以使用其他方法获得更多详细信息

以帮助调查。

使用调试器

在开始之前,请注意我此部分提及的调试命令仅适用于 Windows® 调试器。如果需要查看 LOH 上实际存在的对

象,您可以使用 CLR 提供的 SoS 调试器扩展,在前面提到的 2006 年 11 月期刊中已对此进行了介绍。图 5

中显示了分析 LOH 的输出示例。

图 5 中的加粗部分显示 LOH 堆的大小为 (16,754,224 + 16,699,288 + 16,284,504 =) 49,738,016 个字节。

而在 023e1000 和 033db630 之间,System.Object[] 对象占用了 8,008,736 个字节;System.Byte[] 对象占

用了 6,663,696 个字节;可用空间占用了 2,081,792 个字节。

   图 5 LOH 输出

 0:003> .loadby sos mscorwks

0:003> !eeheap -gc

Number of GC Heaps: 1

generation 0 starts at 0x013e35ec

generation 1 starts at 0x013e1b6c

generation 2 starts at 0x013e1000

ephemeral segment allocation context: none

 segment    begin allocated     size

0018f2d0 790d5588  790f4b38 0x0001f5b0(128432)

013e0000 013e1000  013e35f8 0x000025f8(9720)

Large object heap starts at 0x023e1000

 segment    begin allocated     size

023e0000 023e1000  033db630 0x00ffa630(16754224)

033e0000 033e1000  043cdf98 0x00fecf98(16699288)

043e0000 043e1000  05368b58 0x00f87b58(16284504)

Total Size  0x2f90cc8(49876168)

------------------------------

GC Heap Size  0x2f90cc8(49876168)

0:003> !dumpheap -stat 023e1000  033db630

total 133 objects

Statistics:

      MT    Count    TotalSize Class Name

001521d0       66      2081792      Free

7912273c       63      6663696 System.Byte[]

7912254c        4      8008736 System.Object[]

Total 133 objects

有时,您会看到 LOH 的总大小少于 85,000 个字节。为什么会这样?这是因为运行时本身实际使用 LOH 分配

某些小于大型对象的对象。

由于不会压缩 LOH,有时人们会怀疑 LOH 是碎片源。事实上,在得出这个结论前,您最好先弄清什么是碎片。

有一种托管堆碎片,由托管对象之间的可用空间量指示(换句话说,在 SoS 中执行 !dumpheap –type Free 

时看到的内容);还有虚拟内存 (VM) 地址空间碎片,即标记为 MEM_FREE 的内存以及在 windbg 中使用各种

调试器命令可看到的内容(请参见 go.microsoft.com/fwlink/?LinkId=116470)。图 6 显示了虚拟内存空间

中的碎片(请注意图中的加粗文本)。

   图 6 VM 空间碎片

 0:000> !address

    00000000 : 00000000 - 00010000

                    Type     00000000 

                    Protect  00000001 PAGE_NOACCESS

                    State    00010000 MEM_FREE

                    Usage    RegionUsageFree

    00010000 : 00010000 - 00002000

                    Type     00020000 MEM_PRIVATE

                    Protect  00000004 PAGE_READWRITE

                    State    00001000 MEM_COMMIT

                    Usage    RegionUsageEnvironmentBlock

    00012000 : 00012000 - 0000e000

                    Type     00000000 

                    Protect  00000001 PAGE_NOACCESS

                    State    00010000 MEM_FREE

                    Usage    RegionUsageFree

... [omitted]

-------------------- Usage SUMMARY --------------------------

    TotSize (      KB)   Pct(Tots) Pct(Busy)   Usage

     701000 (    7172) : 00.34%    20.69%    : RegionUsageIsVAD

   7de15000 ( 2062420) : 98.35%    00.00%    : RegionUsageFree

    1452000 (   20808) : 00.99%    60.02%    : RegionUsageImage

     300000 (    3072) : 00.15%    08.86%    : RegionUsageStack

       3000 (      12) : 00.00%    00.03%    : RegionUsageTeb

     381000 (    3588) : 00.17%    10.35%    : RegionUsageHeap

          0 (       0) : 00.00%    00.00%    : RegionUsagePageHeap

       1000 (       4) : 00.00%    00.01%    : RegionUsagePeb

       1000 (       4) : 00.00%    00.01%    : RegionUsageProcessParametrs

       2000 (       8) : 00.00%    00.02%    : RegionUsageEnvironmentBlock

       Tot: 7fff0000 (2097088 KB) Busy: 021db000 (34668 KB)

-------------------- Type SUMMARY --------------------------

    TotSize (      KB)   Pct(Tots)  Usage

   7de15000 ( 2062420) : 98.35%   : <free>

    1452000 (   20808) : 00.99%   : MEM_IMAGE

     69f000 (    6780) : 00.32%   : MEM_MAPPED

     6ea000 (    7080) : 00.34%   : MEM_PRIVATE

-------------------- State SUMMARY --------------------------

    TotSize (      KB)   Pct(Tots)  Usage

    1a58000 (   26976) : 01.29%   : MEM_COMMIT

   7de15000 ( 2062420) : 98.35%   : MEM_FREE

     783000 (    7692) : 00.37%   : MEM_RESERVE

Largest free region: Base 01432000 - Size 707ee000 (1843128 KB)

前面曾提到,托管堆上的碎片用于分配请求。通常看到的更多是由临时大型对象导致的虚拟内存碎片,需要频

繁进行垃圾回收以便从操作系统获取新的托管堆段,并将空托管堆段释放回操作系统。

要验证 LOH 是否会生成 VM 碎片,可在 VirtualAlloc 和 VirtualFree 上设置一个断点,查看是谁调用了它

们。例如,如果想知道谁曾尝试从操作系统分配大于 8MB 的 VM 块,可按以下方式设置断点:

 bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"

如果调用 VirtualAlloc 时分配大小大于 8MB (0x800000),此代码会中断调试器并显示调用堆栈,否则不会中

断调试器。

在 CLR 2.0 中,我们添加了名为 VM Hoarding 的功能,如果需要经常获取和释放段(包括用于大型对象堆和

小型对象堆两者的段),则可以使用此功能。要指定 VM Hoarding 功能,请通过宿主 API 指定名为 

STARTUP_HOARD_GC_VM 的启动标志(请参见 go.microsoft.com/fwlink/?LinkId=116471)。指定此标志后,只

会退回这些段上的内存并将其添加到备用列表中,而不会将该空段释放回操作系统。备用列表上的段以后可用

于满足新的段请求。因此,下次需要新段时,如果可以从此备用列表找到足够大的段,便可以使用它。

请注意,对于太大的段,该功能不起作用。此功能还可供某些应用程序用以承载其已获得的段,如一些服务器

应用程序,它们会尽可能避免生成 VM 空间碎片以防出现内存不足错误。由于它们通常是计算机上的主应用程

序,所以可以执行这些操作。强烈建议您在使用此功能时认真测试您的应用程序,以确保内存使用情况比较稳

定。

大型对象费用很高。由于 CLR 需要清除一些新分配大型对象的内存,以满足 CLR 清除所有新分配对象内存的

保证,所以分配成本相当高。LOH 将与堆的其余部分一起回收,所以请仔细分析这会对您的应用程序性能造成

什么影响。如果可以,建议重新使用大型对象以避免托管堆和 VM 空间中生成碎片。

最后,到目前为止,在回收过程中尚不能压缩 LOH,但不应依赖于此实现详情。因此,要确保某些内容未被 GC 

移动,请始终将其固定起来。现在,请利用您刚学到的 LOH 知识对堆进行控制。

请将您的问题和意见发送至 clrinout@microsoft.com。

Maoni Stephens 是 Microsoft 的 CLR 团队中研究垃圾回收器的高级开发人员。在加入 CLR 前,Maoni 已在 

Microsoft 操作系统组工作多年。

========

C# 内存管理优化畅想(一)---- 大对象堆(LOH)的压缩

http://www.tuicool.com/articles/b22iqiI

原文  http://www.cnblogs.com/ygc369/p/4861610.html

主题 C#

我们都知道,.net的GC是不会压缩大对象堆的,因为其时间开销不可接受,但这是以大对象堆产生大块碎片为

代价的,如果以后要分配的大对象比最大的碎片还大,那么即使它比所有碎片的总大小要小,也是无法在不扩

展大对象堆的前提下分配成功的,此时有可能引发内存不足的异常。

我想到一个方案,可以让大对象堆也能压缩,而且时间开销在可接受的范围内,原理是利用页表。我们知道,

程序能看到的内存地址都是虚拟地址,是通过页表映射到物理地址的,连续的虚拟地址对应的物理地址未必连

续,反之亦然。在内存中移动大量数据,开销很大,因为数据真的要在物理内存上复制,但如果我们不动物理

内存上的数据,只修改页表及其缓存TLB,即修改了物理地址与虚拟地址的映射关系,开销就会小得多,而且对

于应用程序来说,同样达到了内存移动的效果。(物理内存上没有数据移动,但对象的虚拟地址却变了,对应

用程序来说,这就是数据移动了!)

当然,如果要用这种方法实现压缩大对象堆,也会有一些局限性:比如每个大对象必须占据整数页的空间,且

大对象的起始地址必须是某页的起始地址,这样大对象之间会出现一些小碎片(不会超过一页的大小,即不超

过4K,与85K以上的大对象本身相比,还是很小的),但小碎片总比大碎片好呀,就看怎么权衡了,而且这些小

碎片也是可以被利用的,比如可以把一些大小合适的2代小对象存储到这些小碎片中,以节约小对象堆的空间。

PS: 现在的一些虚拟机软件的实现似乎就使用了类似的方法,以达到提高效率的目的。

该问题的英文讨论贴:https://github.com/dotnet/coreclr/issues/555

========

C#:.NET陷阱之五:奇怪的OutOfMemoryException----大对象堆引起的问题与对策

http://blog.sina.com.cn/s/blog_47642c6e0102vh0v.html

我们在开发过程中曾经遇到过一个奇怪的问题:当软件加载了很多比较大规模的数据后,会偶尔出现

OutOfMemoryException异常,但通过内存检查工具却发现还有很多可用内存。于是我们怀疑是可用内存总量充

足,但却没有足够的连续内存了----也就是说存在很多未分配的内存空隙。但不是说.NET运行时的垃圾收集器

会压缩使用中的内存,从而使已经释放的内存空隙连成一片吗?于是我深入研究了一下垃圾回收相关的内容,

最终明确的了问题所在----大对象堆(LOH)的使用。如果你也遇到过类似的问题或者对相关的细节有兴趣的话

,就继续读读吧。

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

首先我们来探讨另外一个问题:不考虑非托管内存的使用,在最坏情况下,当系统出现OutOfMemoryException

异常时,有效的内存(程序中有GC Root的对象所占用的内存)使用量会是多大呢?2G? 1G? 500M? 50M?或

者更小(是不是以为我在开玩笑)?来看下面这段代码(参考 https://www.simple-talk.com/dotnet/.net-
framework/the-dangers-of-the-large-object-heap/)。

public class Program
{
static void Main(string[] args)
{
var smallBlockSize = 90000;
var largeBlockSize = 1 << 24;
var count = 0;
var bigBlock = new byte[0];
try
{
var smallBlocks = new List<byte[]>();
while (true)
{
GC.Collect();
bigBlock = new byte[largeBlockSize];
largeBlockSize++;
smallBlocks.Add(new byte[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 Object Heap)中的,而且,每个大对象在创建时即属于G2,也就是说只有在

进行Generation 2的垃圾回收时,才会处理LOH。而且在对LOH进行垃圾回收时不会压缩内存!更进一步,LOH上

空间的使用方式也很特殊----当分配一个大对象时,运行时会优先尝试在LOH的尾部进行分配,如果尾部空间不

足,就会尝试向操作系统请求更多的内存空间,只有在这一步也失败时,才会重新搜索之前无效对象留下的内

存空隙。如下图所示:

从上到下看

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

部分配空间;

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

压缩;

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

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

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

所以前面的示例代码会造成LOH变成下面这个样子,当最后要分配16M + N的内存时,因为前面已经没有任何一

块连续区域满足要求时,所以就会引发OutOfMemoryExceptiojn异常。

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

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

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

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

最终我们发现,我们的软件中使用数组(List<float>)保存了一些曲线数据,而这些曲线的大小很可能会超过

了85, 000字节,同时曲线对象的个数也非常多,从而对LOH造成了很大的压力,甚至出现了文章开头所描述的

情况。针对这一情况,我们采用了策略1的方法,定义了一个类似C++中deque的数据结构,它以分块内存的方式

存储数据,而且保证每一块的大小都小于85, 000,从而解决了这一问题。

此外要说的是,不要以为64位环境中可以忽略这一问题。虽然64位环境下有更大的内存空间,但对于操作系统

来说,.NET中的LOH会提交很大范围的内存区域,所以当存在大量的内存空隙时,即使不会出现

OutOfMemoryException异常,也会使得内页页面交换的频率不断上升,从而使软件运行的越来越慢。

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

方法。

public class BlockList<T> : IList<T>

{

    private static int maxAllocSize;

    private static int initAllocSize;

    private T[][] blocks;

    private int blockCount;

    private int[] blockSizes;

    private int version;

    private int countCache;

    private int countCacheVersion;

 

    static BlockList()

    {

        var type = typeof(T);

        var size = type.IsValueType ? Marshal.SizeOf(default(T)) : IntPtr.Size;

        maxAllocSize = 80000 / size;

        initAllocSize = 8;

    }

 

    public BlockList()

    {

        blocks = new T[8][];

        blockSizes = new int[8];

        blockCount = 0;

    }

 

    public void Add(T item)

    {

        int blockId = 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;

    }

 

    public void Insert(int index, T item)

    {

        if (index > Count)

        {

            throw new ArgumentOutOfRangeException("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();

                var newBlock = 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;

            }

 

            var nextBlock = blocks[i + 1];

            var nextBlockSize = 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;

    }

 

    public void RemoveAt(int index)

    {

        if (index < 0 || index >= Count)

        {

   

========
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  clr c#