C#基础 GC 算法 一些积累

本文详细介绍了.NET框架中的内存管理,包括内联函数、垃圾回收(GC)的基本概念、GCRoot、各种GC算法如标记清除、复制收集、引用计数、分代回收以及增量和并行回收。GC通过自动管理内存,解决了手动管理内存可能导致的问题,如内存泄露和数据访问异常。文章还讨论了不同GC算法的优缺点,以及在特定条件下的适用性。最后,探讨了如何通过理解和利用GC来优化程序性能,强调了避免不必要的显式调用GC的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

[MethodImpl(MethodImplOptions.AggressiveInlining)] 内联函数

Impl:implement的缩写

内联函数

计算机科学中,内联函数(有时称作在线函数编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。但在选择使用内联函数时,必须在程序占用空间和程序执行效率之间进行权衡,因为过多的比较复杂的函数进行内联扩展将带来很大的存储资源开支。另外还需要特别注意的是对递归函数的内联扩展可能引起部分编译器的无穷编译。

设计内联函数的动机

内联扩展是一种特别的用于消除调用函数时所造成的固有的时间消耗方法。一般用于能够快速执行的函数,因为在这种情况下函数调用的时间消耗显得更为突出。这种方法对于很小的函数也有空间上的益处,并且它也使得一些其他的优化成为可能。

没有了内联函式,程式员难以控制哪些函数内联哪些不内联;由编译器自行决定是否内联。加上这种控制维度准许特定于应用的知识,诸如执行函式的频繁程度,被利用于选择哪些函数要内联。

此外,在一些语言中,内联函数与编译模型联系紧密:如在C++中,有必要在每个使用它的模块中定义一个内联函数;与之相对应的,普通函数必须定义在单个模块中。这使得模块编译独立于其他的模块。

只有C++ C 才有内联函数关键字,因为C++/c注重运行效率。 C#中没有提供内联关键字,不过在在.NET4.5中开始提供了  内联函数ethodImplOptions.aggressiveinline特性, 提示/建议1CLR允许使用M值方法内联 。例如:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int GetCurrentProcessorId()
        {
          ///其他代码
        }

引入内联函数的目的

函数是一种更高级的抽象。它的引入使得编程者只关心函数的功能和使用方法,而不必关心函数功能的具体实现;函数的引入可以减少程序的目标代码,实现程序代码和数据的共享。但是,函数调用也会带来降低效率的问题,因为调用函数实际上将程序执行顺序转移到函数所存放在内存中某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移操作要求在转去前要保护现场并记忆执行的地址,转回后先要恢复现场,并按原来保存地址继续执行。因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率。特别是对于一些函数体代码不是很大,但又频繁地被调用的函数来讲,解决其效率问题更为重要。引入内联函数实际上就是为了解决这一问题。

使用函数内联的条件:

 虽然C#不支持inline关键字,但是JIT支持自动inline,即将IL转成真正机器码时,会自动将某些函数进行inline展开,只是条件非常苛刻,网上提到JIT自动进行inline展开的一些选择依据:

1)函数内部有循环语句、catch语句等复杂结构,都不做inline优化。
2)函数体比较长的不做inline优化,只有比较简单的才可能inline优化。(有人说IL不足32字节才做inline),
3)编译成机器码时,inline展开的代码比函数调用更短的,一定做inline。(注:如果参数多而代码少,就符合此情况)

//

C#基础-gc算法

c++是需要程序员手动管理内存的,然而手动释放内存很容易被程序员遗漏,从而导致资源浪费或内存泄露。为解决这个问题,垃圾回收器诞生了,代替程序员自动管理内存的释放。至于gc算法则是垃圾回收器清除垃圾的方法了。

本篇文章简单介绍一下各个gc算法的原理和优缺点

GC Root

GC roots are not objects in themselves but are instead references to objects.

.NET中可以当作GC Root的对象有如下几种:

1、全局变量

2、静态变量

3、栈上的所有局部变量(JIT)

4、栈上传入的参数变量

5、寄存器中的变量

在Java中,可以当做GC Root的对象有以下几种:

1、虚拟机(JVM)栈中的引用的对象

