C# 内存分配(堆和栈)和内存回收

目录

一,引言

二,内存分配

2.1 栈(stack)

2.2 堆(heap) 

 2.3 引用类型嵌套值类型

2.4 值类型嵌套引用类型

2.4 string类型的内存分配

三,内存回收

3.1 托管资源

 3.2 非托管资源

3.2.1 IDisposable接口 

3.2.2 using语句

3.2.3 自定义Dispose和Finalize方法 


一,引言

C#代码在计算机系统中怎么执行的呢?主要通过一下几个部分:

 可以看到,C#语言代码经过编译器编译成中间语言IL(不同计算机操作系统编译的IL是一样的),然后不同操作系统的CLR将IL编译为机器码,最终被计算机执行。

CLR:即公共语言运行时(Common Language Runtime),是中间语言(IL)的运行时环境,负责将编译生成的MSIL编译成计算机可以识别的机器码,负责资源管理(内存分配和垃圾回收等)。

二,内存分配

内存分配是指程序运行时,进程占用的内存,由CLR负责分配。

  • 值类型:值类型是struct的,例如:int、datetime等。
  • 引用类型:即class,例如:类、接口,string等。

2.1 栈(stack)

栈:即线程栈,先进后出的一种数据结构,随着线程而分配。

  1. 值类型分配在线程栈上面,变量和值都是在线程栈上面。
  2. 值类型可以先声明变量而不用初始化。

C# 内存分配(堆和栈)和内存回收_第1张图片

 且看一个例子:(定义一个结构体)

public struct ValuePoint
{
    public int x;
    public ValuePoint(int x)
    {
         this.x = x;
    }
}

 在方法里面调用: 

//先声明变量,没有初始化  但是我可以正常赋值  跟类不同
ValuePoint valuePoint;
valuePoint.x = 123;

ValuePoint point = new ValuePoint();
Console.WriteLine(valuePoint.x);

内存分配情况如下图所示(先定义的在下边,后定义的在上边): 

C# 内存分配(堆和栈)和内存回收_第2张图片

2.2 堆(heap) 

堆:即对象堆,是进程中独立划出来的一块内存,有时一些对象需要长期使用不释放、对象的重用,这些对象就需要放到堆上。

  1. 引用类型实例化的时候,会在堆中开辟一部分空间存储类的实例。类对象的引用(地址)还是存储在栈中。
  2. 引用类型分配内存的步骤:

        (1)new的时候去对象堆里面开辟一块内存,分配一个内存地址。

        (2)调用构造函数(因为在构造函数里面可以使用this),这时才执行构造函数。

        (3)把地址引用传给栈上面的变量。

再看一个例子:(定义一个类)

public class ReferencePoint
{
     public int x;
     public ReferencePoint(int x)
     {
           this.x = x;
     }
}

  在方法里面调用: 

ReferencePoint referencePoint = new ReferencePoint(123);
Console.WriteLine(referencePoint.x);

 其内存分配如下:

C# 内存分配(堆和栈)和内存回收_第3张图片

 2.3 引用类型嵌套值类型

public class ReferenceTypeClass
{
        //值类型属性
        private int _valueTypeField;
        public ReferenceTypeClass()
        {
            _valueTypeField = 0;
        }
        public void Method()
        {
            //局部值类型
            int valueTypeLocalVariable = 0;
        }
}

可以看到,在一个引用类型里面定义了一个值类型的属性:_valueTypeField和一个值类型的局部变量:valueTypeLocalVariable,那么这两个值类型是如何进行内存分配的呢?其内存分配如下:

  • 属性_valueTypeField分配在了上,这是由于引用类型是在堆上面分配了一整块内存,引用类型里面的属性也是在堆上面分配内存。
  • 局部变量valueTypeLocalVariable分配在了上,这是由于valueTypeLocalVariable是一个全新的局部变量,调用方法的时候,会启用一个线程栈来调用方法,然后把局部变量分配到栈上面。

2.4 值类型嵌套引用类型

public struct ValueTypeStruct
{
        //引用类型属性
        private object _referenceTypeField;
        public ValueTypeStruct(int x)
        {
            _referenceTypeField = new object();
        }
        public void Method()
        {
           //引用类型局部变量
            object referenceTypeLocalVariable = new object();
        }
}

 其内存分配如下:

