python、go和java中的垃圾回收机制

常见的 GC 算法分别有:引用计数法、标记清除(Mark-Sweep)法、三色标记法、分代收集法。我们来看看python、go和java中分别用到了什么样的垃圾回收算法。

python

引用计数法

引用计数算法在每个对象都维护着一个内存字段来统计它被多少”部分”使用,每当有一个新的引用指向该对象时,引用计数器就+1 ,每当指向该引用对象失效时该计数器就-1 ,当引用数量为0的时候,则说明对象没有被任何引用指向,可以认定是”垃圾”对象。
优点
简单、实时(将处理垃圾时间分摊到运行代码时,而不是等到一次回收)
缺点
1.保存对象引用数会占用一点点内存空间
2.每次执行语句都可能更新引用数,不再使用大的数据结构时,会引起大量对象被回收
3.不能处理循环引用的情况

标记-清除

标记清除(Mark—Sweep)算法分为两个阶段:第一阶段是标记阶段,GC会把所有的活动对象打上标记,第二阶段是把那些没有标记的对象非活动对象进行回收。

分代回收

根据对象的存活周期的不同而将内存分为几块,存活时间越久的对象,越不可能在后面的程序中变成垃圾。我们的程序中的对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这样一些“长寿”对象,我们会减少在垃圾回收中扫描它们的频率。

Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。

Go

三色标记法

三色标记法是传统标记清除法 (Mark-Sweep)的一个改进,它是一个并发的 GC 算法。

  1. 首先创建三个集合:白、灰、黑。
  2. 将所有对象放入白色集合中。
  3. 然后从根节点开始遍历所有对象,把遍历到的对象从白色集合放入灰色集合。
  4. 之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
  5. 重复 4 直到灰色中无任何对象
  6. 通过write-barrier检测对象有变化,重复以上操作
  7. 收集所有白色对象(垃圾)

这个算法可以实现 “on-the-fly”,也就是在程序执行的同时进行收集,并不需要暂停整个程序。

但是也会有一个缺陷,可能程序中的垃圾产生的速度会大于垃圾收集的速度,这样会导致程序中的垃圾越来越多无法被收集掉。

写屏障

go在进行三色标记的时候并没有进行stw(Stop-The-World),也就是说此时的对象是可以修改的。
这样就会导致在进行扫描的过程中当某个对象被另一个goroutine改变,就会导致这个对象不会被扫描到,而被误认为是白色对象。
写屏障就是为了解决这个问题,go1.9中开始使用混合写屏障,伪代码如下

writerPointer(slot,ptr):
	shad(slot)
	if any stack is grey:
		shade(ptr)
		*slot=ptr

混合写屏障会同时标记写入目标的“新指针”和“原指针”。这样就可以避免并行过程中指针不被标记或者转移指针的情况。

JAVA

可达性分析

可达性分析基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。不能到达的则被可回收对象。
python、go和java中的垃圾回收机制_第1张图片

标记-清除算法(mark and sweep)

python中同样使用到了该算法。我们知道该算法是标记和清除两个阶段进行的,那么它的优缺点是什么呢?

优点: 简单

缺点:效率低,标记清除后会产生大量不连续的内存空间,导致程序在运行过程中如果需要分配较大的内存时,就会出现内存不足的情况,造成内存空间的浪费

标记-整理算法

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

copying算法

算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

分代回收算法(Generational Collector)

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。和python分代所不同的是,java中将对象分为年轻代,年老代和持久代。

年轻代(Young Generation)

  1. 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

  2. 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

  3. 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收

  4. 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

年老代(Old Generation)

  1. 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

  2. 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代(Permanent Generation)
 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

总结

python中是使用了引用计数+标记清楚+分代回收来进行垃圾回收的,而在python程序中触发垃圾回收的条件是:

  1. 被引用为0时,立即回收当前对象
  2. 达到了垃圾回收的阈值,触发标记-清除
  3. 当0代超过700,或1,2代超过10,垃圾回收机制将触发
  4. 手动调用gc.collect()

java语言中选择了可达性分析进行对象存活判断,而不是引用计数,主要也是因为java中软引用、弱引用、虚引用等多种引用方式使用引用计数并不能进行有效的存活判断,同时为了避免循环引用的问题,所以java选择了可达性分析的方式进行对象存活判断。
目前主流的虚拟机实现都采用了分代收集的思想,把整个堆区划分为新生代和老年代,Java堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收,年老代主要使用标记-整理垃圾回收算法
在java中触发垃圾回收的条件是:
(1)cpu空闲的时候
(2)在堆栈满了的时候
(3)主动调用 System.gc() 后尝试进行回收

go中则主要使用了于python和java不同的三色标记算法进行垃圾回收。Go的gc最舒服的应用场景是自身的分配行为不容易导致碎片堆积,并且程序分配新对象的速度不太高的情况,这种情况下go的垃圾回收是比java更高效的。相反的,当对象分配速度高的情况下,java的gc的优势就会明显体现了。

你可能感兴趣的:(python,GO,算法,java,python,go)