2、方法区中的类静态属性引用的对象

3、方法区中的常量引用的对象(主要指声明为final的常量值)

4、本地方法栈中JNI的引用的对象

标记清除法

原理:从GC Root开始递归,对可能引用的对象进行标记,没有标记的作为垃圾被回收

步骤:遍历并标记对象->回收死亡对象,清除存活对象的标记

缺点:

1.清除阶段还需要对大量死亡对象进行扫描,死亡对象多的话会相当耗时

2.清理出来的内存空间不连续

标记整理法

原理:从GC Root开始递归,对可能引用的对象进行标记,之后移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收

步骤:遍历并标记对象->整理存活对象->回收

缺点:效率低

复制清除法

原理:遍历GC Root引用的对象,复制到另外的空间,并递归地对复制对象引用的对象进行复制,之后清除旧空间

步骤:递归复制->废弃旧空间

缺点:

1.复制开销大,存活对象多耗时大

2.浪费一半的内存

引用计数法

原理:为每个对象保存引用计数,引用增减时更新计数

步骤:不需要扫描,对计数0的对象进行垃圾回收

缺点:

1.无法释放循环引用的对象

循环引用

1 A a = new A();
2 B b = new B();
3 C c = new C();
4 A.b = b;
5 B.c = c;
6 C.a = a;

2.引用计数不能遗漏

3.不适合并行处理

分代搜集法

原理:对分配时间短的对象进行清理

比如Net,将内存中的对象分为了三代,每执行N次0代的回收,才会执行一次1代的回收,而每执行N次1代的回收,才会执行一次2代的回收。当某个对象实例在GC执行时被发现仍然在被使用,它将被移动到下一个代中上。

而在mono 中是分2代

各平台GC算法

关于Mono ,集成的是开源项目BOEHM ,BOEHM算法采用标记清除法

《代码的未来》读书笔记:内存管理与GC

一、内存是有限的

近年来,我们的电脑内存都有好几个GB,也许你的电脑是4G,他的电脑是8G,公司服务器内存是32G或者64G。但是,无论内存容量有多大,总归不是无限的。实际上,随着内存容量的增加,软件的内存开销也在以同样的速率增加着。因此,最近的计算机系统会通过“双重”幻觉,让我们以为内存容量是无限的。

  第一重幻觉:垃圾回收(GC)机制

在C/C++中,内存空间的分配是由人工手动进行管理的,当需要内存空间时,要请求OS进行分配,不需要的时候则需要返回给OS。如果不再需要的内存空间没有及时返还给OS,这些无法访问的内存空间就会一直保留下来,造成内存的白白浪费,最终引发性能下降和产生抖动。

  将内存管理,尤其是内存空间的释放实现自动化,这就是GC

  第二重幻觉:OS提供的虚拟内存

  所谓虚拟内存,就好比是将书桌上的比较老的文件先暂时收到抽屉里,用空出来的地方来摊开新的文件。在计算机中,体现在在内存容量不足时将不经常访问的内存空间中的数据写入硬盘,以增加“账面上”可用内存容量的手段(想想我们的内存和硬盘容量对比就知道了)。

BUT,如果在书桌和抽屉之间频繁进行文件的交换,工作效率肯定会下降。如果每次要看一份文件都要先收拾书桌再到抽屉里面拿的话,那工作根本就无法进行了

虚拟内存也有同样的缺点:硬盘的容量比内存大,但也只是相对的,速度却非常缓慢,如果和硬盘之间的数据交换过于频繁,处理速度就会下降,表面上看起来就像卡住了一样,这种现象称为抖动(Thrushing)。相信很多人都有过计算机停止响应的经历,而造成死机的主要原因之一就是抖动。

二、GC的基本方式

2.1 标记清除方式

标记清除是最早的GC算法,其原理是:首先从根开始将可能被引用的对象用递归的方式进行标记,然后将没有标记到的对象作为垃圾进行回收

  下图直观地展示了标记清除算法的大致原理:

  ① 初始阶段:

  ② 标记阶段:

  其中,红色背景白色字体的对象为已标记的对象。重复这一阶段步骤,已标记的对象会被视为“存活”的对象,而没有被标记的对象就将被进行回收。

  ③ 清除阶段:

  将前面阶段中没有被标记的对象进行回收,这一操作被称为清除阶段。在扫描的同时,还需要将存活对象的标记清除掉,以便于下一次GC操作做好准备。标记清除算法的处理时间,是和存活对象与对象总数的总和相关的。

  标记清除算法的缺点:在分配了大量对象并且其中只有一小部分存活的情况下,所消耗的时间会大大超过必要的值,这是因为在清除阶段还需要对大量死亡对象进行扫描

2.2 复制收集方式

复制收集克服了标记清除的缺点,其基本原理是:将从根开始被引用的对象复制到另外的空间中,然后再将复制的对象所能够引用的对象用递归的方式不断复制下去

  下图直观地展示了复制手机的大致原理:

  ① 初始阶段:

  ② 复制收集阶段:

复制阶段-1

复制阶段-2

  ③ 清除阶段:

  在清除阶段会将旧空间废弃掉,也就可以将死亡对象所占用的空间一口气全部释放出来,而没有必要再次扫描每个对象。下次GC的时候,现在的新空间也就成为了下次的旧空间。

  复制收集的缺点是:和标记方式相比,将对象复制一份所需要的开销比较大,因此在“存活”对象比例较高的情况下,反而比较不利。

2.3 引用计数方式

引用计数方式是GC算法中最简单也最容易实现的一种,其基本原理是:在每个对象中保存该对象的引用计数,当引用发生增减时对计数进行更新。引用计数的增减,一般发生在变量赋值、对象内容更新、函数结束(局部变量不再被引用)等时间点,当一个对象的引用计数变为0时,则说明它将来不会再被引用,因此可以释放响应的内存空间

  下图直观地展示了引用计数方式的大致原理:

  ① 初始阶段:

  ② 引用计数阶段:

  当对象引用发生变化时,引用计数也会跟着变化。在这里,由对象B到对象D的引用失效了,于是对象D的引用计数变为0。由于对象D的引用计数变为了0,因此由对象D到对象C和对象E的引用数也分别相应减少。最后,对象D和对象E引用数变为了0,所以需要被清除。

  ③ 清除阶段:

  所有引用计数变为0的对象都将被释放,“存活”的对象则保留了下来。在整个GC处理过程中,并不需要对所有对象进行扫描。

  引用计数的优点在于:易于实现(标记清除和复制收集机制实现由难度);当对象不再被引用的瞬间就会被释放(其他机制预测一个对象何时被释放很困难)。

  引用计数的缺点在于:

  ① 无法释放循环引用的对象

  ② 必须在引用发生增减时对引用计数做出正确的增减:想想漏掉了对某个对象计数的增减会怎么样?

  ③ 引用计数管理并不适合并行处理:想想如果多个线程同时对引用计数进行增减又会怎样?

三、GC的改良方式

GC的基本算法,大体上都逃不出上述三种方式以及它们的衍生品。现在,通过对这三种方式进行融合,出现了一些更加高级的方式。

3.1 分代回收方式

由于GC和程序处理的本质是无关的,因此它所消耗的时间越短越好。分代回收的目的是为了在程序运行期间,将GC所消耗的时间尽量缩短。

  分代回收的基本思路是:大部分对象都会在短时间内成为垃圾,而经过一定时间依然存活的对象往往拥有较长的寿命。如果寿命长的对象更容易存活下来,寿命短的对象则会被很快废弃。那么,对分配不久的“年轻”对象进行重点扫描,应该就可以更有效地回收大部分垃圾

  在分代回收方式中,对象会按照生成时间进行分代,刚刚生成不久的年轻对象划为新生代(Young generation),而存活了较长时间的对象划为老生代(Old generation)。对于不同的实现方式,可能还会划分更多的代,

  在.NET中,CLR就将内存中的对象分为了三代,每执行N次0代的回收,才会执行一次1代的回收,而每执行N次1代的回收,才会执行一次2代的回收。当某个对象实例在GC执行时被发现仍然在被使用,它将被移动到下一个代中上,下图直观地展示了CLR对三个代的回收操作:

