我们遇到了与@Grzenio 类似的问题,但是我们正在使用更大的二维数组,按 1000x1000 到 3000x3000 的顺序,这是在 web 服务中。
添加更多内存并不总是正确的答案,您必须了解您的代码和用例。如果没有 GC 收集,我们需要 16-32gb 的内存(取决于客户大小)。如果没有它,我们将需要 32-64gb 的内存,即使这样也不能保证系统不会受到影响。.NET 垃圾收集器并不完美。
我们的 Web 服务有一个内存缓存,大小为 5-5000 万个字符串(每个键/值对约 80-140 个字符,具体取决于配置),此外,对于每个客户端请求,我们将构建 2 个矩阵,一个是双精度,一个是布尔值,然后传递给另一个服务来完成工作。对于 1000x1000 “矩阵”(二维数组),每个请求约为 25mb 。布尔值会说明我们需要哪些元素(基于我们的缓存)。每个缓存条目代表“矩阵”中的一个“单元”。
当服务器由于分页而具有 > 80% 的内存利用率时,缓存性能会显着降低。
我们发现,除非我们明确地 GC,.net 垃圾收集器永远不会“清理”临时变量,直到我们处于 90-95% 的范围内,此时缓存性能已急剧下降。
由于下游过程通常需要很长时间(3-900 秒),GC 收集的性能影响可以忽略不计(每次收集 3-10 秒)。在我们已经将响应返回给客户端后,我们启动了此收集。
最终,我们使 GC 参数可配置,在 .net 4.6 中还有更多选项。这是我们使用的 .net 4.5 代码。
if (sinceLastGC.Minutes > Service.g_GCMinutes)
{
Service.g_LastGCTime = DateTime.Now;
var sw = Stopwatch.StartNew();
long memBefore = System.GC.GetTotalMemory(false);
context.Response.Flush();
context.ApplicationInstance.CompleteRequest();
System.GC.Collect( Service.g_GCGeneration, Service.g_GCForced ? System.GCCollectionMode.Forced : System.GCCollectionMode.Optimized);
System.GC.WaitForPendingFinalizers();
long memAfter = System.GC.GetTotalMemory(true);
var elapsed = sw.ElapsedMilliseconds;
Log.Info(string.Format("GC starts with {0} bytes, ends with {1} bytes, GC time {2} (ms)", memBefore, memAfter, elapsed));
}
在使用 .net 4.6 重写后,我们将垃圾收集分为两个步骤 - 一个简单的收集和一个压缩收集。
public static RunGC(GCParameters param = null)
{
lock (GCLock)
{
var theParams = param ?? GCParams;
var sw = Stopwatch.StartNew();
var timestamp = DateTime.Now;
long memBefore = GC.GetTotalMemory(false);
GC.Collect(theParams.Generation, theParams.Mode, theParams.Blocking, theParams.Compacting);
GC.WaitForPendingFinalizers();
//GC.Collect(); // may need to collect dead objects created by the finalizers
var elapsed = sw.ElapsedMilliseconds;
long memAfter = GC.GetTotalMemory(true);
Log.Info($"GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");
}
}
// https://msdn.microsoft.com/en-us/library/system.runtime.gcsettings.largeobjectheapcompactionmode.aspx
public static RunCompactingGC()
{
lock (CompactingGCLock)
{
var sw = Stopwatch.StartNew();
var timestamp = DateTime.Now;
long memBefore = GC.GetTotalMemory(false);
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
var elapsed = sw.ElapsedMilliseconds;
long memAfter = GC.GetTotalMemory(true);
Log.Info($"Compacting GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");
}
}
希望这对其他人有帮助,因为我们花了很多时间研究这个。
[编辑] 在此之后,我们发现大型矩阵存在一些额外的问题。我们已经开始遇到沉重的内存压力并且应用程序突然无法分配数组,即使进程/服务器有足够的内存(24gb 可用)。经过深入调查,我们发现该进程的备用内存几乎是“正在使用的内存”的 100%(24gb 正在使用,24gb 备用,1gb 空闲)。当“空闲”内存达到 0 时,应用程序将暂停 10 多秒,而待机被重新分配为空闲,然后它可以开始响应请求。
根据我们的研究,这似乎是由于大对象堆的碎片造成的。
为了解决这个问题,我们采取了两种方法:
- We are going to change to jagged array vs multi-dimensional arrays. This will reduce the amount of continuous memory required, and ideally keep more of these arrays out of the Large Object Heap.
- We are going to implement the arrays using the ArrayPool class.