来源
图像操作,易内存泄露,边界像素
一、为什么需要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)
(一)、标记
目标:找出所有引用不为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..."); } } } }