jvm系列程序员
就是须要回收的对象。
做为编写程序的人,是能够作出“这个对象已经再也不须要了”这样的判断,但计算机是作不到的。所以,若是程序(经过某个变量等等)可能会直接或间接地引用一个对象,那么这个对象就被视为“存活”;与之相反,已经引用不到的对象被视为“死亡”。将这些“死亡”对象找出来,而后做为垃圾进行回收,这就是GC的本质。
就是判断对象是否可被引用的起始点。
至于哪里才是根,不一样的语言和编译器都有不一样的规定,但基本上是将变量和运行栈空间做为根。好了,用上面这两个术语,咱们来说一讲主要的GC算法。
标记清除(Mark and Sweep)是最先开发出的GC算法(1960年)。它的原理很是简单,首先从根开始将可能被引用的对象用递归的方式进行标记,而后将没有标记到的对象做为垃圾进行回收。
图1显示了标记清除算法的大体原理。图1中的(1)部分显示了随着程序的运行而分配出一些对象的状态,一个对象能够对其余的对象进行引用。图中(2)部分中,GC开始执行,从根开始对可能被引用的对象打上“标记”。大多数状况下,这种标记是经过对象内部的标志(Flag)来实现的。因而,被标记的对象咱们把它们涂黑。图中(3)部分中,被标记的对象所可以引用的对象也被打上标记。重复这一步骤的话,就能够将从根开始可能被间接引用到的对象所有打上标记。到此为止的操做,称为标记阶段(Mark phase)。
标记阶段完成时,被标记的对象就被视为“存活”对象。图1中的(4)部分中,将所有对象按顺序扫描一遍,将没有被标记的对象进行回收。这一操做被称为清除阶段(Sweep phase)。
在扫描的同时,还须要将存活对象的标记清除掉,以便为下一次GC操做作好准备。标记清除算法的处理时间,是和存活对象数与对象总数的总和相关的。
做为标记清除的变形,还有一种叫作标记压缩(Mark and Compact)的算法,它不是将被标记的对象清除,而是将它们不断压缩。
标记清除算法有一个缺点,就是在分配了大量对象,而且其中只有一小部分存活的状况下,所消耗的时间会大大超过必要的值,这是由于在清除阶段还须要对大量死亡对象进行扫描。复制收集(Copy and Collection)则试图克服这一缺点。在这种算法中,会将从根开始被引用的对象复制到另外的空间中,而后,再将复制的对象所可以引用的对象用递归的方式不断复制下去。
图2的(1)部分是GC开始前的内存状态,这和图1的(1)部分是同样的。图2的(2)部分中,在旧对象所在的“旧空间”之外,再准备出一块“新空间”,并将可能从根被引用的对象复制到新空间中。图中(3)部分中,从已经复制的对象开始,再将能够被引用的对象像一串糖葫芦同样复制到新空间中。复制完成以后,“死亡”对象就被留在了旧空间中。图中(4)部分中,将旧空间废弃掉,就能够将死亡对象所占用的空间一口气所有释放出来,而没有必要再次扫描每一个对象。下次GC的时候,如今的新空间也就变成了未来的旧空间。
经过图2咱们能够发现,复制收集方式中,只存在至关于标记清除方式中的标记阶段。因为清除阶段中须要对现存的全部对象进行扫描,在存在大量对象,且其中大部分都即将死亡的状况下,所有扫描一遍的开销实在是不小。而在复制收集方式中,就不存在这样的开销。
可是,和标记相比,将对象复制一份所须要的开销则比较大,所以在“存活”对象比例较高的状况下,反而会比较不利。这种算法的另外一个好处是它具备局部性(Lo-cality)。在复制收集过程当中,会按照对象被引用的顺序将对象复制到新空间中。因而,关系较近的对象被放在距离较近的内存空间中的可能性会提升,这被称为局部性。局部性高的状况下,内存缓存会更容易有效运做,程序的运行性能也可以获得提升。
引用计数(Reference Count)方式是GC算法中最简单也最容易实现的一种,它和标记清除方式差很少是在同一时间发明出来的。它的基本原理是,在每一个对象中保存该对象的引用计数,当引用发生增减时对计数进行更新。引用计数的增减,通常发生在变量赋值、对象内容更新、函数结束(局部变量再也不被引用)等时间点。当一个对象的引用计数变为0时,则说明它未来不会再被引用,所以能够释放相应的内存空间。
图3的(1)部分中,全部对象中都保存着本身被多少个其余对象进行引用的数量(引用计数),图中每一个对象右上角的数字就是引用计数。图中(2)部分中,当对象引用发生变化时,引用计数也跟着变化。在这里,由对象B到对象D的引用失效了,因而对象D的引用计数变为0。因为对象D的引用计数为0,所以由对象D到对象C和E的引用数也分别相应减小。结果,对象E的引用计数也变为0,因而对象E也被释放掉了。图3的(3)部分中,引用计数变为0的对象被释放,“存活”对象则保留了下来。你们应该注意到,在整个GC处理过程当中,并不须要对全部对象进行扫描。
实现容易是引用计数算法最大的优势。标记清除和复制收集这些GC机制在实现上都有必定难度;而引用计数方式的话,凡有些年头的C++程序员(包括我在内),应该都曾经实现过相似的机制,能够说这种算法是至关具备广泛性的。除此以外,当对象再也不被引用的瞬间就会被释放,这也是一个优势。其余的GC机制中,要预测一个对象什么时候会被释放是很困难的,而在引用计数方式中则是当即被释放的。并且,因为释放操做是针对每一个对象个别执行的,所以和其余算法相比,由GC而产生的中断时间(Pause time)就比较短,这也是一个优势。
引用计数方式的缺点另外一方面,这种方式也有缺点。引用计数最大的缺点,就是没法释放循环引用的对象。
图4中,A、B、C三个对象没有被其余对象引用,而是互相之间循环引用,所以它们的引用计数永远不会为0,结果这些对象就永远不会被释放。引用计数的第二个缺点,就是必须在引用发生增减时对引用计数作出正确的增减,而若是漏掉了某个增减的话,就会引起很难找到缘由的内存错误。引用数忘了增长的话,会对不恰当的对象进行释放;而引用数忘了减小的话,对象会一直残留在内存中,从而致使内存泄漏。若是语言编译器自己对引用计数进行管理的话还好,不然,若是是手动管理引用计数的话,那将成为孕育bug的温床。
最后一个缺点就是,引用计数管理并不适合并行处理。若是多个线程同时对引用计数进行增减的话,引用计数的值就可能会产生不一致的问题(结果则会致使内存错误)。为了不这种状况的发生,对引用计数的操做必须采用独占的方式来进行。若是引用操做频繁发生,每次都要使用加锁等并发控制机制的话,其开销也是不可小觑的。综上所述,引用计数方式的原理和实现虽然简单,但缺点也不少,所以最近基本上再也不使用了。如今,依然采用引用计数方式的语言主要有Perl和Python,但它们为了不循环引用的问题,都配合使用了其余的GC机制。这些语言中,GC基本上是经过引用计数方式来进行的,但偶尔也会用其余的算法来执行GC,这样就能够将引用计数方式没法回收的那些对象处理掉。