zoukankan      html  css  js  c++  java
  • 第十节:代

    代是CLR垃圾回收的一种机制,它唯一的目的是提升应用程序的性能,一个基于代的垃圾回收器做出了以下几点假设。

    1. 对象越新,生存期越短。
    2. 对象越老,生存期越长。
    3. 回收堆得一部分,速度快于回收整个堆。

    无数的研究证明,对于现今的大多数应用程序,这些假设都是成立的,而且这些假设影响了垃圾回收器的实现方式。本节将解释代的工作原理。

    托管堆在初始化时,不包含任何对象,添加到堆得对象称为第0代对象。简单的说,第0代对象是那些新构造的对象,垃圾回收器从未检查过他们。下图展示了一个新启动的应用程序,它分配了5个对象(从A到E)。过了一会,对象C和E变得不可达。

    CLR初始化时,它会为第0代对象选择一个预算容器,假定为256KB(实际容量可能有所不同)。所以,如果分配一个新对象造成第0代超过预算,就必须启动一次垃圾回收。

    假设对象A-E刚好占用256K。对象F分配时,垃圾回收器必须启动。垃圾回收器判定对象C和E为垃圾因此会压缩(碎片整理)对象D,使其与对象B相邻,顺便说一句,之所以第0代的预算容器是256K,是因为所有这些对象能装入CPU的L2缓存,使内存的压缩能以非常快的速度完成。在垃圾回收中存活的对象被认为第1代对象。第1代对象已经经历了垃圾回收器的一次检查 ,如下如所示:

    一次垃圾回收后,第0代就不包含任何对象了。和前面一样,新对象会分配到第0代中。如下图所示,应用程序继续运行,并新分配了对象F到K,另外,随着应用程序继续运行,对象B、H、G变的不可达,他们的内存将在某一时刻回收,

    现在假设分配新对象L会造成第0代超过其256K的预算。由于第0代达到了预算,所以必须启动垃圾回收。开始一次垃圾回收时,垃圾回收器必须决定检查哪些代。前面说过,当CLR初始时,它为第0代对象选择了一个预算。同样的,它还必须为第1代选择一个预算,假定为第1代选择的预算为2MB。

    开始一次垃圾回收时,垃圾回收器还检查第1代占据了多少内存。在本例中,由于第1代占据的内存远少于2MB,所以垃圾回收器只检查第0代的对象,回顾了一下基于代的垃圾回收器做出的假设,第一个假设是新创建的对象具有一个短的生命期。因此,第0代可能包含大量垃圾,所以对第0代的回收,可能回收比较多的内存。垃圾回收期会忽略第1代中的对象,从而加快垃圾回收的速度。

    显然,忽略第1代中的对象可增强垃圾回收器的性能。然而,对性能有更大提振作用的是现在不必遍历托管堆中的每个对象。如果一个根或者一个对象引用了老一代的某个对象。垃圾回收器就可以忽略老对象的所有内部引用,从而能更快的速度构造好可达的对象图。当然,老对象的字段也有可能引用新对象。为了确保对老对象中的这些已更新的字段进行检查,垃圾回收器利用了JIT编译器内部的一个机制。这个机制在对象的引用字段发生变化时,会更新一个位标志,这样一来,垃圾回收期就知道从上一次垃圾回收之后,哪些老对象已被写入。只有字段发生变化的老对象才需检查是否引用了第0代中的任何新对象。

    基于代的垃圾回收器还假设活的比较久的对象能继续活下去。也就是说,第1代对象在应用程序中很有可能是继续可达的。如果垃圾回收器检查第1代的对象很有可能找不到多少垃圾,结果是回收不了多少内存。因此,对第1代进行垃圾回收很可能是浪费时间,如果真有垃圾在第1代,它将呆在那里。

    所有幸存下来的第0代对象被提升为第1代的一部分。由于垃圾回收器没有检查第1代,所以对象B的内存并没有回收,及时它在上一次垃圾回收时已经不可达。同样,在一次垃圾回收后,第0代对象不包含任何对象,等着分配新对象。假定应用程序继续运行,并分配对象L到对象O,另外,在运行过程中,应用程序停止使用对象G,L和M,使他们变的不可达,

    假定分配对象P导致第0代对象超过预算,垃圾回收发生。由于第1代对象占据的内存仍小于2MB,所以垃圾回收期再次决定只回收第0代,忽略第1代中不可达的对象。

    回收后,堆得情况如下:

    第一代对象正在缓缓增长。假定第1代的增长导致它的所有对象占据了2MB内存。这时,应用程序继续运行,并分配对象P到S,使第0代对象达到它的预算容量。这时堆如下图所示:

    应用程序试图分配对象T时,由于第0代已满,所以必须开始垃圾回收,但是,这一次垃圾回收发现,第1代占据了太多内存,以至于达到第1代2Mb的预算。由于前几次对第0代进行回收时,第1代已经有很多对象变的不可达,所以这一次垃圾回收决定检查第1代和第0代所有对象,两次都被回收之后,堆得情况如下:

    下图所示:

    像前面一样,垃圾回收后,第0代的幸存者被提升至第1代,第1代的幸存者被提升到第2代,第0代再次空出来,准备迎接新对象的到来。第二次的对象已经经过了至少2次或更多次的检查。虽然到目前为止已经发生过多次垃圾回收,但只有在第1代达到它的预算时,才会检查第1代中的对象。而在此之前,一般都对第0代进行了好几次垃圾回收。

    CLR的托管堆只支持3代:第0代、第1代、第2代。没有第3代。CLR初始化时,会为每一代选择预算。如前所示,第0代的预算约为256K,第1代约为2Mb,D第2代约为10mb,同样的。预算的大小以提升性能为宜,预算越大,垃圾回收的频率越低。再次提醒你,性能的提升源于最开始的假设:新对象的生存期较短,老对象倾向于活的久一些。CLR的垃圾回收期是自调节的。这意味着垃圾回收期会在执行垃圾回收的过程中了解应用程序的行为。例如,假定应用程序构造了许多对象,但每个对象用的时间很短,在这种情况下,对第0代的垃圾回收会回收大量内存。实际上,第0代的所有对象都可能被回收。

    如果垃圾回收期发现在回收0代后存活下来的对象很少,就可能将第0代的预算从256KB降低128kB,已分配空间的减少,意味着垃圾回收将更频繁的发生,但垃圾回收器需要多的工作会减少,从而减少进程的工作集。实际上,如果第0代的对象都是垃圾,垃圾回收期就不必压缩任何内存,只需要让指针(NextObjPtr)指回第0代的起始处即可。

    另一方面,如果垃圾回收器回收了第0代,发现还有很多对象存活,没有多少内存被回收,就会增大第0代的预算,比如增大到521kb,现在垃圾回收的次数就减少了,但每次进行回收时,回收的内存要多得多。如果没有回收到足够的内存,垃圾回收器就会执行一次完整垃圾回收。如果还不够,就跑出OutofMemoryException异常。

    到目前为止,我只讨论了垃圾回收之后如何动态调整第0代的预算。但是,垃圾回收器还会用类似的启发式的算法调整第1代和第2代的预算,这些代被垃圾回收时,垃圾回收器会检查有多少内存被回收,以及有多少对象幸存,基于这些结果垃圾回收器可能要增大或减少这些代的预算。从而提升应用程序的整体性能。最终的结果是,垃圾回收器会根据应用程序要求的内存负载来自动优化。

  • 相关阅读:
    如何避免JavaScript的内存泄露及内存管理技巧
    【跟我一起学python吧】python chr()、unichr()和ord()
    阿里云,实力与担当并存!
    首届阿里白帽大会成功举办,用技术“肩天下”
    DataV数据可视化年度峰会——唤醒数据,看见未来
    支付宝移动端 Hybrid 解决方案探索与实践
    大数据上云第一课:(1)MaxCompute授权和外表操作躲坑指南
    函数计算支持应用中心功能
    Serverless 解惑——函数计算如何访问 MySQL 数据库
    开发函数计算的正确姿势——使用交互模式安装依赖
  • 原文地址:https://www.cnblogs.com/bingbinggui/p/4448468.html
Copyright © 2011-2022 走看看