Three Generations

  回想刚刚说到的几种基本回收方式,我们可以将其组合一下来为分代回收奠定实现基础。

  (1)首先,从根开始一次常规扫描,找到“存活”对象。这个步骤可以采用标记清除或复制收集,不过大多数分代回收的实现都采用了复制收集算法。不过在扫描的过程中,如果遇到被划分到更高级别的代的对象则不对该对象继续进行递归扫描。这样一来,需要扫描的对象数量就大幅度减少

  (2)其次,将第一次扫描后残留下来的对象划分到更高级别的代上。具体来说,如果是用复制收集算法的话,只要将复制目标空间设置为更高级别的代就可以。而如果用标记清除算法的话,则大多采用在对象上设置某种级别标志的方式。但是,被分配到更高的级别的代上后,该对象所占用的内存空间的时间也会随之增加,如何确保及时利用和释放的平衡点也是需要考虑的。

3.2 增量回收方式

在对实时性要求很高的程序中,往往更重视缩短GC的最大中断时间(想想车辆制动控制程序因为GC而延迟响应的话后果是不堪设想的),必须能够对GC所产生的中断时间做出预测(例如将最多只能中断10ms作为附加条件)。

  因此,为了维持程序的实时性,不等到GC全部完成,而是将GC操作细分成多个部分逐一执行,这种方式就被称为“增量回收”(Incremental GC)。

  由于增量回收的过程是渐进式的,可以将中断时间控制在一定长度之内,另外由于由于中断操作需要消耗一定的时间,GC所消耗的总时间也会增加

3.3 并行回收方式

  在多核环境中,可以通过利用多线程发挥多CPU的性能,并行回收正是通过最大限度地利用多CPU的处理能力来进行GC操作的一种方式。

  并行回收的基本原理是:在原有程序运行的同时进行GC操作。相对于在一个CPU上进行GC任务分割的增量回收来说,并行回收可以利用多CPU的性能,尽可能让这些GC任务并行(同时)进行。

  不过,要让GC操作完全并行并且一点都不影响原有程序的运行是做不到的。因此,在GC操作的某些特定阶段,还是需要暂停原有程序的运行。

四、GC大一统理论

像标记清除和复制收集之类的算法是从根开始扫描以判断对象生死的算法,被称为跟踪回收(Tracing GC)。而引用计数算法则是当对象之间的引用关系发生变化时,通过对引用计数进行更新来判定对象生死。

  2004年IBM研究中心发表了一篇论文,提出了一个理论:任何一种GC算法都是跟踪回收和引用计数两种方式的组合,两者的关系正如“物质”和“反物质”一样,是相互对立的。对其中一方进行改善的技术之中,必然存在对另一方进行改善的技术,而其结果只是两者的组合而已

参考资料

(1)本文全文源自Ruby之父松本行弘的《代码的未来》一书!

(2)霍旭东,《不得不知的CLR中的GC》【好文一篇,值得阅读】

(3)cposture,《GC/垃圾回收简介

(4)周旭龙,《.NET基础拾遗之内存管理基础

///

垃圾回收机制GC知识再总结兼谈如何用好GC

一、为什么需要GC

应用程序对资源操作,通常简单分为以下几个步骤:

1、为对应的资源分配内存

2、初始化内存

3、使用资源

4、清理资源

5、释放内存

应用程序对资源(内存使用)管理的方式,常见的一般有如下几种:

1、手动管理:C,C++

2、计数管理:COM

3、自动管理:.NET,Java,PHP,GO…

但是,手动管理和计数管理的复杂性很容易产生以下典型问题:

1.程序员忘记去释放内存

2.应用程序访问已经释放的内存

产生的后果很严重,常见的如内存泄露、数据内容乱码,而且大部分时候,程序的行为会变得怪异而不可预测,还有Access Violation等。

.NET、Java等给出的解决方案,就是通过自动垃圾回收机制GC进行内存管理。这样,问题1自然得到解决,问题2也没有存在的基础。

总结:无法自动化的内存管理方式极容易产生bug,影响系统稳定性,尤其是线上多服务器的集群环境,程序出现执行时bug必须定位到某台服务器然后dump内存再分析bug所在,极其打击开发人员编程积极性,而且源源不断的类似bug让人厌恶。

