内存管理
栈(stack)
值类型存储在栈中
栈模拟图:
800000(已用)栈指针目前位置
|
799999(未用)
|
799998(未用)
|
799997(未用)
|
799996(未用)
|
799995(未用)
|
地址从高到低
工作原理:
{
int a = 3;
{
double b = 320.00;
}
}
int c = 4;
①a进入作用域,赋值为3,int型4个字节,分配799999 - 799996,此时栈指针下一个地址是799995
②b进入作用域,赋值为320.00,double类型8个字节,分配79995 - 79988,此时栈指针下一个地址是799987
③b出作用域,删除变量地址,栈指针下一个地址是799995
④a出作用域,删除变量地址,栈指针下一个地址是799999
⑤c进入作用域,赋值为4,int型4个字节,分配799999 - 799996,此时栈指针下一个地址是799995
由此可见:栈释放变量,其顺序总是与给他们分配内存的顺序相反。(先进后出)
托管堆(heap)
引用类型存储在托管堆上。
栈模拟图:
800000(已用)栈指针目前位置
|
799999(未用)
|
799998(未用)
|
799997(未用)
|
799996(未用)
|
799995(未用)
|
堆模拟图:
200005(未用)
|
200004(未用)
|
200003(未用)
|
200002(未用)
|
200001(未用)
|
200000(已用)
|
199999(未用)
|
199998(未用)
|
199997(已用)
|
地址从低到高
工作原理:
{
Student m_student;
m_student = new Student();
{
Student m_student2 = new GoodStudent();
}
}
①Student m_student ==>声明Student类的引用m_student,因为是引用还未是对象,所以在栈上分配存储空间,m_student引用占4个字节,包含了存储Student引用的地址,分配799999 - 799996,此时栈指针下一个地址是799995
②m_student = new Student() ==>分配堆上内存,以存储Student对象实例,假设Student对象占64字节(CLR在托管堆上搜索能够放得下64字节的空间,比如199998 - 19999是空的但是不够放下64字节,所以往下找),在托管堆上分配 20001 - 20064位置
③Student m_student2 = new GoodStudent() ==>从栈上分配799995 - 79991给m_student2引用,从托管堆上分配20065 - 20128给m_student2对象。
④m_student2引用出作用域,释放m_student2引用,收回栈上799995 - 79991地址,但是m_student2对象还在托管堆的20065 - 20128地址上,除非程序终止或者垃圾回收器删除它,而且只有该数据不被任何变量引用时才可以删除。
⑤m_student引用出作用域,释放m_student引用,收回栈上799999 - 799996地址,但是m_student对象还在托管堆的20001 - 20064地址上,除非程序终止或者垃圾回收器删除它,而且只有该数据不被任何变量引用时才可以删除。
垃圾回收
垃圾回收机制运行时,它会删除不再引用的对象,删除后,堆会立刻把释放的内存与空闲的内存结合在一起。
效果图如下:
垃圾回收前:
已用
|
已用
|
已用
|
空闲
|
空闲
|
已用
|
垃圾回收后:
已用
|
空闲
|
空闲
|
空闲
|
空闲
|
已用
|
但是,有一个问题,如果新的引用需要不止4个字节的存储空间,假如32个字节,则需要跳过这个存储空间继续寻找下一个32个字节大小的空间。
所以垃圾回收器还做下一步工作,删除不在引用的对象后,将其他对象移动回堆的顶部,再次形成连续的内存块,那样堆就跟栈一样拥有连续的空闲的内存空间了(但是在此过程中需要调整这些引用的地址)。
总结:垃圾回收器,回收引用,修改引用地址,使堆内存成块。
垃圾回收期使用场景:一般CLR自动运行垃圾回收机制,不要认为的进行回收,回收原理如下图。使用场景之一:有大量的对象刚刚取消引用。
eg.
使用 Optimized 设置对第 2 代对象进行垃圾回收。
using System;
class Program
{
static void Main(string[] args)
{
GC.Collect(2, GCCollectionMode.Optimized);
}
}
释放非托管资源
析构函数:文件流,数据库连接,窗口等释放还是可以的。但是有一定弊端,与C++的析构函数不同,C#析构函数你不知何时运行,未知的总是恐怖的。
public class MyClass
{
StreamWriter sw;
~MyClass()
{
//进行资源的清理
sw.Close();
}
public void Func()
{
FileStream fs = new FileStream("D:\\te.txt", FileMode.OpenOrCreate);
sw = new StreamWriter(fs);
string str="哈哈";
sw.Write(str);
}
}
IDisposable接口:这个好,微软推荐,加V就是不一样。
eg.
Class Student :IDisposable
{
public void Dispose()
{}
}
如果在处理过程中出现异常,则theInstance没有释放,所以需要try块中finally来释放
ResourceGobbler theInstance = null
try
(
theInstance = new ResourceGobbler()
//todo...
)
finally
{
if(theInstance != null)
{
theInstance.Dispose();
)
)
微软爸爸说,总是finally来释放烦不烦,来一个简洁方式!
using关键字确保在实现IDisosable接口的对象的引用超出作用域时,在该对象上自动调用Dispose()方法。
using(ResourceGobbler theInstance =new ResourceGobbler())
{
//todo...
}
(注:部分图是借鉴而来)