之垃圾回收机制GC

更新时间:2023-06-04 11:08:27 阅读: 评论:0

之垃圾回收机制GC
⼀、GC的必要性
  1、应⽤程序对资源操作,通常简单分为以下⼏个步骤:为对应的资源分配内存→初始化内存→使⽤资源→清理资源→释放内存。
  2、应⽤程序对资源(内存使⽤)管理的⽅式,常见的⼀般有如下⼏种:
  [1] ⼿动管理:C,C++
  [2] 计数管理:COM
  [3] ⾃动管理:,Java,PHP,GO…
  3、但是,⼿动管理和计数管理的复杂性很容易产⽣以下典型问题:
  [1] 程序员忘记去释放内存
拼读怎么教孩子  [2] 应⽤程序访问已经释放的内存
  产⽣的后果很严重,常见的如内存泄露、数据内容乱码,⽽且⼤部分时候,程序的⾏为会变得怪异⽽不可预测,还有Access Violation 等。
  、Java等给出的解决⽅案,就是通过⾃动垃圾回收机制GC进⾏内存管理。这样,问题1⾃然得到解决,问题2也没有存在的基础。
心理健康教育主题  总结:⽆法⾃动化的内存管理⽅式极容易产⽣bug,影响系统稳定性,尤其是线上多服务器的集群环境,程序出现执⾏时bug必须定位到某台服务器然后dump内存再分析bug所在,极其打击开发⼈员编程积极性,⽽且源源不断的类似bug让⼈厌恶。
⼆、GC是如何⼯作的
  GC的⼯作流程主要分为如下⼏个步骤:
  标记(Mark) →计划(Plan) →清理(Sweep) →引⽤更新(Relocate) →压缩(Compact)
  1、标记
  ⽬标:找出所有引⽤不为0(live)的实例
  ⽅法:找到所有的GC的根结点(GC Root), 将他们放到队列⾥,然后依次递归地遍历所有的根结点以及引⽤的所有⼦节点和⼦⼦节点,将所有被遍历到的结点标记成live。不会被考虑在内
  2、计划和清理
金秋野  [1] 计划
  ⽬标:判断是否需要压缩
  ⽅法:遍历当前所有的generation上所有的标记(Live),根据特定算法作出决策手写
  [2] 清理
  ⽬标:回收所有的free空间
  ⽅法:遍历当前所有的generation上所有的标记(Live or Dead),把所有处在Live实例中间的内存块加⼊到可⽤内存链表中去
  3、引⽤更新和压缩
  [1] 引⽤更新
  ⽬标:将所有引⽤的地址进⾏更新
  ⽅法:计算出压缩后每个实例对应的新地址,找到所有的GC的根结点(GC Root), 将他们放到队列⾥,然后依次递归地遍历所有的根结点以及引⽤的所有⼦节点和⼦⼦节点,将所有被遍历到的结点中引⽤的地址进⾏更新,包括弱引⽤。
  [2] 压缩
  ⽬标:减少内存碎⽚
  ⽅法:根据计算出来的新地址,把实例移动到相应的位置。
三、GC的根节点
  本⽂反复出现的GC的根节点也即GC Root是个什么东西呢?
  每个应⽤程序都包含⼀组根(root)。每个根都是⼀个存储位置,其中包含指向引⽤类型对象的⼀个指针。该指针要么引⽤托管堆中的⼀个对象,要么为null。
  在应⽤程序中,只要某对象变得不可达,也就是没有根(root)引⽤该对象,这个对象就会成为垃圾回收器的⽬标。
  ⽤⼀句简洁的英⽂描述就是:⽽且,Any object referenced by a GC root will automatically survive the next garbage collection.
  中可以当作GC Root的对象有如下⼏种:
  1、全局变量
  2、静态变量
  3、栈上的所有局部变量(JIT)
  4、栈上传⼊的参数变量
  5、寄存器中的变量
  注意,只有引⽤类型的变量才被认为是根,值类型的变量永远不被认为是根。因为值类型存储在堆栈中,⽽引⽤类型存储在托管堆上。
四、什么时候发⽣GC
  1、当应⽤程序分配新的对象,GC的代的预算⼤⼩已经达到阈值,⽐如GC的第0代已满;
中式田园风格
  2、代码主动显式调⽤System.GC.Collect();
  3、其他特殊情况,⽐如,windows报告内存不⾜、CLR卸载AppDomain、CLR关闭,甚⾄某些极端情况下系统参数设置改变也可能导致GC回收。
五、GC中的代
  代(Generation)引⼊的原因主要是为了提⾼性能(Performance),以避免收集整个堆(Heap)。⼀个基于代的垃圾回收器做出了如下⼏点假设:
  1、对象越新,⽣存期越短;
  2、对象越⽼,⽣存期越长;
  3、回收堆的⼀部分,速度快于回收整个堆。
  的垃圾收集器将对象分为三代(Generation0,Generation1,Generation2)。不同的代⾥⾯的内容如下:
  1、G0 ⼩对象(Size<85000Byte):新分配的⼩于85000字节的对象。
  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
  这⾥必须知道,CLR要求所有的资源都从托管堆(managed heap)分配,CLR会管理两种类型的堆,⼩对象堆(small object heap,SOH)和⼤对象堆(large object heap,LOH),其中所有⼤于85000byte的内存分配都会在LOH上进⾏。
  代收集规则:当⼀个代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, becau it's still referenced
蒋良超o2.ToString();
}
  如果没有GC.Collect(),o1和o2都将在下⼀次垃圾⾃动回收中进⼊Gen0,但是加上GC.Collect(),o2将被标记为Gen1,也就是0代回收没有释放o2占据的内存
  还有的情况是编程不规范可能导致死锁,⽐如流传很⼴的⼀段代码:
public class MyClass
鸭血豆腐
形容舞蹈的成语{
private bool isDispod = fal;
~MyClass()
{
Console.WriteLine("");
lock (this) //some situation lead to deadlock
{
if (!isDispod)
{
Console.WriteLine("");
}
}
}
}
  通过如下代码进⾏调⽤:
       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和Relea模式下完全不同(Jeffrey Richter在<<CLR Via C#>>举过⼀个Timer的例⼦说明这个问题)。⽐如上述代码,在Debug模式下你可能发现它是正常运⾏的,⽽Relea模式下则会死锁。
七、当GC遇到多线程
  前⾯讨论的垃圾回收算法有⼀个很⼤的前提就是:只在⼀个线程运⾏。⽽在现实开发中,经常会出现多个线程同时访问托管堆的情况,或⾄少会有多个线程同时操作堆中的对象。⼀个线程引发垃圾回收时,其它线程绝对不能访问任何线程,因为垃圾回收器可能移动这些对象,更改它们的内存位置。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:4.5.1及其以上版本已经⽀持压缩⼤对象堆,可通过System.Runtime.GCSettings.LargeObjectHeapCompactionMode进⾏控制实现需要压缩LOH。
九、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#>>⼀个有趣的依赖性问题)。

本文发布于:2023-06-04 11:08:27,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/82/859295.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:线程   回收   内存   对象   垃圾   可能
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图