减少大对象堆的碎片

如果不能完全避免大对象堆的分配,则要尽量避免碎片化。

对于LOH不小心就会有无限增长,但LOH使用的空闲列表机制可以减轻增长的影响。利用这个空闲列表,我们可以在两块分配区域中间找到你所想要的可分配区域。

要做到这一点,就需要保证你在LOH里的分配都按照同一个尺寸或者同一个尺寸的倍数进行。例如,一个常见的需求是在LOH里分配缓冲区。要确保分配的每个缓冲区都是一个大小,或者是一个知名数字(1M)的倍数,而不要创建大小不一的缓冲区。这样做的话,如果一个缓冲区被回收,那么下一个缓冲区在分配的时候,很大概率不会在堆结尾分配,而是会在被回收的地方重新分配。

继续用前面的MemoryStreams的的故事。我们的第一个实现我们只对PooledMemoryStream进行的池化,它的缓冲区增长还是沿用MemoryStreams的默认算法,当超过容量是,会按照当前的缓冲区大小加倍申请。这虽然解决分配问题,但是又造成了碎片问题。第二次迭代的时候,我们抛弃了这种申请算法,我们倾向于实现一个流的抽象类,将多个128K直接的缓冲区合并使用,将这些小的缓冲区用链接的方式组成一个大的缓冲区,他们大小为1MB的倍数(最大为8MB)。这个新的实现大大减少了我们的碎片问题,当然我们偶尔还会不得不将一些128KB的数据复制到1MB的缓冲区里,但这样的改进也是值得的。

在某些情况下强制执行完整GC

在几乎所有的正常情况下,你是不应该主动执行完整GC操作的,这可能会打乱GC的自动处理流程,导致一些不好的结果。但是,在一些高性能系统里存在一些情况,我们还是会建议你进行一次完整GC。

通常,在有合适的时间窗口下进行完整GC,可以避免在今后不好的时间段执行GC。注意,这里讨论的只是耗时比较多完整GC,对于0代和1代的回收还是应该频繁出发,以避免构建的0代内存区太大。

在下面情况可以做一次完整的完整GC:

  1. 你如果使用了低延迟模式,在这种模式下,堆的大小会一直增长,这个时候你需要在合适的时间点来执行一次完成GC。

  2. 如果会偶尔大量分配一些长生命周期的对象(初始化对象池),在对象创建后,可以执行一次完整GC,将对象尽快转为2代对象。或者当你不再使用这些对象,也最好在删除引用后强制回收他们。

  3. 如果你现在所处的状态,因为碎片太多,必须要做大对象堆做压缩的时候。

对于情况1,2都是在特定时间里通过强制执行GC来避免在不合适的时机被执行GC。情况3,如果你在LOH里有很大的碎片,则可以帮助你减少堆的大小。如果不是上面的情况,你最好另外想一些其它优化方案。

要执行完整GC,可以使用GC.Collect来回收所希望的代纪。还可以通过GCCollectionMode的枚举参数告诉GC是否立即执行。参数有3个值

Default--(默认)当前,强制

Forced--(强制)告诉GC立即开始收集

Optimized--(优化)由GC决定现在是否是要的时机执行回收


GC.Collect(2);
// 等价于
GC.Collect(2, GCCollectionMode.Forced);

按需压缩大对象堆

即使使用了对象池,仍然可能会在大对象堆里分配对象,随着时间的推移,在里面会存在很多碎片。从.NET 4.5.1 开始,你可以告诉GC在下一次做完整GC时顺便也对LOH做一次压缩。

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;

根据LOH的大小,这个压缩过程可能会很慢,甚至会用到好几秒。你最好是在你的程序能够长时间暂停的时候,才让垃圾回收器做一次这样的完整GC。修改该设置值,只会在下一次完整GC时会触发压缩,一旦完成了LOH的压缩,GCSettings.LargeObjectHeapCompactionMode就会被重新设置为GCLargeObjectHeapCompactionMode.Default。

因为这个过程很耗时,我还是建议你减少对LOH的分配或者使用对象池。这样将大大减少压缩的数据。压缩LOH功能只能作为碎片过多,分配的堆太大时的最后手段。

在GC前收到消息通知

如果你的应用完全不希望受到2代的的GC影响,你可以在GC快来临前收到一个通知。这样可以给你一个机会,暂停现有的业务处理,将请求分流到其它服务器,或者进入某种对你更合理的状态。

但是我建议你谨慎使用,这个GC通知机制可能会给你产生一些意料之外的情况。你应该在所有的优化手段都使用后才考虑它。如果你有下面的情况,你可以利用GC通知功能。

  1. 系统在进行一次完整GC时耗时太长,你完全无法接受
  2. 你可以完全关闭进程。(可以动态将相应请求交给其它进程)
  3. 你可以快速停止当前的业务处理。(暂停逻辑处理时间不要比执行GC的时间更多)
  4. 2代GC发生的几率很少,值得你这样处理。

2代的回收起始很少发生,更多的时候是在很多0代小对象分配时会达到触发的阈值,所以在收到GC的通知时,你还有很多工作需要做。

