.NET
内存管理是自动进行的,包括以下几个过程
- 内存分配
- 内存释放
- 代(
Generations
) - 非托管资源的内存释放
内存分配
当初始化一个进程时,运行时会为该进程分配一个连续的地址空间区域——即为托管堆。
托管堆就像一个管家一样,始终持有一把钥匙的钥匙(一个指针)——下一个空房间(可用空间首地址,即为下一个对象分配的空间的开始位置)。当然,最开始管家手中的钥匙,是大门的钥匙(即托管堆的基地址)。
所有的引用类型,都是在托管堆上分配的;而值类型的空间则在栈上分配。
需要注意的是:
- 引用类型在托管堆上分配空间(房间)之后,对这个空间的引用(钥匙)则是放在栈上(管家手中)的
- 对引用(钥匙)的复制,属于简单的复制(浅拷贝)。因为这也仅仅是多了一把钥匙而已,这两把钥匙都只能开同一个房间(两个引用都指向同一个地址空间)
在托管堆中进行内存的分配,还可以带来性能的优势:
- 运行时为新对象分配内存时,通过不断的更改指针的值(更改管家手中的钥匙),而不是从系统内存中新分配内存(即建造一个新房间)。这样,内存分配的速度几乎可以同在栈上分配一样快了
- 内存分配的连续性,可以做到快速的访问这些对象
不过,这里有个前提
托管堆中的内存足够应用程序使用。如果不够,运行时将会频繁的进行
GC
,这会对性能造成很大的影响。比如在Unity3D
的开发中,频繁的GC
可能会造成游戏画面不连续。
内存的释放
GC
会根据对象的分配,来决定该对象回收的最佳时机。
它通过检查应用程序的根来确定不再使用的对象,每个应用程序都有一组根(包含线程堆栈和CPU
寄存器上的静态字段、局部变量和参数),每个根要么引用托管堆中的对象,要么被设置为null
。
GC
通过访问JIT
和运行时维护的活动根的列表来检查应用程序的根(可访问性检查),同时创建一个可访问的对象图。不在该图中的对象,即表示不可达(无法从根访问),将被GC
视为垃圾,并释放为这些对象分配的内存。
在回收过程中,如果发现大量不可访问的对象,则会使用内存复制功能来压缩内存中可访问的对象:移动对象,以保证可访问的对象在一块连续的内存空间内。
同时,对托管堆指针进行更正(重新为管家拿一把钥匙)。这样的好处是,可以让剩下的内存空间连续。
值得注意的是,为了性能,在进行内存复制的时候,将不会处理托管堆中的大型对象(如图像)。
其一,这些大对象的移动可能会花很长时间;
其二,GC
的过程中,会挂起正在运行的线程,如果在这过程中去移动这些大对象,则可能会造成程序假死。
代(Generations
)
为了优化GC
性能,托管堆被分为了三代:第0
代、第1
代和第2
代。
其垃圾回收算法的原理如下:
- 压缩一部分内存要比压缩整个托管堆快
- 较新的对象的生存期较短,较老的对象的生存期较长
- 较新的对象与其他对象有更大的关联性,且基本上会在某一时间段内被应用程序访问
鉴于以上原理,有以下的过程:
- 新的对象存储在第
0
代 - 老的对象如果未被回收,则升级为第
1
代和第2
代 GC
在第0
代已满的时候,将回收第0
代中的对象(新对象),而这往往可以回收足够多的内存- 在第
0
代回收的过程中,未被回收的对象,将会升级为第1
代 - 若第
0
代的回收中,未能回收到足够的内存,这时,GC
将对第1
代的对象进行回收 - 以此类推,第
1
代未被回收的,将会升级为第2
代,等等。
非托管资源的内存释放
非托管资源与托管资源不同,它们的内存需要我们显式的释放。
其释放方式,除了上一篇文章温故之.NET托管资源与非托管资源中介绍的方式外,还有另外一种方式。
这种方式我们将在下一篇文章【温故之.NET垃圾回收】中一一道来。
小结
每次GC
时,做了什么
- 检索堆上的每个对象
- 搜索所有当前对象引用以确定堆上的对象是否仍在作用域内
- 不在作用域内的对象被标记为删除
- 删除被标记的对象并将内存返回给堆
故堆上的对象越多,代码中的引用数越多,GC
就越费时。
另外需要注意的是,每次GC
时,其都会挂起当前正在运行的线程,这肯定会对性能有影响,因此需要避免过多的GC
。
何时触发GC
- 堆分配时堆上的可用内存不足时触发
GC
:顺序为第0
代、第1
代、第2
代,具体可见上面【代(Generations
)】 GC
会不时的自动运行(频率因平台而异),此即为“合适的时机”,但第一条中的情况下,一定会运行- 手动
GC
:调用GC.Collect()
或GC.Collect(int generation)
- 操作系统内存不足时,会触发
GC
至此,本节内容讲解完毕。欢迎关注公众号【嘿嘿的学习日记】,所有的文章,都会在公众号首发,Thank you~