目录
一,引言
二,内存分配
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等。
栈:即线程栈,先进后出的一种数据结构,随着线程而分配。
- 值类型分配在线程栈上面,变量和值都是在线程栈上面。
- 值类型可以先声明变量而不用初始化。
且看一个例子:(定义一个结构体)
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);
内存分配情况如下图所示(先定义的在下边,后定义的在上边):
堆:即对象堆,是进程中独立划出来的一块内存,有时一些对象需要长期使用不释放、对象的重用,这些对象就需要放到堆上。
- 引用类型实例化的时候,会在堆中开辟一部分空间存储类的实例。类对象的引用(地址)还是存储在栈中。
- 引用类型分配内存的步骤:
(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);
其内存分配如下:
public class ReferenceTypeClass
{
//值类型属性
private int _valueTypeField;
public ReferenceTypeClass()
{
_valueTypeField = 0;
}
public void Method()
{
//局部值类型
int valueTypeLocalVariable = 0;
}
}
可以看到,在一个引用类型里面定义了一个值类型的属性:_valueTypeField和一个值类型的局部变量:valueTypeLocalVariable,那么这两个值类型是如何进行内存分配的呢?其内存分配如下:
public struct ValueTypeStruct
{
//引用类型属性
private object _referenceTypeField;
public ValueTypeStruct(int x)
{
_referenceTypeField = new object();
}
public void Method()
{
//引用类型局部变量
object referenceTypeLocalVariable = new object();
}
}
其内存分配如下:
可以看到,两个引用类型 _referenceTypeField和referenceTypeLocalVariable 的引用类型变量(地址)分配在栈上,值分配在堆上。
首先要明确一点:string是引用类型。
按照引用类型内存分配,先声明一个string类型变量student,然后再声明一个变量student2,然后用student给student2赋值,内存分配如下:
可以看到,引用类型对象复制,只复制引用的地址(浅拷贝),但是当修改student2变量的值后:
student2 = "App";
可以看到结果:
这是因为string字符串的不可变性造成的。一个string变量一旦声明并初始化以后,其在堆上面分配的值就不会改变了。这时修改student2的值,并不会去修改堆上面分配的值,而是重新在堆上面开辟一块内存来存放student2修改后的值。
这也是string类型区别于其他引用类型的地方(其他引用类型,当复制(赋值)时,只会把栈的地址传递过去,也就是说两个变量共用一个地址,当一个变量改变值时,另一个变量也会改变。
- 值类型存放在线程栈上,线程栈是每次调用都会产生,用完自己就会释放。
- 引用类型存放在堆上面,全局共享一个堆,空间有限,所以才需要垃圾回收。
CLR在堆上面是连续分配内存的。在C#中内存资源主要分为托管资源和非托管资源。
由CLR管理的存在于托管堆上的称为托管资源,注意这里有2个关键点,第一是由CLR管理,
第二存在于托管堆上。托管资源的回收工作是不需要人工干预的,CLR会在合适的时候调用GC(垃圾回收器)进行回收。
(1)垃圾回收期(GC)
定期或在内存不够时,通过销毁不再需要或不再被引用的对象,来释放内存,是CLR的一个重要组件。垃圾回收器销毁对象的两个条件
(2)垃圾回收发生时机
垃圾回收发生在new的时候,new一个对象时,会在堆中开辟一块内存,这时会查看内存空间是否充足,如果内存空间不够,则进行垃圾回收。程序退出的时候也会进行垃圾回收。
(3)垃圾回收机制
GC定期检查对象是否未被引用,如果对象没有被引用,则在检查销毁器列表。若在销毁器列表中没有标记,则立即回收。若在销毁器列表中有标记,则开启销毁器线程,由该线程调用析构函数,析构函数执行完,删除销毁器列表中的标记。
注意:
不建议写析构函数,原因如下:
1)对象即使不用,也会在内存中驻留很长一段时间。
2)销毁器线程为单独的线程,非常耗费资源。
(4)垃圾回收(GC)中的代
越是最近分配的,越是会被回收。因为最近分配的都是0级对象,每次垃圾回收时都是先查询0级对象。
非托管资源是不由CLR管理,例如:Image Socket, StreamWriter, Timer, Tooltip, 文件句柄, GDI资源, 数据库连接等等资源(这里仅仅列举出几个常用的)。这些资源GC是不会自动回收的,需要手动释放。
在定义一个类时,可以使用三种方法来自动释放非托管的资源。这些方法常常放在一起实现,因为每种机制都为问题提供了略为不同的解决方法。重点说一说第二,第三种方法。
(1)声明一个析构函数(或终结器),作为类的一个成员。不推荐
(2)在类中实现System.IDisposable接口。推荐
(3)使用using语句。推荐
在C#中,推荐使用System.IDisposable接口替代析构函数。IDisposable接口定义了一种模式,该模式为释放非托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾回收器相关的问题。IDisposable接口声明了一个Dispose()方法,它不带参数,返回void。例如:
public class People : IDisposable
{
public void Dispose()
{
this.Dispose();
}
}
Dispose()方法的实现代码显式地释放由对象直接使用的所有非托管资源,并在所有也实现了IDisposable接口的封装对象上调用Dispose()方法。这样,Dispose()方法为何时释放非托管资源提供了精确的控制。
C#提供了一种语法,可以确保在实现了IDisposable接口的对象的引用超出作用域时,在该对象上自动调用Dispose()方法。该语法使用了using关键字来完成此工作。例如:
using (var people = new People())
{
// 要处理的代码
}
虽然不推荐采用析构方法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的设计模板,其中有几点需要特别注意: