.NET:垃圾回收器何时运行?内存泄漏?

3
我知道什么是不可变性,以及String .NET类的特殊性。即使它是一个引用类型,不可变性使其表现得像一个值类型。明白了。C#参考文献强调了这一点(见string (C# Reference),我添加了强调):

字符串是不可变的——在对象被创建后,字符串对象的内容不能更改,尽管语法使其似乎可以这样做。例如,当您编写此代码时,编译器实际上会创建一个新的字符串对象来保存新的字符序列,并将该新对象分配给b。然后,字符串"h"就有资格进行垃圾回收。

作为一个自学成才的程序员,我对垃圾收集器、内存泄漏和指针等方面并不精通。这就是为什么我正在问一个关于它的问题。C#编译器如何自动创建新的字符串对象并放弃旧的对象的描述让人感觉似乎会消耗大量的内存来处理废弃的字符串内容。许多对象都有销毁方法或析构函数,以便即使是自动化的CLR垃圾收集器也知道何时和如何清理不再需要的对象。但对于String类型却没有这样的内容。我想看看如果创建并立即放弃字符串对象,会发生什么情况。
以下是程序:
class Program {
    static void Main(string[] args)
    {
        Console.ReadKey();
        int megaByte = (int)Math.Pow(1024, 2);
        string[] hog = new string[2048];
        char c;
        for (int i = 0; i < 2048; i++)
        {
            c = Convert.ToChar(i);
            Console.WriteLine("Generating iteration {0} (char = '{1}')", i, c);
            hog[i] = new string(c, megaByte);
            if ((i + 1) % 256 == 0) { 
                for (int j = (i - 255); j <= i; j++) { hog[j] = hog[i]; } }
            }
        Console.ReadKey();

        List<string> uniqueStrings = new List<string>();
        for (int i = 0; i < 2048; i++) {
            if (!uniqueStrings.Contains(hog[i])) { uniqueStrings.Add(hog[i]); }
        }
        Console.WriteLine("There are {0} unique strings in hog.", uniqueStrings.Count);
        Console.ReadKey();

        // Create a timer with an interval of 30 minutes 
        // (30 minutes * 60 seconds * 1000 milliseconds)
        System.Timers.Timer t = new System.Timers.Timer(30 * 60 * 1000);
        t.Elapsed += new System.Timers.ElapsedEventHandler(t_Elapsed);
        t.Start();
        Console.WriteLine("Waiting 30 minutes...");

        Console.ReadKey();
    }

    static void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        Console.WriteLine("Time's up. I'm collecting the garbage.");
        GC.Collect();
    }
}

它创建并销毁了大量唯一的字符串,但最终只留下8个唯一的字符串在hog数组中。在我的测试中,该进程仍然占用着570MB到1.1GB不等的内存。计时器部分等待30分钟,同时保持进程处于活动状态(不休眠),而在30分钟结束时,进程依然占用所有额外的内存,直到我强制进行垃圾回收。这使得 .NET 垃圾收集器似乎错过了某些东西。很多其他人说调用 GC.Collect() 是一件非常可怕的事情。因此,使用这种方法强制回收内存是唯一看起来可以恢复内存的方法,这仍然使问题看起来有些不对劲。


1
哇,这篇文章很长,但是没有什么实际意义。你的第一个“不可变”的示例是错误的。那只是改变了引用,与不可变性无关。 - John Saunders
它意味着垃圾回收是工作的,但不会在有需要之前运行。 - Paul Phillips
1
我认为你可以把这个问题简化成一个非常简短的问题:如果一个.NET进程首先使用了大量内存,然后释放对该内存的引用。GC需要多长时间才能回收内存?进程的内存大小会保持多长时间?它是否会被释放回操作系统?尝试提出这样一个简短的问题,而不是这个庞大的文本流。 - Anders Abel
我想知道你是否意识到 1024^21026 而不是 1048576 - Blindy
@pst: 我唯一觉得有趣的事情就是这几乎是OP学习GC工作的记录。 - John Saunders
显示剩余2条评论
3个回答

8
你的帖子有点冗长。简而言之,你在保留引用的同时分配了大量内存,然后发现即使这些引用不再被使用,甚至经过很长时间,GC也不会触发。
这意味着GC不是基于时间触发的,而只是基于发生的分配。一旦没有分配发生,它就不会运行。如果你重新开始分配,内存最终会降下来。
这与不可变性或特定字符串无关。

一个关于GC-字符串膨胀的臃肿问题。 :-) - Warren P
这与不可变性或特定的字符串没有任何关系。是的,特别是在C#语法中,字符串的不可变性引起了一个独特的情况,在分配字符串文字时没有“new”语句;当重新分配新值时,您已经失去了引用先前字符串对象的任何手段,即使您没有执行任何显式dispose()、设置为null或其他操作。 - Joshua Honig
这就是垃圾回收的工作方式。你为什么认为会有任何类型的内存泄漏呢? - John Saunders
你发的帖子有点太冗长了… 这个问题有点冗余… 哎呀。没有人强迫你阅读它。我只是知道,如果我不清楚地记录我是如何得出结论的,我会因为对CLR垃圾收集器提出了离谱的无根据的指责而受到攻击。 - Joshua Honig
1
@jmh 字符串字面量在这里有什么相关性?你正在使用 new 分配 2MB 的字符串。字符串字面量是内部化的,因此它们不会导致任何额外的分配,但是在你的程序中,唯一使用字符串字面量的地方是用于日志记录,在问题的上下文中不相关。如果使用可变数组,您的程序将展示完全相同的行为。 - CodesInChaos

4

不变性意味着

string a = "abc"; 
string b = a; 
a=a+"def";

创建一个新字符串包含"abcdef",并将其赋值给a。变量b的值不变,仍然引用包含"abc"的字符串。

我相信楼主已经知道这一点,但这个提醒可能对其他未来的读者有所帮助。 - Warren P
2
@WarrenP:通过他发布的那堵墙式的文字,你怎么能判断出原帖作者知道什么或不知道什么呢? - John Saunders
1
因为他说“我知道什么是不可变性”,所以告诉他他不知道什么是不可变性是不真诚的。他的帖子表明,他确实意识到在将B赋值给A后更改B的值不会更改A的值。 - Warren P
@WarrenP:我的回答是基于他最初的帖子,而不是编辑后的。他最初的帖子并没有尝试“更改字符串”,只是尝试分配一个不同的字符串。我的示例展示了尝试更改字符串,并显示创建了一个新字符串来保存更改。 - John Saunders
哦,是的。我脑子里又出现了时间机器问题。抱歉。 - Warren P

2

您的CLR版本的垃圾回收器只响应内存压力,而不是经过的时间。为什么要浪费宝贵的CPU时间清理根本不需要的内存呢?

当然,您可以争辩说在程序“空闲”时执行GC会更好。问题是,您希望尽可能简单(通常意味着快速)地保持GC算法,并且该算法无法读取您的思想。它不知道应用程序何时在“没有建设性的事情”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接