分代收集(generation)
经过大量实际观察得知,在面向对象编程语言中,绝大多数对象的生命周期都非常短。分代收集的基本思想是,将堆划分为两个或多个称为 代(generation)的空间。新创建的对象存放在称为 新生代(young generation)中(一般来说,新生代的大小会比 老年代小很多),随着垃圾回收的重复执行,生命周期较长的对象会被 提升(promotion)到老年代中。因此,新生代垃圾回收和老年代垃圾回收两种不同的垃圾回收方式应运而生,分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度非常快,比老年代快几个数量级,即使新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是因为大多数对象的生命周期都很短,根本无需提升到老年代。
标记-清扫
标记-清扫算法是第一种自动内存管理,基于追踪的垃圾收集算法。算法思想在 70 年代就提出了,是一种非常古老的算法。内存单元并不会在变成垃圾立刻回收,而是保持不可达状态,直到到达某个阈值或者固定时间长度。这个时候系统会挂起用户程序,也就是 STW,转而执行垃圾回收程序。垃圾回收程序对所有的存活单元进行一次全局遍历确定哪些单元可以回收。算法分两个部分:标记(mark)和清扫(sweep)。标记阶段表明所有的存活单元,清扫阶段将垃圾单元回收。
它有一个很明显的缺点,就是需要STW。
三⾊标记
⾸先当垃圾回收器第⼀次启动的时候,它把所有的对象都看成⽩⾊的,如果这个对象引⽤了另外⼀个对象,那么被引⽤的对象称之为灰⾊的,把灰⾊的放⼊⼀个队列⾥去,那么当它第⼀次扫描完了以后这个⽆⾮就是变成两种状态,⽩⾊的和灰⾊的,⽩⾊的不属于我们要管的。
接下来扫描所有灰⾊的对象,灰⾊对象从队列⾥拿出来进⾏扫描,灰⾊对象被拿出来以后灰⾊对象本⾝被标记为⿊⾊的。如果它引⽤了其他对象那么这个对象重新变成灰⾊的,它会放⼊队列⾥⾯去,那么⿊⾊对象肯定是活着的不⽤管了,那么通过这样⼀级⼀级的扫描最终因为灰⾊对象被放⼊队列⾥⾯然后灰⾊对象拿出来进⾏扫描,灰⾊对象本⾝变成⿊⾊的,最终⾥就变成两种对象,⼀种是活下来⿊⾊的,第⼆种是所有扫描都没有⼈碰过的⽩⾊,那么⿊⾊的都是活着的,⽩⾊的都是统统干掉的。
那么最早的扫描是从哪来的呢,我们称之为从根 Root 对象来的,⽣命周期可以保证的对象是根对象,线程栈本⾝就是⼀个根,线程栈⾥⾯可能存了某个对象的指针,那线程栈就会引⽤那个对象,所以像全局变量、线程栈这些就是根对象。从它们开始扫描,如果全局变量没有引⽤任何东⻄,线程栈也没有引⽤任何东⻄,那这些根对象引⽤的对象肯定可以干掉。全局变量就不说了,线程栈就表⽰了当前正在引⽤的那对象,如果线程栈都没有引⽤过,那些对象肯定不要了,⽩⾊对象可以去掉了。
从根对象开始扫描从⼀开始⼤家都是⽩的,如果根对象有引⽤,那个对象变成灰⾊的,灰⾊对象依次扫描以后就剩下变成两种对象,⽩⾊对象和灰⾊对象,⽩⾊对象先放在这,灰⾊对象放⼊队列⾥⾯去,接下来我们从队列⾥把灰⾊对象取出来,看看灰⾊对象引⽤了什么对象,灰⾊对象本⾝变成⿊⾊的它肯定活下来的,因为它是被别⼈引⽤了才会放⼊队列⾥⾯,所以它从灰⾊变成⿊⾊肯定是活下来的。通过这样把灰⾊对象⼀级⼀级进⾏递归扫描以后最后这个队列被清空了,剩下来的世界只有两种对象,⼀种是⿊⾊的肯定被引⽤过,第⼆种是没有被引⽤过的⽩⾊对象,⿊⽩两⾊,⿊⾊活着⽩⾊干掉,这就是很典型的三⾊标记
golang垃圾回收使用的标记清理
以前的STW(stop the world)
在扫描之前执⾏ STW(Stop The World)操作,就是Runtime把所有的线程全部冻结掉,所有的线程全部冻结掉意味着⽤户逻辑肯定都是暂停的,所有的⽤户对象都不会被修改了,这时候去扫描肯定是安全的,对象要么活着要么死着,所以会造成在 STW 操作时所有的线程全部暂停,⽤户逻辑全部停掉,中间暂停时间可能会很⻓,⽤户逻辑对于⽤户的反应就中⽌了。
现在的三色标记
如何减短这个过程呢, STW过程中有两部分逻辑可以分开处理。我们看⿊⽩对象,扫描完结束以后对象只有⿊⽩对象,⿊⾊对象是接下来程序恢复之后需要使⽤的对象,如果不碰⿊⾊对象只回收⽩⾊对象的话肯定不会给⽤户逻辑产⽣关联,因为⽩⾊对象肯定不会被⽤户线程引⽤的,所以回收操作实际上可以和⽤户逻辑并发的,因为可以保证回收的所有目标都不会被⽤户线程使⽤,所以第⼀步回收操作和⽤户逻辑可以并发,因为我们回收的是⽩⾊对象,扫描完以后⽩⾊对象不会被全局变量引⽤、线程栈引⽤。回收⽩⾊对象肯定不会对⽤户线程产⽣竞争,⾸先回收操作肯定可以并发的,既然可以和⽤户逻辑并发,这样回收操作不放在 STW时间段⾥⾯缩短 STW 时间。
写屏障
写屏障:该屏障之前的写操作和之后的写操作相比,先被系统其它组件感知。
刚把⼀个对象标记为⽩⾊的,⽤户逻辑执⾏了突然引⽤了它,或者说刚刚扫描了 100 个对象正准备回收结果⼜创建了1000个对象在⾥⾯,因为没法结束没办法扫描状态不稳定,像扫描操作就⽐较⿇烦。于是引⼊了写屏障的技术。
,先做⼀次很短暂的STW,为什么需要很短暂的呢,它⾸先要执⾏⼀些简单的状态处理,接下来对内存进⾏扫描,这个时候⽤户逻辑也可以执⾏。⽤户所有新建的对象认为就是⿊⾊的,这次不扫描了下次再说,新建对象不关⼼了,剩下来处理已经扫描过的对象是不是可能会出问题,已经扫描后的对象可能因为⽤户逻辑造成对象状态发⽣改变,所以对扫描过后的对象使⽤操作系统写屏障功能⽤来监控⽤户逻辑这段内存。
回收时间控制器
Java、 Go 语⾔都有垃圾回收预值,甚⾄来决定预值什么时候启动垃圾回收,像Go语⾔有百分⽐来控制到底有多⼤合适,这个 GC堆到底分配多⼤合理,这都需要在了解垃圾回收器原理情况下做动态调节。因为我们的服务程序很复杂,在服务器上可能⻓时间运⾏,垃圾回收器算法对性能影响很关键的.
Go 语⾔垃圾回收器⼀直被⼤家说实现的是原始版,因为Go早期版本对垃圾回收器预值怎么触发的特别蠢,第⼀次回收的时候回收完了剩下来对象是2M,那么下次垃圾回收的内存消耗变成4M,假设第⼀次回收之前内存是 100G,下次回收可能就变成 200G,可问题是下次回收⽤不了200G,可能第⼀次回收⽤的 100G 是引⽤了⼤字典,在下次回收之前这字典清空了接下来⼀直⽤⼏⼗M,垃圾回收器很难启动,所以 Go 语⾔在后台⽤⼀个循环线程扫描,每2分钟发现不执⾏就强制回收⼀次,这样的做法显然⽐较蠢。后来在 1.5 版本引⼊⼀个控制器,控制器有点像Java语⾔动态概念,当这次回收释放⽐例、或者是这些对象相关⼀些数据,控制器和辅助回收的作⽤GitChat来对预值动态调整决定下次回收
网友评论