C# 内存结构 ,以及大对象堆的管理,大对象堆引发OutOfMemory异常

C#的内存分类
由于C#是一种托管语言,它的垃圾回收机制(GC)是由.net平台负责的,加之C#语言并没有指针,所以我们在使用过程中极少会考虑到内存使用状况以及项目在运行过程中是如何进行内存管理的。但是,C#只是在内存管理方面对程序员隐藏了,并不代表它不涉及这些东西,甚至其内部内存管理或许比自己手动管理更加复杂。

参考前面文章中的内存分类——四分类,本文会依据自己的理解,从这四个分类来说明。

1、代码区

开辟在栈区的内存中

2、线程栈  

C#程序在程序运行的时候,每个线程(Thread)都会维护一个自己的专属线程堆栈。此即为“栈”,或者称之为“线程栈”

值类型存储在线程栈。栈由操作系统进行管理,不受GC管理,当值类型不在其作用域(主要是指其所在函数内)时,其所占栈空间自动释放。栈的执行效率是非常高的。

3、托管堆

当CLR载入内存之后,会初始化两个托管堆,

一个大对象堆(LOH –large object heap)
一个小对象对(SOH – small object heap)
GC堆

(1)小对象堆(SOH)

对于SOH,对象在执行一次垃圾回收之后,会进入到下一代。也就是说如果在第一次执行垃圾回收时,存活下来的对象会进入第1代,如果在第2次垃圾回收之后该对象仍然没有被当作垃圾回收掉,它就会成为2代对象;2代对象就是最老的对象不会在提升代数。从代的角度看,大对象属于2代对象,因为只有在2代回收时才会处理大对象。

(2)大对象堆(LOH)
用于分配大对象实例。大对象就是大小大于85000字节(约为83k)的实例对象。大对象分配在LOH上,不受GC控制,不会被压缩,只有在完全GC回收(只有在2代回收时才会处理大对象)时才会被回收。

(3)GC堆
用于分配小对象实例。所谓小对象就是大小小于85000字节(约为83k)的实例对象。GC堆分三代垃圾进行管理,当进行GC操作(垃圾回收)时,垃圾收集器会对GC堆进行压缩回收。

 

4、全局数据区

       全局变量、常量、静态类和静态成员(静态变量、静态方法),都存放在全局区,而它们的地址放在声明时变量(在代码区)开辟在栈区的内存中。(这些地址都是在程序运行时最先压栈的,这点很重要)

       全局变量和静态类、常量、静态成员,都是在全局区,但是它们的地址仍放在栈区,为什么会保存住呢?因为在.net程序编译时这些静态和全局都是最先编译的,所以最先压栈,那么也就只能等程序结束时才会弹栈,所以全程可用。缺点就是启动慢、编译时间长;当然优点也有,如常说的,静态类常用于窗体传值和实现单例模式,这就得益于它的一次编译全程可用。

(这就是为什么静态类,静态数据可以全程使用的原因了,因为它们最先入栈,最后出栈)

        所以全局和静态上面说过,会在程序运行结束时才会被释放和回收,所以应限制使用全局变量、常量、和静态变量和静态类,否则程序负荷高。
————————————————
 

托管堆中大对象堆的释放过程

垃圾回收器运行时,它会从堆中删除不再引用的对象。垃圾回收器在引用的根表中找到所有引用的对象,接着在引用的对象树中查找。在完成删除操作后,堆会立即吧对象分散开,与已经释放的内存混合在一起。

垃圾回收器压缩:垃圾回收器会把其他对象移动回到端部,再次形成连续的内存块。

垃圾回收器的这个压缩操作是托管的堆与非托管的堆区别所在。使用托管的堆,就只需要读取堆指针的值即可,不需要便利地址的链表。

需要进行垃圾回收时,可以调用System.GC.Collcet()方法,强迫垃圾回收器在代码的某个地方运行回收垃圾。

在测试中运行GC比较有用,这样看到应该回收的对象仍然未回收而导致内存泄漏。

GC垃圾回收总的有三代:0,1,2

创建对象时,把对象放在托管堆上,堆的第一部分称为0代。

经过第一次垃圾回收后,仍然保留的话对象会先被压缩,然后移动第一代对应的部分,现在是第一代。

重复下一次回收过程,第一代遗留下来的还没被回收的会移动到第2代。

新创建的对象都是0代。

这个过程回极大的提高应用程序的性能。一般而言。最新的对象通常是可以回收的对象。

如果对象在堆中的位置是相邻的,垃圾回收过程就会更快。

 

在.Net中,垃圾回收提高性能的另一个领域是架构处理堆上较大的对象方式。

在.Net下,较大的对象有自己的托管堆,称为大对象堆。使用大于85000个字节 的对象时,它们就会放在这个特殊的堆上,而不是主堆上。

.Net应用程序不知道两者的区别,因为这是自动完成的。

为什么要有大对象堆?

因为在堆上压缩大对象是比较昂贵的,因此驻留在大对象堆上的对象不执行压缩过程。

第二代和大对象堆上的回收现在是放在后台线程上进行的。

这表示应用程序线程仅会为第0代和第1代的回收而阻塞,减少了总暂停的时间,服务器和工作站默认打开这个功能。

有利于提高应用程序性能的另一个优化是:垃圾回收的平衡。它专用域服务器的垃圾回收。

服务器一般有一个线程池,执行大致相同的工作,内存分配在所有线程上都是类似的。

每个逻辑服务器都有一个垃圾回收堆。因此其中一个堆用尽了内存,触发了垃圾回收过程时,所有堆也可能会得益于垃圾回收,这就不是很高效,垃圾回收过程会平衡这些堆----小对象堆和大对象堆。进行这个平衡过程,可以减少不必要的回收。

 

频繁分配大内存对象(大于等于85000byte),可能会引发OutOfMemory异常

每一次分配一个大对象时,优先使用尾部的空间,当最后要分配内存时前面已经没有任何一

块连续区域满足要求时,所以就会引发OutOfMemoryExceptiojn异常。

要解决这一问题其实并不容易,但可以考虑下面的策略。 

将比较大的对象分割成较小的对象,使每个小对象大小小于85, 000字节,从而不再分配在LOH上;
尽量“重用”少量的大对象,而不是分配很多大对象;

发布了35 篇原创文章 · 获赞 15 · 访问量 5万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览