C# 托管代码和垃圾回收

Search for a command to run...

No comments yet. Be the first to comment.
春节回家,跟老爸一起刷了小破球2。 之后忽然想到了这个话题:你愿意相信机器,还是人 (一) 春节的时候,陪老爸去配了一副眼镜。 因为前不久我也正好花重金配了一副,在之前做了很多的功课,诸如眼镜的前倾角、面弯、镜间距、单眼瞳高、单眼瞳距之类的知识,了解了一些配镜过程中应该注意的事项。 老家是一个小县城,虽然眼镜店遍地都是,但专业的还真没几个。在配镜的过程中,能注意到这些细节的眼镜店几乎没有。蔡司专业的三维定位的仪器,更是全县城都没有。 当我提出需要仪器辅助测量这些参数的时候,一个配镜师傅跟我说,...

哎,我一个个人项目基本上都没有超过过两位数的QPS的人,面试你天天问我缓存的这些问题,你们礼貌吗? 缓存穿透 是啥 当请求试图访问一个在缓存和数据库都不存在的 key时,因为缓存没有,所以请求会直接转发到数据库上。然而数据库里面也没有这个 key 的记录,所以也就没有办法将这个数据写入缓存。当下一次同样的请求来了的时候,还是会转发到数据库上。 在这种情况下,缓存基本上也就没用了,就像被穿透了一样,请求每次都会走到数据库,流量大时数据库可能会扛不住。 咋整 对请求进行校验。一些明显不合理的参数直...

Merge weight (Amazon 2022 OA)

#CSharp #PubSub 发布-订阅模式 有些时候,发布-订阅模式也会被称为观察者模式,但其实,如果细分的话,这两者之间还是有些细微的差别的。 观察者模式是在主题者(Subject)内部,维护一个观察者(Observer)列表。然后当主题者的状态发生变更的时候,主题者可以通知观察者发生了变化。观察者可以主动访问主题者,获得变更。也可以在通知观察者的时候,直接带上变更的消息。 在观察者模式中,主题者和观察者还是存在部分的耦合。因此,我们可以引入一个中介,来接触这种耦合。 类 A(发布者)发生...

