主要内容:
介绍内存管理和内存访问的各个方面的知识。尽管 .Net 为程序员处理了大部分的内存管理工作,但是如果了解内存管理的工作原理,可以了解如何出未托管的资源。
1 运行库是如何在堆栈和堆上分配空间
2 垃圾回收收集的工作原理
3 如何利用析构函数和 System.Idisposable 接口来确保正确的释放为托管的资源
4 c # 中使用指针的语法和益处
1 后台内存的管理
1.1 值数据类型:
Windows 使用的一个系统:虚拟寻址系统,该系统把程序可用的内存映射到硬件内存中的实际地址行, 32 位的处理器中的每个进程可以使用 4G 的相对独立内存。
在虚拟内存当中,有一块区域称为堆栈:堆栈存储的不是对象成员的值数据类型,也在调用一个方法的时候使用它来存储传递给方法的所有参数的复本,例如:
{
Int a;
//doing something
{
Int b;
//doing something
}
}
堆栈的工作方式:释放变量的时候总是和它分配(声明)的时候的顺序相反,这种先进后出的方式。
在超出了作用域后就会马上释放掉资源。所有它的性能是非常高,但是变量的生存期必须嵌套,过于苛刻,我们希望还可以一种分配内存在方法退出的很长的一段时间后还是可以使用的,这个就是引用数据类型。
1.2 引用数据类型:
只要是用 New 运算来请求内存存储空间,就可以实现上面说的另一种方法。这个就是托管堆。
托管堆是工作在垃圾收集器的控制下工作,必传统的 c++ heap 有明显的性能优势。
例:
{
Customer arable;
Arable=new Customer();
Customer OtherCustomer=new Customer();
}
下面来讲解下内存的分配:
首先:
Customer arable;
声明了一个 Customer 的引用 arable, 会在堆栈上给这个引用分配存储空间,但是这个仅仅是一个引用,并不是实际的 Customer 对象,这个引用 arable 占用四个字节的空间,包含了存储 Customer 对象的地址。默认是指向为空 Null 。
再来:
Arabel=new Customer();
完成以下操作:首先分配堆上的内存,以存贮 Customer 实例(一个真正的实例,而不是一个地址),然后把变量 arable 的值设置为分配给新 Customer 对象的内存地址,即 arabel 指向了存储 customer 实例,保留了它的地址
可以清晰的看见:
Customer 的实例没有存放在堆栈中,二是存放在内存的堆中。
当引用变量出作用域的时候,它会从堆栈中删除,但是引用对象仍保留在堆中,一直到程序停止或者垃圾回收器删除它为止。而且只有该数据在不被任何变量引用的时候才会被删除。
1.3 垃圾收集
托管堆的工作方式非常类似于堆栈,对象在某种程度上讲会在内存中一个挨着一个放置。但是由于对象的生存周期和引用他们的变量的作用域是不匹配,所以会有点不同。
在垃圾回收器运行的时候,会在堆中删除不再被引用的对象,对象完成删除操作以后,堆中的对象就会分开,不是连续的排放,但是托管堆会压缩对象,移动他们使得他们再次形成一个连续的块。这个过程是有垃圾回收器自动更新各个引用的新地址。
注意 ; 垃圾回收器在 .Net 运行库认为需要的时候运行,我们也可以调用 System.GC.Collect() 强迫垃圾回收器在代码中执行。(垃圾回收器不能保证在一个的回收后,所有的未引用的对象都从堆中删除。)
2 释放未托管的资源
垃圾回收器的出现使得不用担心对象的释放,只需要所有的引用都超出了作用域,垃圾器就会自动回收。但是垃圾回收器不能回收未托管的资源(例如文件句柄,网络连接和数据库连接)。
在定义一个类的时候可以有两种机制来自动释放未托管的资源。
1 声明一个析构函数(或者终结器),作为类的一个成员
2 在类中执行 System.Idisposable
1 析构函数
Class MyClass()
{
~MyClass()
{
//Destructor implementation
}
}
但是注意在 C# 中,由于垃圾回收器的使用,使得析构函数的调用释放容易出现问题。所以在这里就不推荐使用这种方法来释放未托管的资源。
2 Idisposable 接口
C# 中推荐使用 System.Idisposable 接口来替代析构函数。 Idisposable 为释放未托管的资源提供了确定的机制,例如下:
Class MyClass ; Idisposable
{
Public void Dispose()
{
//implementation
}
}
提示,这个 Dispose 是我们认为来调用的,当我们认为不再需要的时候就人为的来调用来释放未托管的资源,也可以利用 c# 提供的一种语法来自动的释放
Using (ResourceGlobbler theInstantce =new ResourceGobbler() )
{
//Do your processing !
}
这样, theInstantce 仅仅是在 {} 内是有效的,超出作用域后会自动调用 Dispose 来释放资源,不需要手动来调用的了。
下面给出一个例子来采用析构和接口结合的方法来实现:
Public Class ResourceHolder:Idisposable
{
Private bool isDisposed=false;
Public void Dispose(0
{
Dispose(true);
GC.SuppressFinalize(this);
}
Protect virtual void Dispose(bool disposing)
{
If(!isDisposed)
{
If(disposing)
{
//cleanup managed object by calling their dispose() method
}
//cleanup unmanged objects
}
isDisposed=true;
}
~ResourceHolder(0
{
Dispose(true);
}
Public void SomeMethod()
{
//Ensure object not aleardy disposed before exeution of any method
If(isDisposed)
{
Throw new objectDisposedException(“ReousceHolder ”);
}
//Method implementation ……
}
}
这样通过双重保险保证。
3 不安全的代码
3.1 c# 中的指针
指针只是一个以与引用相同的凡是存储地址的变量。区别: c# 中不允许直接访问引用变量中包含的地址。有了引用以后,从语法上看,变量就可以存储引用的实际内容。
C# 引用主要用于使 C# 语言易于使用,防止用户无意中执行某些破坏内存中内存的操作,另一方面,使用指针,就可以访问实际内存地址,执行新类型的操作。例如给地址加 4 字节,就可以查看甚至是修改存储在新地址中的数据。这就是为什么不能在 C# 中直接使用指针的原因。
使用指针的两个主要原因:
1 向后兼容性,调用 Windows Api
2 性能
下面我们简单的说明在 C# 中使用指针的一些事情。
1 :编写不安全的代码
由于使用指针会带来风险,所以 c# 只允许在特定标记的代码块中使用指针。关键字是 Unsafe , 例如下面的代码把一个方法标记为 unsafe :
Unsafe int GetSomeNumber()
{
//code that can use pointers
}|
任何的方法都可以标记为 unsafe--- 无论是否应用了其他的修饰符,还可以放在方法的参数上,允许把整个类或者结构标记为 unsafe 等。但是注意不能把局部变量本身标记为 unsafe.
2: 指针的语法 ;
把代码块标记为 unsafe 后就可以使用下面的语法声明指针了:
Int * pWidth,pHeight;
Double * pResult;
Byte * [] pFlags
注意在 c++ 中这个语法和 c# 中是不同的。对应的 c++ 语法是 int *px,*py 。
在 c# 当中, * 符号与类型相关,但是不和变量名相关。
3: 把指针转换成整数类型
指针实际上存储的是一个表示地址的整数,所以任何指针的地址都是可以转换为任何整数类型。但是要注意:指针到整数类型的转换必须是显示指示,隐式的转换是不允许的。
32 位系统上,地址是占用 4 个字节,转换时只能把指针转换成 Unit ,long ,ulong 类型。否则会发生溢出。注意这个的溢出我们用 Checked 关键字是无法涉及到指针的转换(无作用)。
还需要注意的是, c# 在用于 64 位处理器时,地址那么就是占用 8 个字节,因此在这样的系统上,把指针转换成非 ULONG 类型都是有可能发生溢出错误。
4 指针类型之间的转换
指向不同类型的指针之间是可以进行显示的转换。例如:
Byte aByte=8;
Byte *pByte=&aByte;
Double *pDouble=(Double *)pByte;.
注意上面是一段合法的代码,但是执行的时候就要小心了。如果要查找 pDouble 指向的 Double, 就会查找包含一个字节( aByte )的内存,并和一些其他的内存内容合并在一起,把它当作一个 double 的内存区域来对待,这样不会得到一个有意义的值。
但是可以类型之间的转换,实现类型的统一,例如把指针转换为 sbyte, 检查内存的单个字节。
5 Void 指针
如果要使用一个指针,但是不希望指定它指向的数据类型,可以把指针声明为 void :
Int *pointerToInt;
Void *pointerToVoid;
pointerToVoid = pointerToInt;
6 指针的算法:
不允许对 void 指针执行算术运算。
指针的算术运算是和指针声明的类型相关的。
7 Sizeof 运算
求的各种数据类型的大小
8 结构指针:指针成员访问运算符
结构指针的工作方式和预定义值类型的指针的工作方式是一样的,但是有一个条件:结构不能包含任何引用类型,因为指针不能指向任何引用的类型。如果有这种情况,编译器会标记一个错误。
9 类成员指针
不能创建指向类的指针,这个是因为垃圾收集器不能维护指针的任何信息,值能维护引用的信息,所以创建指向类的指针会使得垃圾收集器不能正常工作。
但是为什么呢?
垃圾回收器在处理对象的时候,需要把对象进行压缩,这样就会移动对象在堆中的位置。垃圾回收器对于引用来说是可以自动修改它的值来指向对象移动后的新的位置值,但是如果是指针就不行了,移动后指针就会指向错误的位置,这个就会出现错误。所以就是因为垃圾回收器的作用使得我们不能创建简单的直接指向类的指针。我们在 c# 中可以使用 Fixed 关键字告诉垃圾回收器某实例的某些成员有指向他们的指针,所以这些实例是不能移动的。例如:
申明一个指针,使用 fixed 的语法如下:
Myclass()
{
Public int X;
Public Int F;
}
Myclass myobject=new Myclass();
Fixed(long *pObject=&(myobject.x))
{
//do something
}
这个指针变量的作用域范围是后面的 {} ,这样编译器就知道在执行 Fixed 块的时候是不能移动 MyObject 对象。如果要声明多个这样的指针,可以在 Fixed 处多加几个,例如:
Myclass myObject=new Myclass();
Fixed( long *pX=&(myobject.X))
Fixed(long *pF=&(myobject.F))
{
//do something
}
如果在不同的阶段固定几个指针,还可以嵌套整个 fixed 块,也可以在一个 fixed 语句初始化多个变量,但是这些变量的类型是必须相同。
3.3 使用指针的优化性能:这个才是最重要的
介绍了这么多,有用才是最重要的。
1 基于堆栈的数组:
在堆栈中创建数组是高性能,低系统开销的数组。 C# 中很容易使用一维数组或者是多为数组,但是这些都是有一个致命的缺点:这些数组实际上都是对象,是 System.Array 的实例。因此这样的数组只能存储在堆上,会增加系统的开销。有时候我们希望能够得到一个高性能的数组,不希望有引用对象的系统开销,指针就可以做到。但是指针只能用于一维数组。
为了创建一个高性能的数组,我们可以使用一个关键字, stackalloc, 它指示 .Net 运行库分配堆栈上一定数量的内存。在调用它的时候,需要为它提供两条信息:
要存储的数据类型
需要存储的数据项数
例如要法呢配 10 个 decimal 数据项
Decimal *pDecimals=stackalloc double[10];
注意这个命令只是分配堆栈内存而已,它不会试图把内存初始化成任何默认值,这刚好满足我们的要求:高性能。默认的初始化也会降低性能。
Double *pDouble=stackalloc double[20];
可以看出来, stackalloc 后卖弄必须紧根要分配的数组的类型名,且这个类型必须是一个值类型,由于这个是动态分配的,所以分配的量是变量。
Int size;
size =20;
Double *pDouble=stackalloc double[size];
还有点需要注意:
Double []mydoublearray=new double[20];
Mydoublearray[50]=3.0;
这个东西会报错,越界了。
但是利用 stackalloc 是不会抛出异常,因为它是直接操作内存。