在测试应用程序性能时,我遇到了一些非常奇怪的GC行为。简而言之,GC甚至在没有运行时分配的空程序上运行!
以下应用程序演示了此问题:
using System;
using System.Collections.Generic;
public class Program
{
// Preallocate strings to avoid runtime allocations.
static readonly List<string> Integers = new List<string>();
static int StartingCollections0, StartingCollections1, StartingCollections2;
static Program()
{
for (int i = 0; i < 1000000; i++)
Integers.Add(i.ToString());
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
static void Main(string[] args)
{
DateTime start = DateTime.Now;
int i = 0;
Console.WriteLine("Test 1");
StartingCollections0 = GC.CollectionCount(0);
StartingCollections1 = GC.CollectionCount(1);
StartingCollections2 = GC.CollectionCount(2);
while (true)
{
if (++i >= Integers.Count)
{
Console.WriteLine();
break;
}
// 1st test - no collections!
{
if (i % 50000 == 0)
{
PrintCollections();
Console.Write(" - ");
Console.WriteLine(Integers[i]);
//System.Threading.Thread.Sleep(100);
// or a busy wait (run in debug mode)
for (int j = 0; j < 50000000; j++)
{ }
}
}
}
i = 0;
Console.WriteLine("Test 2");
StartingCollections0 = GC.CollectionCount(0);
StartingCollections1 = GC.CollectionCount(1);
StartingCollections2 = GC.CollectionCount(2);
while (true)
{
if (++i >= Integers.Count)
{
Console.WriteLine("Press any key to continue...");
Console.ReadKey(true);
return;
}
DateTime now = DateTime.Now;
TimeSpan span = now.Subtract(start);
double seconds = span.TotalSeconds;
// 2nd test - several collections
if (seconds >= 0.1)
{
PrintCollections();
Console.Write(" - ");
Console.WriteLine(Integers[i]);
start = now;
}
}
}
static void PrintCollections()
{
Console.Write(Integers[GC.CollectionCount(0) - StartingCollections0]);
Console.Write("|");
Console.Write(Integers[GC.CollectionCount(1) - StartingCollections1]);
Console.Write("|");
Console.Write(Integers[GC.CollectionCount(2) - StartingCollections2]);
}
}
有人可以解释这里发生了什么吗?我的印象是,除非内存压力达到特定限制,否则GC不会运行。但是,它似乎一直在运行(并收集) - 这是正常的吗?
编辑:我修改了程序以避免所有运行时分配。
编辑2:好的,新的迭代,似乎DateTime是罪魁祸首。其中一个DateTime方法分配内存(可能是Subtract),这会导致GC运行。第一个测试现在导致绝对没有集合 - 正如预期的那样 - 而第二个导致几个集合。
简而言之,GC只在需要运行时运行 - 我只是在不知不觉中产生内存压力(DateTime是一个结构,我认为它不会产生垃圾)。
答案 0 :(得分:5)
GC.CollectionCount(0)
返回以下内容:
自进程启动以来指定代的垃圾收集发生的次数。
因此,您应该看到数字增加,并且这种增加并不意味着内存泄漏但GC已经运行。
同样在第一种情况下,您可以看到这种增加。它会发生得慢得多,因为非常慢的Console.WriteLine
方法被更频繁地调用,减慢了很多东西。
答案 1 :(得分:3)
此处应该注意的另一件事是GC.Collect()
不是同步函数调用。它触发垃圾收集,但垃圾收集发生在后台线程上,理论上,当你到处检查GC
统计信息时,它可能还没有完成运行。
在GC.WaitForPendingFinalizers
之后可以进行GC.Collect
调用,直到垃圾收集发生为止。
如果您真的想在不同情况下尝试准确跟踪GC统计信息,我会在您的流程中使用Windows性能监视器,您可以在其中创建各种事物的监视器,包括.NET堆统计信息。
答案 2 :(得分:1)
如果您只是等待几秒钟,您会发现第一次测试中的收集计数也会增加,但速度不会很快。
代码之间的差异在于第一个测试会尽可能快地写出集合计数,而第二个测试在没有写入任何内容的情况下循环,直到达到时间限制。
第一个测试花费大部分时间等待文本写入控制台,而第二个测试花费大部分时间循环,等待时间限制。第二次测试将在同一时间内进行更多的迭代。
我计算了迭代次数,并打印出每次垃圾回收的迭代次数。在我的计算机上,第一次测试稳定了每个GC大约45000次迭代,而第二次测试稳定了每个GC大约130000次迭代。
所以,第一次测试实际上比第二次测试的更多垃圾收集,大约是第三次测试。
答案 3 :(得分:0)
谢谢大家!您的建议有助于揭示罪魁祸首:DateTime
正在分配堆内存。
GC不会一直运行,只有在分配内存时才会运行。如果内存使用量持平,GC
将永远不会运行,GC.CollectionCount(0)
将始终按预期返回0
。
测试的最新版本展示了这种行为。第一次测试运行不分配任何堆内存(GC.CollectionCount(0)
仍为0
),而第二次测试运行以非显而易见的方式分配内存:通过DateTime.Subtract()
- &gt; Timespan
。
现在,DateTime
和Timespan
都是值类型,这就是我发现这种行为令人惊讶的原因。不过,你有它:毕竟存在内存泄漏。