二、GC是如何工作的

GC的工作流程主要分为如下几个步骤:

1、标记(Mark)

2、计划(Plan)

3、清理(Sweep)

4、引用更新(Relocate)

5、压缩(Compact)

GC

(一)、标记

目标:找出所有引用不为0(live)的实例

方法:找到所有的GC的根结点(GC Root), 将他们放到队列里,然后依次递归地遍历所有的根结点以及引用的所有子节点和子子节点,将所有被遍历到的结点标记成live。弱引用不会被考虑在内

(二)、计划和清理

1、计划

目标:判断是否需要压缩

方法:遍历当前所有的generation上所有的标记(Live),根据特定算法作出决策

2、清理

目标:回收所有的free空间

方法:遍历当前所有的generation上所有的标记(Live or Dead),把所有处在Live实例中间的内存块加入到可用内存链表中去

(三)、引用更新和压缩

1、引用更新

目标: 将所有引用的地址进行更新

方法:计算出压缩后每个实例对应的新地址,找到所有的GC的根结点(GC Root), 将他们放到队列里,然后依次递归地遍历所有的根结点以及引用的所有子节点和子子节点,将所有被遍历到的结点中引用的地址进行更新,包括弱引用。

2、压缩

目标:减少内存碎片

方法:根据计算出来的新地址,把实例移动到相应的位置。

三、GC的根节点

本文反复出现的GC的根节点也即GC Root是个什么东西呢?

每个应用程序都包含一组根(root)。每个根都是一个存储位置,其中包含指向引用类型对象的一个指针。该指针要么引用托管堆中的一个对象,要么为null。

在应用程序中,只要某对象变得不可达,也就是没有根(root)引用该对象,这个对象就会成为垃圾回收器的目标。

用一句简洁的英文描述就是:GC roots are not objects in themselves but are instead references to objects.而且,Any object referenced by a GC root will automatically survive the next garbage collection. 

.NET中可以当作GC Root的对象有如下几种:

1、全局变量

2、静态变量

3、栈上的所有局部变量(JIT)

4、栈上传入的参数变量

5、寄存器中的变量

注意,只有引用类型的变量才被认为是根,值类型的变量永远不被认为是根。只有深刻理解引用类型和值类型的内存分配和管理的不同,才能知道为什么root只能是引用类型。

顺带提一下JAVA,在Java中,可以当做GC Root的对象有以下几种:

1、虚拟机(JVM)栈中的引用的对象

2、方法区中的类静态属性引用的对象

3、方法区中的常量引用的对象(主要指声明为final的常量值)

4、本地方法栈中JNI的引用的对象

四、什么时候发生GC

1、当应用程序分配新的对象,GC的代的预算大小已经达到阈值,比如GC的第0代已满

2、代码主动显式调用System.GC.Collect()

3、其他特殊情况,比如,windows报告内存不足、CLR卸载AppDomain、CLR关闭,甚至某些极端情况下系统参数设置改变也可能导致GC回收

五、GC中的代

代(Generation)引入的原因主要是为了提高性能(Performance),以避免收集整个堆(Heap)。一个基于代的垃圾回收器做出了如下几点假设:

1、对象越新,生存期越短

2、对象越老,生存期越长

3、回收堆的一部分,速度快于回收整个堆

.NET的垃圾收集器将对象分为三代(Generation0,Generation1,Generation2)。不同的代里面的内容如下:

1、G0 小对象(Size<85000Byte)

2、G1:在GC中幸存下来的G0对象

3、G2:大对象(Size>=85000Byte);在GC中幸存下来的G1对象

  object o = new Byte[85000]; //large object
  Console.WriteLine(GC.GetGeneration(o)); //output is 2,not 0

ps,这里必须知道,CLR要求所有的资源都从托管堆(managed heap)分配,CLR会管理两种类型的堆,小对象堆(small object heap,SOH)和大对象堆(large object heap,LOH),其中所有大于85000byte的内存分配都会在LOH上进行。一个有趣的问题是为什么是85000字节?

