- 1.什么是托管与非托管?
托管资源:一般是指被CLR(公共语言运行库)控制的内存资源,这些资源由CLR来管理。可以认为是.net 类库中的资源。
非托管资源:不受CLR控制和管理的资源。
对于托管资源,GC负责垃圾回收。对于非托管资源,GC可以跟踪非托管资源的生存期,但是不知道如何释放它,这时候就要人工进行释放。
2. 后台内存管理
介绍给变量分配内存时在计算机的内存中发生的情况
(1) 值数据类型:
- 不是对象成员的值数据类型采用栈存储方式。
- 栈指针表示栈中下一个空闲存储单元的地址。
- 栈存储是从高内存地址向低内存地址填充。
- 值数据变量超出作用域时,CLR就知道不再需要这个变量。释放变量时,其顺序总是与给它们分配内存的顺序相反。
(2) 引用数据类型:
- 用new运算符请求的内存空间(即托管堆)。用于存储一些数据,在方法退出后很长一段时间内数据仍可用。
- 托管堆是在垃圾回收器GC的控制下工作。
- 堆工作原理及内存分配:
void DoWork()
{
Customer arabel; // 在栈上给这个应用分配存储空间,但仅是一个引用,占用4个字节的空间,而不是实际的Customer对象。
arabel = new Customer(); // 首先分配堆上的内存,以存储Customer对象;再把变量arabel的值设置为分配给新Customer对象的内存地址。
// Customer实例没有放在栈中,而是放在堆中
// 与栈不同,堆上的内存是向上分配的
//------------------------------------------------------------------
Customer otherCustomer2 = new EnhancedCustomer(); //用一行代码在栈上为otherCustomer2引用分配空间,同时在堆上为EnhancedCustomer对象分配空间。
}
- 引用变量赋值:把一个引用变量的值赋予另一个相同类型的变量,则有两个变量引用内存中的同一个对象。
- 引用变量回收:当一个引用变量超出作用域是,它会从栈中删除引用,但引用对象的数据仍保留在堆中;一直到程序终止,或GC删除它为止;只有在该数据不再被任何变量引用时,它才会被删除。
(3) 垃圾回收
- GC运行时,会从堆中删除不再引用的所有对象。
- GC压缩操作:对于托管堆,在删除了能释放的所有对象后,就会把其他对象移动回堆的顶端,再次形成一个连续的内存块。
- 堆系列:
- 第0代:创建新对象时,会把它们移动到堆的这个部分中;驻留最新的对象;对象会继续放在这个部分,知道垃圾回收过程第一次进行回收。
- 第1代:第一次回收清理过程之后,仍保留的对象会被压缩,并移动到该部分。此时第0代对应的部分为空。
- 第2代:对于第1代中的老对象,这样的移动会再次发生,则其遗留下来的对象会移动到堆的第2代。位于第0代的对象会移动到第1代,第0代仍用于放置新对象。
- 在给对象分配内存空间时,如果超出了第0代对应的部分的容量,或调用GC.Collect()方法,就会进行垃圾回收。
- 有助于提高性能的领域:
- 大对象堆(大于85 000个字节的对象):架构处理堆上较大对象的方式,其对象不执行压缩过程。第2代和大对象堆的回收放在后台线程上进行,即应用程序线程仅会为第0代和第1代的回收而阻塞,从而减少了总暂停时间。
- 垃圾回收的平衡:专用于服务器的垃圾回收。平衡小对象堆和大对象堆,可以减少不必要的回收。
3. 强引用和弱引用
- 强引用:GC不能回收仍在引用的对象的内存。
- 缺点是强引用实例超出作用域,或指定为null。如果GC在运行,很容易错过引用的清理,不能释放引用的内存。可使用WeakReference避免这种情况。
- 弱引用:使用WeakReference类创建。使用构造函数,可以传递强引用(Target属性)。
- 弱引用对象可能在任意时刻被回收,因此引用该对象前必须确认存在(IsAlive属性为true)。成功检索强引用后,可以通过正常方式使用它。
//创建一个DataObject,并传递构造函数返回的弱引用
var myWeakReference = new WeakReference(new DataObject());
if (myWeakReference.IsAlive)
{
DataObject strongReference = myWeakReference.Target as DataObject;
if (strongReference != null)
{
//使用强引用对象 strongReference
}
}
else
{
// 引用不可用
}
4. 处理非托管的资源
- GC不知道如何释放非托管资源(如文件句柄、网络连接和数据库连接)
- 托管类在封装对非托管资源的直接或间接引用时,需制定专门的规则,确保非托管的资源在回收类的一个实例时释放。
- 自动释放非托管资源的机制:
- 声明一个析构函数(或终结器),作为类的一个成员
- 在类中实现System.IDisposable接口
(1) 析构函数或终结器
- 在C#中定义析构函数时,编译器发送给程序集的实际上是Finalize()方法
- C#析构函数问题
- 不确定性:由于GC的工作方式,无法确定C#对象的析构函数何时执行。
- 会延迟对象最终从内存中删除的时间。
- 没有析构函数的对象:在GC的一次处理中从内存删除
- 有析构函数的对象:需要两次才能销毁。
(2) IDisposable接口:推荐使用,为释放非托管的资源提供了确定的机制,精确控制何时释放资源。
- 一般方式
SqlConnection conn = null;
try
{
conn = new SqlConnection();
//do something;
}
finally
{
conn?.Dispose();
}
- 改进方式,使用using简化输入,编译器自动翻译成 try...finally。在变量超出作用域是,会自动调用其Dispose()方法。
using(SqlConnection conn = new SqlConnection())
{
//do something;
}
(3) 双重实现:正确调用Dispose(),同时将析构函数作为一种安全机制。
public class BaseResource : IDisposable
{
private IntPtr _handle; // 句柄,属于非托管资源
private System.ComponentModel.Component _comp; // 组件,托管资源
private bool _isDisposed = false; // 是否已释放资源的标志
//实现接口方法
//由类的使用者,在外部显示调用,释放类资源
public void Dispose()
{
Dispose(true);// 释放托管和非托管资源
// 将对象从垃圾回收器链表中移除,
// 从而在垃圾回收器工作时,只释放托管资源,而不执行此对象的析构函数
GC.SuppressFinalize(this);
}
//由垃圾回收器调用,释放非托管资源
~BaseResource()
{
Dispose(false);// 释放非托管资源
}
//参数为true表示释放所有资源,只能由使用者调用
//参数为false表示释放非托管资源,只能由垃圾回收器自动调用
//如果子类有自己的非托管资源,可以重载这个函数,添加自己的非托管资源的释放
//但是要记住,重载此函数必须保证调用基类的版本,以保证基类的资源正常释放
protected virtual void Dispose(bool disposing)
{
if (!this._isDisposed)// 如果资源未释放 这个判断主要用了防止对象被多次释放
{
if (disposing)
{
// 释放托管资源,调用其Dispose方法
_comp.Dispose();
}
// 释放非托管资源
closeHandle(_handle);
_handle= IntPtr.Zero;
}
this._isDisposed = true; // 标识此对象已释放
}
}
5.不安全的代码:C#直接访问内存
(1) 用指针直接访问内存:
- 引用就是一个类型安全的指针。
- 使用指针可以访问实际内存地址,执行新类型的操作。
- 使用指针的原因:
- 向后兼容性:用于调用本地的Windows API => 可使用DllImport声明,以避免使用指针。
- 性能:指针提供最优速度性能 => 可使用代码配置文件,查找代码中的瓶颈。?
- 使用指针,必须授予代码运行库的代码访问安全机制的高级别信任。
- 强烈建议不要轻易使用指针。
(2) 用unsafe关键字编写不安全的代码
- 方法、方法的参数、类或结构、成员,均可使用unsafe
- 方法中的一块代码可标记为unsafe
void MyMethod()
{
// code that doesn't use pointers
unsafe
{
// unsafe code that uses pointers here
}
// more 'safe' code that doesn't use pointers
}
- 不能把局部变量本身标记为unsafe
(3) 指针的语法
- 把代码块标记为unsafe后,可以使用指针语法声明。
- 指针运算符
- & 寻址运算符:表示“取地址”,并把一个值数据类型转换为指针。
- * 间接寻址运算符:表示“获取地址的内容”,把一个指针转换为值数据类型。
- 指针可以声明为任意一种值类型,包括结构;但不能声明为类或数组。
(4) 将指针强制转换为整数类型
- 由于指针实际上存储了一个表示地址的整数,因此任何指针中的地址都可以和任何整数类型之间相互转换。
- 转换必须是显示指定的。
- 转换的主要目的是显示指针地址
- 32位系统,可转换为uint、long、ulong
- 64位系统,可转换为ulong
(5) 指针类型之间的强制转换
- 可以在指向不同类型的指针之间进行显式转换
- 指针类型之间转换,实现C union类型的等价形式
(6) void指针
- 维护一个指针,但不希望指定它指向的数据类型,可声明为void指针
- 主要用途:调用需要void*参数的API函数
(7) 指针算术运算
- 指针加减整数,是指指针存储地址值的变化。
- 不同类型的指针字节数不同
- 给类型为T的指针加上数值X,其中指针的值为P,则得到的结果是P+X*(sizeof(T))
- 如果类型是byte或char,总字节数不是4的倍数,连续值不是默认地存储在连续的存储单元中
- 对指针使用+、-、+=、-=、++、--,其右边变量必须是long或ulong
- 不允许对void指针执行算术运算
(8)sizeof运算符:确定各种数据类型的大小。
- 优点是不必在代码中硬编码数据类型的大小
sizeof(char) = 2; sizeof(bool) = 1;
- 结构可以使用,类不能使用。
(9)结构指针:指针成员访问运算符
- 结构不能包含任何引用类型,因为指针不能指向任何引用类型。
- 指针成员访问运算符: ->
// 结构
struct MyStruct
{
public long X;
public float F;
}
// 结构指针
MyStruct* pStruct;
// 初始化
var myStruct = new MyStruct();
pStruct = &myStruct;
// 通过指针访问结构的成员值
(*pStruct).X = 4;
(*pStruct).F = 3.4f;
// 使用成员访问运算符
pStruct->X = 4;
pStruct->F = 3.4f;
//指针指向结构中一个字段
long* pL = &(pStruct->X);
float* pF = &(pStruct->F);
(10) 类成员指针
- 对类中值类型成员创建指针,需要使用fixed关键字,告知GC这些对象不能移动。
- 在执行fixed块中代码时,不能移动对象位置。
- 声明多个fixed指针,就可以在同一个代码块钱放置多条fixed对象
- 可以嵌套fixed块
- 类型相同,可在同一个fixed块中初始化多个变量
(11)使用指针优化性能:创建基于栈的数组。
- 在栈中的数组具有高性能、低系统开销的特点,但只对于一位数组比较简单。
- 关键字stackalloc:指示在栈上分配一定量的内存。
- 要存储的数据类型
- 要存储的数据项数
int size;
size = 20;
double* pDoubles = stackalloc double[size];
-
- 分配的字节数是项数乘以sizeof(数据类型)
- stackalloc返回的指针指向新分配内存块的顶部
- 数组元素访问:
- 使用指针算术,即表达式*(pDouble+X)访问数组中下标为X的元素
- 表达式pDouble[X]在编译时解释为*(pDouble+X)
6.平台调用
- 并不是Windows API调用的所有特性都可用于.NET Framework,可采用平台调用方法实现。
- 采用extern修饰符标记
- 用属性[DllImport]引用DLL
- 非托管方法定义的参数类型必须用托管代码映射类型
- C++有不同Boolen数据类型,可使用特性[MarshalAs]指定.NET类型bool应映射为哪个本机类型
// C++ 调用Windows API(kernel32.dll)中CreateHardLink
BOOL CreateHardLink(
LPCTSTR lpFileName,
LPCTSTR lpExistingFileName,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
// C# 调用CreateHardLink
[DllImport("kernel32.dll", SetLastError="true",
EntryPoint="CreateHardLink", CharSet=CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CreateHardLink(string newFileName,
string existingFilename,
IntPtr securityAttributes);
- 通常本地方法时,通常必须使用Windows句柄(IntPtr结构);NET2.0引入SafeHandle类,派生的句柄类型是SafeFileHandle\SafeWaitHandle\SafeNCryptHandle\SafePipeHandle。