浅谈.NET垃圾回收

本篇博客简单地从以下几个方面讨论一下.net的垃圾回收(GC)问题

  1. 何时进行垃圾回收
  2. 回收哪些对象
  3. 回收过程和回收算法
  4. 编程建议

何时进行垃圾回收

一般情况下,.net在创建对象时发现托管堆的内存不够用了,就会进行垃圾回收。除此之外,手动调用GCCollect()方法时也会进行回收,还有就是当windows报告低内存时以及应用程序关闭时,也都会进行垃圾回收。

回收哪些对象

.net采用了引用跟踪算法(也有人叫可达性分析算法)来判断哪些对象需要被回收,简单来说就是,在当前作用域内可以被访问到对象不能被回收,这些对象中所包含的所有对象也要保留,剩下的就是垃圾了,都会被回收掉。

为了说明白这个问题,我们来看一点简单的代码。

class Program
{
    static void Main(string[] args)
    {
        PrintStudents();
        //other code......
    }

    static void PrintStudents()
    {
        List<Student> studentList = GetStudentList();
        foreach(Student item in studentList){
            Console.WriteLine("student name = {0}, id = {1},schoolName = {2}"
                              ,item.StudentName,item.StudentId.ToString(),item.InSchool.SchoolName);
        }
    }

    /// <summary>
    /// 创建一个List对象并返回
    /// </summary>
    /// <returns>The student list.</returns>
    static List<Student> GetStudentList()
    {
        List<Student> students = new List<Student>();
        for (int i = 0; i < 100; i++)
        {
            Student stu = new Student()
            {
                StudentId = i + 1,
                StudentName = "student" + (i + 1).ToString()
            };
            School sch = new School()
            {
                SchoolId = i + 1,
                SchoolName = "school" + (i + 1).ToString()
            };
            stu.InSchool = sch;
            students.Add(stu);
        }
        return students;
    }
}

public class Student
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }
    public School InSchool { get; set; }
}

public class School
{
    public int SchoolId { get; set; }
    public string SchoolName { get; set; }
}

Main()方法中调用了PrintStudents()PrintStudents()又调用了GetStudentList()方法来获取一个数据列表,当GetStudentList()方法创建了一个List返回后,这个List对象在PrintStudents()方法中被赋值给了studentList变量,所以,此时的List是可以被访问到的,是可达的,就不能被回收,并且,List中的每一个Student对象以及每一个Student对象关联的School对象都是可达的,他们的内存都不能被回收。程序继续运行,当PrintStudents()方法运行结束并返回到调用者Main()方法中时,这个List在当前作用域内(也就是Main()方法的作用域内)已经访问不到了,此时的List在托管堆上的内存空间已经被视为垃圾了,在下次GC时就会被回收掉。

回收过程和回收算法

.net clr开始GC时,会首先暂停进程中的所有线程,然后清空垃圾内存中的数据,并将可用的内存移动到一起以减少碎片化,之后再让原来的引用指向新内存地址,再激活线程,一次GC就完成了。对于线程来说,仿佛什么都没有发生过一样。

我举例说明一下这个内存空间的变化吧。假设在GC开始时,托管堆上一共有5块连续的内存,分别A、B、C、D、E,其中A、C、E被认为是垃圾,回收内存时,先把A、C、E的内存清空,然后把B和D放到一起,并且移动到原来A内存的开始位置,下一个对象的内存将会从D的结束位置开始分配。

我描述的如此简单,但真实的GC过程非常的复杂。为了提升性能,.net clr采用了代(generation)的概念,将托管堆分为三代:第0代,第1代和第2代。下面我简单地描述一个更复杂的GC过程。

.net clr初始化时,会为第0代,第1代和第2代分别设置一个预算空间。新创建的对象总是出现在第0代,当创建新对象时发现第0代的内存已经达到了预算,没有更多的空间来创建新对象了,就会开始回收内存。根据引用跟踪算法,将不可达的内存清空,将可达的内存整理到一起。这些可达的内存由于经历了一次GC而没有被回收掉,就会被提升至第一代。重复几次这个过程,第0代经历过几次GC后,更多的内存会被提升到第1代,当某次GC开始时发现第1代的预算用完了,就会回收第一代的内存,将第1代中仍然可达的内存提升至第2代。重复N次之后,当发现第2代的空间也达到了预算,则会回收第2代。

这就是代的用途。大量研究证明,对象越新,存活的时间越短,对象越老,存活的时间约长。所以,新分配的内存总是在第0代,在第0代进行GC时也会回收大量的内存,这样能够显著的提高GC的效率。

还有一个值得一提的点就是大对象的分配。由于大对象的内存分配和回收非常地费劲,所以,大对象会被直接提升到第2代,并且分配到独立的内存空间中,尽量与小对象保持独立。目前认为超过85000字节的对象是大对象。

编程建议

当.net clr试图创建新对象时发现内存不够用,会自动进行垃圾回收,回收之后如果还不够的话,就会回收第1代和第2代的内存,如果还不够,就会抛出OutOfMemoryException

关于编程建议,我给一个反面的例子吧,我先写一个引发OutOfMemoryException的代码,你避免这么写就ok了。

static List<object> objList = new List<object>();
static void Main(string[] args)
{
    long i = 0L;
    while(true)
    {
        objList.Add(new object());    
        Console.WriteLine(++i);
    }
}

上面hold一个List对象,然后拼命地往这个list中加对象,由于在当前作用域内objList一直都是可达的,所以list以及list中的所有项的内存都不能释放,直到把内存撑爆位置。

还有一点,要少用静态变量指向一个对象,因为静态的变量随时都可以访问到,它指向的内存永远不能被回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值