不幸的是,由于GC通知触发的不精确性,你只能在1-99范围你指定一个合适的触发时机。如果数字比较小,你可能会在里真正GC前才会收到消息,没有足够的时间做相应处理。但如果你的数字太高,这可能会被频繁触发而不会触及真正的GC。这两个选择取决你当前内存的分配率与内存负债。注意,这里会指定2个阈值数字,一个用于2代对象,一个用于LOH。与其它功能一样,GC会尽最大努力给你通知,但它不会保证你能不做这次GC。

要使用此功能,请按照一下步骤进行。

  1. 使用 GC.RegisterForFullGCNotification 方法,设置2个触发用的阈值
  2. 轮询的方式使用 GC.WaitForFullGCApproach 方法,你可以一直等待,或者配置超时返回值
  3. 如果 WaitForFullGCApproach 返回Success,请将程序的状态设置为可以进行完整GC状态(例如:暂停请求处理)
  4. 使用 GC.Collect 方法强制进行回收
  5. 调用 GC.WaitForFullGCComplete(可传入超时时间) 方法,等待GC完成。
  6. 重新打开对外的访问请求
  7. 如果你不再需要收到GC的通知,可以使用 GC.CancelFullGCNotification 方法进行取消。

因为通知需要一个轮询的机制,你需要有一个线程定期的检查状态。如果你的程序里已经有这样的定时检查功能,你可以将它嵌入到检查流程里。当然也可以单独为GC检查创建一个独立的线程。

下面的是一个 GCNotification 的完整例子。它会不断的分配内存用来测试通知过程。

internal class Program
{
private static void Main(string[] args)
{
const int ArrSize = 1024;
var arrays = new List<byte[]>();
GC.RegisterForFullGCNotification(25, 25);
// Start a separate thread to wait for GC notifications
Task.Run(() => WaitForGCThread(null));
Console.WriteLine("Press any key to exit");
while (!Console.KeyAvailable)
{
try
{
arrays.Add(new byte[ArrSize]);
}
catch (OutOfMemoryException)
{
Console.WriteLine("OutOfMemoryException!");
arrays.Clear();
}
}
GC.CancelFullGCNotification();
} private static void WaitForGCThread(object arg)
{
const int MaxWaitMs = 10000;
while (true)
{
// There is also an overload of WaitForFullGCApproach
// that waits indefinitely
GCNotificationStatus status = GC.WaitForFullGCApproach(MaxWaitMs);
bool didCollect = false;
switch (status)
{
case GCNotificationStatus.Succeeded:
Console.WriteLine("GC approaching!");
Console.WriteLine("-- redirect processing to another machine -- ");
didCollect = true;
GC.Collect();
break;
case GCNotificationStatus.Canceled:
Console.WriteLine("GC Notification was canceled");
break;
case GCNotificationStatus.Timeout:
Console.WriteLine("GC notification timed out");
break;
}
if (didCollect)
{
do
{
status = GC.WaitForFullGCComplete(MaxWaitMs);
switch (status)
{
case GCNotificationStatus.Succeeded:
Console.WriteLine("GC completed");
Console.WriteLine("-- accept processing on this machine again --");
break;
case GCNotificationStatus.Canceled:
Console.WriteLine("GC Notification was canceled");
break;
case GCNotificationStatus.Timeout:
Console.WriteLine("GC completion notification timed out");
break;
}
// Looping isn't necessary, but it's useful if you want
// to check other state before waiting again.
} while (status == GCNotificationStatus.Timeout);
}
}
}
}

另外一种触发方式是压缩LOH堆,但是基于内存使用触发更合适一些。

使用弱引用缓存对象

被弱引用对象引用的对象时可以在GC的时候被回收的。这与强引用形成对别,强引用后的对象是不会被回收的。弱引用主要用来缓存你想保留的不是很重要的对象,一旦应用有内存上的压力,就有可能被回收。

WeakReference weakRef = new WeakReference(myExpensiveObject);

// Create a strong reference to the object,
// now no longer eligible for GC
var myObject = weakRef.Target;
if (myObject != null)
{
myObject.DoSomethingAwesome();
}