代收集规则:当一个代N被收集以后,在这个代里的幸存下来的对象会被标记为N+1代的对象。GC对不同代的对象执行不同的检查策略以优化性能。每个GC周期都会检查第0代对象。大约1/10的GC周期检查第0代和第1代对象。大约1/100的GC周期检查所有的对象。

六、谨慎显式调用GC

GC的开销通常很大,而且它的运行具有不确定性,微软的编程规范里是强烈建议你不要显式调用GC。但你的代码中还是可以使用framework中GC的某些方法进行手动回收,前提是你必须要深刻理解GC的回收原理,否则手动调用GC在特定场景下很容易干扰到GC的正常回收甚至引入不可预知的错误。

比如如下代码:

void SomeMethod()
        {
            object o1 = new Object();
            object o2 = new Object();

            o1.ToString();
            GC.Collect(); // this forces o2 into Gen1, because it's still referenced
            o2.ToString();
        }

如果没有GC.Collect(),o1和o2都将在下一次垃圾自动回收中进入Gen0,但是加上GC.Collect(),o2将被标记为Gen1,也就是0代回收没有释放o2占据的内存

还有的情况是编程不规范可能导致死锁,比如流传很广的一段代码:

public class MyClass
    {
        private bool isDisposed = false;

        ~MyClass()
        {
            Console.WriteLine("Enter destructor...");

            lock (this) //some situation lead to deadlock
            {
                if (!isDisposed)
                {
                    Console.WriteLine("Do Stuff...");
                }
            }
        }
    }

通过如下代码进行调用:

var instance = new MyClass();

            Monitor.Enter(instance);
            instance = null;

            GC.Collect();
            GC.WaitForPendingFinalizers();
          
            Console.WriteLine("instance is gabage collected");

上述代码将会导致死锁。原因分析如下:

1、客户端主线程调用代码Monitor.Enter(instance)代码段lock住了instance实例

2、接着手动执行GC回收,主(Finalizer)线程会执行MyClass析构函数

3、在MyClass析构函数内部,使用了lock (this)代码,而主(Finalizer)线程还没有释放instance(也即这里的this),此时主线程只能等待

虽然严格来说,上述代码并不是GC的错,和多线程操作似乎也无关,而是Lock使用不正确造成的。