C# 内存分配(堆和栈)和内存回收_第4张图片

可以看到,两个引用类型 _referenceTypeFieldreferenceTypeLocalVariable 的引用类型变量(地址)分配在栈上,值分配在堆上。

2.4 string类型的内存分配

首先要明确一点:string是引用类型。

按照引用类型内存分配,先声明一个string类型变量student,然后再声明一个变量student2,然后用student给student2赋值,内存分配如下:

C# 内存分配(堆和栈)和内存回收_第5张图片

可以看到,引用类型对象复制,只复制引用的地址(浅拷贝),但是当修改student2变量的值后:

student2 = "App";

 可以看到结果:

这是因为string字符串的不可变性造成的。一个string变量一旦声明并初始化以后,其在堆上面分配的值就不会改变了。这时修改student2的值,并不会去修改堆上面分配的值,而是重新在堆上面开辟一块内存来存放student2修改后的值。

这也是string类型区别于其他引用类型的地方(其他引用类型,当复制(赋值)时,只会把栈的地址传递过去,也就是说两个变量共用一个地址,当一个变量改变值时,另一个变量也会改变。

三,内存回收

  • 值类型存放在线程栈上,线程栈是每次调用都会产生,用完自己就会释放。
  • 引用类型存放在堆上面,全局共享一个堆,空间有限,所以才需要垃圾回收。

 CLR在堆上面是连续分配内存的。在C#中内存资源主要分为托管资源非托管资源

3.1 托管资源

由CLR管理的存在于托管堆上的称为托管资源,注意这里有2个关键点,第一是由CLR管理,

第二存在于托管堆上。托管资源的回收工作是不需要人工干预的,CLR会在合适的时候调用GC(垃圾回收器)进行回收。

(1)垃圾回收期(GC)

定期或在内存不够时,通过销毁不再需要或不再被引用的对象,来释放内存,是CLR的一个重要组件。垃圾回收器销毁对象的两个条件

  1. 对象不再被引用----设置对象=null。
  2. 对象在销毁器列表中没有被标记。

(2)垃圾回收发生时机

垃圾回收发生在new的时候,new一个对象时,会在堆中开辟一块内存,这时会查看内存空间是否充足,如果内存空间不够,则进行垃圾回收。程序退出的时候也会进行垃圾回收。

(3)垃圾回收机制

GC定期检查对象是否未被引用,如果对象没有被引用,则在检查销毁器列表。若在销毁器列表中没有标记,则立即回收。若在销毁器列表中有标记,则开启销毁器线程,由该线程调用析构函数,析构函数执行完,删除销毁器列表中的标记。

注意:

不建议写析构函数,原因如下:

  1)对象即使不用,也会在内存中驻留很长一段时间。

  2)销毁器线程为单独的线程,非常耗费资源。

(4)垃圾回收(GC)中的代

  1. 首次GC前 全部对象都是0代。
  2. 第一次GC后,还保留的对象叫1代。这时新创建的对象就是0代。
  3. 垃圾回收时,先查找0代对象,如果空间还不够,再去找1代对象,这之后,还存在的一代对象就变成2代,0代对象就变成一代对象。
  4. 垃圾回收时如果0~2代都不够,那么就内存溢出了。

越是最近分配的,越是会被回收。因为最近分配的都是0级对象,每次垃圾回收时都是先查询0级对象。

 3.2 非托管资源

非托管资源是不由CLR管理,例如:Image Socket, StreamWriter, Timer, Tooltip, 文件句柄, GDI资源, 数据库连接等等资源(这里仅仅列举出几个常用的)。这些资源GC是不会自动回收的,需要手动释放。

在定义一个类时,可以使用三种方法来自动释放非托管的资源。这些方法常常放在一起实现,因为每种机制都为问题提供了略为不同的解决方法。重点说一说第二,第三种方法。

(1)声明一个析构函数(或终结器),作为类的一个成员。不推荐

(2)在类中实现System.IDisposable接口。推荐

(3)使用using语句。推荐

3.2.1 IDisposable接口 

在C#中,推荐使用System.IDisposable接口替代析构函数。IDisposable接口定义了一种模式,该模式为释放非托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾回收器相关的问题。IDisposable接口声明了一个Dispose()方法,它不带参数,返回void。例如:

public class People : IDisposable
{
        public void Dispose()
        {
            this.Dispose();
        }
}

Dispose()方法的实现代码显式地释放由对象直接使用的所有非托管资源,并在所有也实现了IDisposable接口的封装对象上调用Dispose()方法。这样,Dispose()方法为何时释放非托管资源提供了精确的控制。

3.2.2 using语句

 C#提供了一种语法,可以确保在实现了IDisposable接口的对象的引用超出作用域时,在该对象上自动调用Dispose()方法。该语法使用了using关键字来完成此工作。例如:

 using (var people = new People())
 {
       // 要处理的代码
 }

3.2.3 自定义Dispose和Finalize方法 

虽然不推荐采用析构方法Finalize,但确实比Dispose方法更加安全,因为它由CLR保证调用,但是性能方面Finalize方法却要差的多。

正确的类型设计是:把Finalize方法作为Dispose方法的后备,只有在使用者没有调用Dispose方法的情况下,Finalize方法才能被视为需要执行。下面是一个正确高效的设计模板,建议牢记这个模板并且套用到每一个需要DIspose和Finalize方法的类型上去。

using System;

namespace usingDemo
{
    public class FinalizeDisposeBase : IDisposable
    {
        // 标记对象是否已被释放
        private bool _disposed = false;
        // Finalize方法
        ~FinalizeDisposeBase()
        {
            Dispose(false);
        }

        /// 
        /// 这里实现了IDisposable中的Dispose方法
        /// 
        public void Dispose()
        {
            Dispose(true);
            // 告诉GC此对象的Finalize方法不再需要调用
            GC.SuppressFinalize(true);
        }

        /// 
        /// 在这里做实际的析构工作
        /// 声明为虚方法以供子类在必要时重写
        /// 
        /// 
        protected virtual void Dispose(bool isDisposing)
        {
            // 当对象已经被析构时,不在执行
            if(_disposed)
            {
                return;
            }
            if(isDisposing)
            {
                // 在这里释放托管资源
                // 只在用户调用Dispose方法时执行
            }
            // 在这里释放非托管资源
            // 标记对象已被释放
            _disposed = true;
        }
    }

    public sealed class FinalizeDispose:FinalizeDisposeBase
    {
        private bool _mydisposed = false;
        protected override void Dispose(bool isDisposing)
        {
            // 保证只释放一次
            if (_mydisposed)
            {
                return;
            }
            if(isDisposing)
            {
                // 在这里释放托管的并且在这个类型中声明的资源
            }
            // 在这里释放非托管的并且在这个类型中声明的资源
            // 调用父类的Dispose方法来释放父类中的资源
            base.Dispose(isDisposing);
            // 设置子类的标记
            _mydisposed = true;
        }
        static void Main()
        {

        }
    }
}

 上面的代码是一个近乎完美的Dispose配合Finalize的设计模板,其中有几点需要特别注意:

  • 真正做释放工作的只是Virtual的受保护方法Dispose方法,事实上这个方法的名字并不重要,仅仅为了通用和更好理解,称呼它为Dispose。
  • 虚方法Dispose需要接受一个布尔类型的参数,主要用于区分调用方是类型的使用者还是.NET的垃圾回收。前者通过IDisposable的Dispose方法,而后者通过Finalize方法。两者的区别是通过Finalize方法释放资源时不能再释放或使用对象中的托管资源,这是因为这时的对象已经处于不被使用的状态,很有可能其中的托管资源已经被释放掉了。
  • 在IDisposable的Dispose方法的实现中通过GC.SuppressFinalize()方法来告诉.NET此对象在被回收时不需要调用Finalize方法,这一句是改善性能的关键,记住实现Dispose方法的本质目的就是避免所有释放工作在Finalize方法中进行。
  • 子类型必须定义自己的释放标记来标明子类中的资源是否已经被释放,同时子类的虚方法Dispose方法也只需要释放自己新定义的资源。
  • 确保在虚方法Dispose中做的都是释放工作,有些逻辑上的结束工作需要反复斟酌,以防止一个简单的赋值语句使对象再度存活。

你可能感兴趣的:(#,C#基础篇,c#)