[翻译] 编写高性能 .NET 代码--第二章 GC -- 减少大对象堆的碎片,在某些情况下强制执行完整GC,按需压缩大对象堆,在GC前收到消息通知,使用弱引用缓存对象的更多相关文章

  1. [翻译] 编写高性能 .NET 代码--第二章 GC -- 将长生命周期对象和大对象池化

    将长生命周期对象和大对象池化 请记住最开始说的原则:对象要么立即回收要么一直存在.它们要么在0代被回收,要么在2代里一直存在.有些对象本质是静态的,生命周期从它们被创建开始,到程序停止才会结束.其它对 ...

  2. [翻译] 编写高性能 .NET 代码--第二章 GC -- 减少分配率, 最重要的规则,缩短对象的生命周期,减少对象层次的深度,减少对象之间的引用,避免钉住对象(Pinning)

    减少分配率 这个几乎不用解释,减少了内存的使用量,自然就减少GC回收时的压力,同时降低了内存碎片与CPU的使用量.你可以用一些方法来达到这一目的,但它可能会与其它设计相冲突. 你需要在设计对象时仔细检 ...

  3. [翻译] 编写高性能 .NET 代码--第二章 GC -- 配置选项

    配置选项 在基于"less rope to hang yourself with"思想下,.NET 框架没有给开发提供很多太多的配置选项.但在大多数情况下,GC会跟你的硬件配置,及 ...

  4. [翻译] 编写高性能 .NET 代码--第二章 GC -- 避免使用终结器,避免大对象,避免复制缓冲区

    避免使用终结器 如果没有必要,是不需要实现一个终结器(Finalizer).终结器的代码主要是让GC回收非托管资源用.它会在GC完成标记对象为可回收后,放入一个终结器队列里,在由另外一个线程执行队列里 ...

  5. [翻译]编写高性能 .NET 代码 第二章:垃圾回收

    返回目录 第二章:垃圾回收 垃圾回收是你开发工作中要了解的最重要的事情.它是造成性能问题里最显著的原因,但只要你保持持续的关注(代码审查,监控数据)就可以很快修复这些问题.我这里说的"显著的 ...

  6. [翻译]编写高性能 .NET 代码 第二章:垃圾回收 基本操作

    返回目录 基本操作 垃圾回收的算法细节还在不断完善中,性能还会有进一步的提升.下文介绍的内容在不同的.NET版本里会略有不同,但大方向是不会有变动的. 在.net进程里会管理2个类型的内存堆:托管和非 ...

  7. [翻译]编写高性能 .NET 代码 第一章:性能测试与工具 -- 平均值 vs 百分比

    <<返回目录 平均值 vs 百分比 在考虑要性能测试的目标值时,我们需要考虑用什么统计口径.大多数人都会首选平均值,但在大多数情况下,这个正确的,但你也应该适当的考虑百分数.但你有可用性的 ...

  8. [翻译]编写高性能 .NET 代码 第一章:工具介绍 -- Visual Studio

    <<返回目录 Visual Studio vs虽然不是全宇宙唯一的IDE,但它是.net开发人员最常用的开发工具.它自带一个性能分析工具,你可以使用它来做开发,不同的vs版本在工具上会略有 ...

  9. [翻译]编写高性能 .NET 代码 第一章:工具介绍 -- Performance Counters(性能计数器)

    <<返回目录 Performance Counters(性能计数器) 性能计数器是监视应用程序和系统性能的最简单的方法之一.它有几十个类别数百个计数器在,包括一些.net特有的计数器.要访 ...

随机推荐

  1. Failed to load the JNI library "E:\JDK6.0\bin\client\jvm.dll"

    在打开Eclipse是错误提示:Failed to load the JNI library "E:\JDK6.0\bin\client\jvm.dll" 如图1所示 图1 遇到这 ...

  2. node.js核心模块

    全局对象 global 是全局变量的宿主 全局变量 在最外层定义的 全局对象的属性 隐士定义的变量(未定义直接赋值的变量) 当定义一个全局变量时 这个变量同时也会成为全局对象的属性 反之亦然 注意: ...

  3. JavaScript ES6 Arrow Functions(箭头函数)

    1. 介绍 第一眼看到ES6新增加的 arrow function 时,感觉非常像 lambda 表达式. 那么arrow function是干什么的呢?可以看作为匿名函数的简写方式. 如: var ...

  4. rpm包

    rpm包有什么命名规则与依赖? 命令规则: 包名-版本号.发布次数-linux平台.l.硬件平台.rpm 依赖: 树型依赖:a --> b --> c 安装a包需要安装b包,安装b包需要安 ...

  5. 流API--缩减操作

    在Stream流操作中,比如说min(),max(),count()方法,这几个操作都会将一个流缩减成一个值,流API将这些操作称为特例缩减.另外,流API同时泛华了缩减这种概念,提供了reduce( ...

  6. CSS--使用方式

    创建CSS有三种方式: 外部样式表, 内部样式表和内联样式. 外部样式表 先建立外部样式表文件(.css),然后在网页文件的<head>内使用<link>链接.这种方式将样式文 ...

  7. Asp.net core 2.0.1 Razor 的使用学习笔记(三)

    ASP.net core 2.0.0 中 asp.net identity 2.0.0 的基本使用(二)—用户账户及cookie配置 修改用户账户及cookie配置 一.修改密码强度和用户邮箱验证规则 ...

  8. jdbc参数

    JDBC连接池参数:    jdbc.initialSize=0       //初始化连接    jdbc.maxActive=30     //连接池的最大数据库连接数,设为0表示无限制    j ...

  9. ulua c#调用lua中模拟的类成员函数

    项目使用ulua,我神烦这个东西.lua单纯在lua环境使用还好,一旦要跟外界交互,各种月经不调就来了.要记住贼多的细节,你才能稍微处理好.一个破栈,pop来push去,位置一会在-1,一会在-3,2 ...

  10. KVM详情

    KVM介绍 Kernel-based Virtual Machine的简称,是一个开源的系统虚拟化模块,自Linux 2.6.20之后集成在Linux的各个主要发行版本中.它使用Linux自身的调度器 ...