托管代码就是执行过程交由运行时管理的代码。运行时一般是指 CLR,公共语言运行时。
CLR 负责提取托管代码、将其编译成机器代码,然后执行它。除此之外,运行时还负责自动内存管理、安全边界、类型安全等等。
如果在 .Net 里面直接调用 C/C++ 程序,此类代码也称为“非托管代码”。在非托管代码的环境中,操作系统将程序加载进内存,然后调用内部的二进制代码,所以从内存管理到安全等诸多因素都需要程序员自己处理。
正常使用 .Net 编写的代码,会先编译成中间语言 (IL)。执行的时候,CLR 会接管编译后的 IL,然后通过实时编译(JIT)将 IL 编译成可以在CPU上执行的机器码。这样 CLR 就能知道代码的确切作用,并管理代码。
初始化新进程时,运行时会为进程创建托管堆。
托管堆是一个连续的地址空间区域,托管堆内维护着一个指针,用它指向将在堆中分配的下一个对象的地址。最初的时候,该指针指向托管堆的基址。
当应用程序创建第一个引用类型时,将为托管堆的基址中的类型分配内存。 应用程序创建下一个对象时,垃圾回收器在紧接第一个对象后面的地址空间内为它分配内存。 只要地址空间可用,垃圾回收器就会继续以这种方式为新对象分配空间。
严格来说,必须在托管堆上分配所有非 null 引用类型对象和所有装箱值类型对象。
从托管堆中分配内存要比非托管内存分配速度快。 由于运行时通过为指针添加值来为对象分配内存,所以这几乎和从堆栈中分配内存一样快。 另外,由于连续分配的新对象在托管堆中是连续存储,所以应用程序可以快速访问这些对象。
垃圾回收器的优化引擎根据所执行的分配决定执行回收的最佳时间。
每个应用程序都有一组根。每个根或者指向托管堆中的对象,或者为空。 应用程序的根包含线程堆栈上的静态字段、局部变量和参数以及 CPU 寄存器。
垃圾回收器可以访问由实时 (JIT) 编译器和运行时维护的活动根的列表。垃圾回收器对照此根列表检查应用程序的根,并在此过程中创建一个图表,在其中包含所有可从这些根中访问的对象。
如果有一个对象,不在上面创建的图表中,那意味着这个对象将无法从应用程序的根中访问。这个时候,垃圾回收器就可以考虑释放为它们分配的内存。
回收的时候,垃圾回收器会检查托管堆,查找无法访问的对象所占据的地址块。当发现无法访问的对象的时候,GC 会通过内存复制的方法来压缩内存中可以访问的对象,并释放分配给不可访问对象的地址空间块。在压缩可访问的对象之后,GC 会更正原来指向这个对象的指针,使应用程序的根指向新地址中的对象。在 GC 完成之后,会将托管堆指针指向最后一个可访问的对象。
只有在发现了大量无法访问的对象的时候,才会压缩内存。
为了提高 GC 的性能,托管堆被划分为3代:第0代、第1代和第2代。
划分不同代的依据:
- 压缩托管堆的一部分内存比压缩整个托管堆速度快
- 较新的对象生存期较短,较旧的对象生存期较长
- 较新的对象趋向于互相关联,并且大致同时由应用程序访问
新创建的对象储存在第0代。在第0代满了的时候,GC 会执行回收,尝试释放第0代中的地址空间。在第0代的回收执行之后,GC 会将第0代的内存对象升级到第1代。
如果第0代托管堆的回收没有回收足够的内存,不能使应用程序成功完成创建新对象的尝试,垃圾回收器就会先执行第1代托管堆的回收,然后再执行第2代托管堆的回收。如果这样仍不能回收足够的内存,垃圾回收器将执行第2、1 和 0 代托管堆的回收。
每次回收后,垃圾回收器都会压缩第0代托管堆中的可访问对象并将它们升级至第1代托管堆。 第1代托管堆中未被回收的对象将会升级至第2代托管堆。 由于垃圾回收器只支持三个级别,因此第2代托管堆中未被回收的对象会继续保留在第2代托管堆中,直到在将来的回收中确定它们为无法访问为止。
如果新创建的对象是大型对象,它们将延续到大型对象堆 (LOH),这有时称为第 3 代。 第 3 代是在第 2 代中逻辑收集的物理生成。
非托管资源需要显式清除。
最常用的非托管资源类型是包装操作系统资源的对象(例如文件句柄、窗口句柄或网络连接)。GC 可以跟踪封装非托管资源的托管对象的生存期,但却无法具体了解如何清理资源。
创建封装非托管资源的对象时,建议在公共 Dispose 方法中提供必要的代码以清理非托管资源。 通过提供 Dispose 方法,对象的用户可以在使用完对象后显式释放其内存。 使用封装非托管资源的对象时,应该了解 Dispose 并在必要时调用它。
还必须提供一种释放非托管资源的方法,以防类型使用者忘记调用 Dispose。 可以使用安全句柄来包装非托管资源,也可以重写 Object.Finalize() 方法。
即使在物理内存压力(即物理内存的需求)较低的情况下也会使用页文件。 首次出现物理内存压力较高的情况时,操作系统必须在物理内存中腾出空间来存储数据,并将物理内存中的部分数据备份到页文件中。 该数据只会在需要时进行分页,所以在物理内存压力较低的情况下也可能会进行分页。
垃圾回收分为以下几个阶段:
因为第 2 代回收可以占用多个段,所以可以将已提升到第 2 代中的对象移动到时间较早的段中。 可以将第 1 代幸存者和第 2 代幸存者都移动到不同的段,因为它们已被提升到第 2 代。
通常,由于复制大型对象会造成性能代偿,因此不会压缩大型对象堆 (LOH)。 但是,在 .NET Core 和 .NET Framework 4.5.1 及更高版本中,可以根据需要使用 GCSettings.LargeObjectHeapCompactionMode 属性按需压缩大型对象堆。
垃圾回收器使用以下信息来确定对象是否为活动对象:
在垃圾回收启动之前,除了触发垃圾回收的线程以外的所有托管线程均会挂起。

//todo 内存虚拟地址和物理地址的转换过程