同时请注意,GC的某些行为在Debug和Release模式下完全不同(Jeffrey Richter在<<CLR Via C#>>举过一个Timer的例子说明这个问题)。比如上述代码,在Debug模式下你可能发现它是正常运行的,而Release模式下则会死锁。

七、当GC遇到多线程

这一段主要参考<<CLR Via C#>>的线程劫持一节。

前面讨论的垃圾回收算法有一个很大的前提就是:只在一个线程运行。而在现实开发中,经常会出现多个线程同时访问托管堆的情况,或至少会有多个线程同时操作堆中的对象。一个线程引发垃圾回收时,其它线程绝对不能访问任何线程,因为垃圾回收器可能移动这些对象,更改它们的内存位置。CLR想要进行垃圾回收时,会立即挂起执行托管代码中的所有线程,正在执行非托管代码的线程不会挂起。然后,CLR检查每个线程的指令指针,判断线程指向到哪里。接着,指令指针与JIT生成的表进行比较,判断线程正在执行什么代码。

如果线程的指令指针恰好在一个表中标记好的偏移位置,就说明该线程抵达了一个安全点。线程可在安全点安全地挂起,直至垃圾回收结束。如果线程指令指针不在表中标记的偏移位置,则表明该线程不在安全点,CLR也就不会开始垃圾回收。在这种情况下,CLR就会劫持该线程。也就是说,CLR会修改该线程栈,使该线程指向一个CLR内部的一个特殊函数。然后,线程恢复执行。当前的方法执行完后,他就会执行这个特殊函数,这个特殊函数会将该线程安全地挂起。然而,线程有时长时间执行当前所在方法。所以,当线程恢复执行后,大约有250毫秒的时间尝试劫持线程。过了这个时间,CLR会再次挂起线程,并检查该线程的指令指针。如果线程已抵达一个安全点,垃圾回收就可以开始了。但是,如果线程还没有抵达一个安全点,CLR就检查是否调用了另一个方法。如果是,CLR再一次修改线程栈,以便从最近执行的一个方法返回之后劫持线程。然后,CLR恢复线程,进行下一次劫持尝试。所有线程都抵达安全点或被劫持之后,垃圾回收才能使用。垃圾回收完之后,所有线程都会恢复,应用程序继续运行,被劫持的线程返回最初调用它们的方法。

实际应用中,CLR大多数时候都是通过劫持线程来挂起线程,而不是根据JIT生成的表来判断线程是否到达了一个安全点。之所以如此,原因是JIT生成表需要大量内存,会增大工作集,进而严重影响性能。

概念叙述到此结束,手都抄软了^_^,这书卖的贵和书里面的理论水平一样有道理。

这里再说一个真实案例。某web应用程序中大量使用Task,后在生产环境发生莫名其妙的现象,程序时灵时不灵,根据数据库日志(其实还可以根据Windows事件跟踪(ETW)、IIS日志以及dump文件),发现了Task执行过程中有不规律的未处理的异常,分析后怀疑是CLR垃圾回收导致,当然这种情况也只有在高并发条件下才会暴露出来。

八、开发中的一些建议和意见

由于GC的代价很大,平时开发中注意一些良好的编程习惯有可能对GC有积极正面的影响,否则有可能产生不良效果。

1、尽量不要new很大的object,大对象(>=85000Byte)直接归为G2代,GC回收算法从来不对大对象堆(LOH)进行内存压缩整理,因为在堆中下移85000字节或更大的内存块会浪费太多CPU时间

2、不要频繁的new生命周期很短object,这样频繁垃圾回收频繁压缩有可能会导致很多内存碎片,可以使用设计良好稳定运行的对象池(ObjectPool)技术来规避这种问题

3、使用更好的编程技巧,比如更好的算法、更优的数据结构、更佳的解决策略等等

update:.NET4.5.1及其以上版本已经支持压缩大对象堆,可通过System.Runtime.GCSettings.LargeObjectHeapCompactionMode进行控制实现需要压缩LOH。可参考这里

根据经验,有时候编程思想里的空间换时间真不能乱用,用的不好,不但系统性能不能保证,说不定就会导致内存溢出(Out Of Memory),关于OOM,可以参考我之前写过的一篇文章有效预防.NET应用程序OOM的经验备忘

之前在维护一个系统的时候,发现有很多大数据量的处理逻辑,但竟然都没有批量和分页处理,随着数据量的不断膨胀,隐藏的问题会不断暴露。然后我在重写的时候,都按照批量多次的思路设计实现,有了多线程、多进程和分布式集群技术,再大的数据量也能很好处理,而且性能不会下降,系统也会变得更加稳定可靠。

九、GC线程和Finalizer线程

GC在一个独立的线程中运行来删除不再被引用的内存。

Finalizer则由另一个独立(高优先级CLR)线程来执行Finalizer的对象的内存回收。

对象的Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间,并非和C++中一样在对象超出生命周期时立即执行析构函数。

GC把每一个需要执行Finalizer的对象放到一个队列(从终结列表移至freachable队列)中去,然后启动另一个线程而不是在GC执行的线程来执行所有这些Finalizer,GC线程继续去删除其他待回收的对象。

在下一个GC周期,这些执行完Finalizer的对象的内存才会被回收。也就是说一个实现了Finalize方法的对象必需等两次GC才能被完全释放。这也表明有Finalize的方法(Object默认的不算)的对象会在GC中自动“延长”生存周期。

特别注意:负责调用Finalize的线程并不保证各个对象的Finalize的调用顺序,这可能会带来微妙的依赖性问题(见<<CLR Via C#>>一个有趣的依赖性问题)。

最后感慨一下,反复看一本好书远远比看十本二十本不那么靠谱的书收获更多。

参考:

<<CLR Via C#>>

<<深入理解Java虚拟机>>

<<C# In Depth>>

<<Think In Java>>

How To: Use CLR Profiler | Microsoft Docs

.NET Column: Safe Thread Synchronization | Microsoft Docs

/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值