golang的垃圾回收详解

news2024/11/15 11:19:07

golang的垃圾回收详解

一、三色标记法

  作为一门现代化的语言,golang与java一样,都在语言中内置了垃圾回收的功能,不需要程序员自己去回收堆内存。而垃圾回收中,最重要的两个部分就是垃圾检测算法以及垃圾回收算法。垃圾检测算法决定哪些对象是垃圾需要被回收,主要有引用计数法和三色标记法。垃圾回收算法决定如何回收内存,主要有标记清除,标记复制,标记压缩等。由于,引用计数法有循环引用的问题,故大部分的语言都是使用三色标记法来检测垃圾的。
  三色标记法需要从一些对象出发进行分析,这些对象是必然不能被回收的,如栈对象(栈是程序自动回收的,不归垃圾回收管理),全局变量等,它们会被记录起来,放到一个列表中,这个列表在java中就被称为GC ROOTS,golang也有类似的定义。三色标记法从GC ROOTS出发,通过层层引用,GC ROOTS可以间接引用到的对象(对象可达),就不是垃圾,而GC ROOTS无法间接引用到的对象(对象不可达),就是我们需要回收的垃圾,而使用算法分析对象可不可达的过程,也被称为可达性分析。
可达性分析

  三色标记法将对象分为三种颜色,黑色,灰色以及白色。

  • 黑色,黑色代表着对象是GC ROOTS可达的,即该对象是有用的,不能被回收。而且该对象引用的对象都已经被标记,一般标记的过程就是将该对象所有引用的对象加入灰色对象的队列。
  • 灰色,灰色代表该对象是GC ROOTS可达的,并且该对象引用的对象还没有被全部标记,一旦该对象引用的对象被全部标记,灰色就会变为黑色。
  • 白色,白色代表该对象是GC ROOTS不可达的或者当前还没有标记到该对象,一般GC(垃圾回收)开始时,GC ROOTS中的对象会全部标记为黑色,GC ROOTS引用的对象标记为灰色,其它的对象则是白色,当GC的标记过程结束以后,剩下的白色对象就是要清除的垃圾。
  三色标记法首先会将GC ROOTS中的对象全部标记为黑色,然后将GC ROOTS引用的对象标记为灰色,加入灰色对象队列。然后不断的扫描灰色对象队列中的对象,将灰色对象引用的对象全部标记为灰色并加入灰色对象队列。然后每扫描完成一个灰色对象,就将该对象标记为黑色,从灰色对象队列中删除,一直重复此过程,直到灰色对象队列中的对象都被扫描完毕。此时,再次扫描整个内存,剩下的白色对象就是需要处理的垃圾。

二、并发垃圾回收

  在垃圾回收的过程中,有一部分操作是必须要停止所有的用户线程,这被称为STW(stop the world)。STW时间的长短,是衡量一个垃圾回收算法好坏的一个重要因素。在Golang早期的时候,Golang的垃圾回收是串行的,所以STW时间特别长,达到几百毫秒,在后续的更新中,Golang垃圾回收进行了多次优化。Golang1.8后,STW停顿时间低于1ms。
  Golang垃圾回收一般分为2个阶段,标记和清除。而在Golang早期的时候, 标记和清除都要STW,并且标记和清除都是单线程执行。
单线程

  首先,标记需要扫描整个内存的对象,这也就意味着,内存越大,标记的时间越久,而STW的时间也会越久。其次,单线程只能使用单个cpu,无法最大化的使用多核服务器上的cpu资源。所以在Golang后面的优化中,就改用了多线程来清理垃圾。同时,清理阶段垃圾回收线程可以和用户线程一起并行执行,使STW时间降低了一些。
多线程  但是,标记阶段仍然会耗时几百毫秒,对于正常的程序来说,仍然是较难接受的。故必须要对标记进行优化,减少标记阶段的STW时间。golang的思路时,通过STW进行初始标记,然后退出STW状态,通过多个goroutine进行并发标记,标记完之后进入STW,进行再次标记以校准对象的状态,标记完成后,进行清理阶段的准备。最后退出STW状态,恢复用户goroutine,并启动多个垃圾清理协程进行垃圾清理。
多协程
  之所以需要再次STW进行最终标记,是因为并发标记时,如果用户协程和标记协程对同一个变量进行操作,会产生浮动垃圾(是需要处理的垃圾,但是标记算法误认为它不是垃圾)或者错误标记把有用的对象标记为垃圾。前者只是少回收一点垃圾,但是后者会导致用户的数据丢失,导致程序运行出错。

三、并发垃圾回收导致的问题

在这里插入图片描述
  如上图所示,当标记协程标记完C后,C已经变为黑色对象,而黑色对象是不会再次扫描的。然后用户断开了A对象对C对象的链接,所以C对象其实已经是垃圾了,可以被回收。但是由于C对象是黑色对象,所以本轮GC是不会回收C对象的,C对象就变成了浮动垃圾。当然,等到下一次GC,系统仍然可以正常回收C对象。
在这里插入图片描述  如上图所示,当标记协程扫描完B后,B已经变为黑色对象,此时D还没有被扫描,所以是灰色对象。然后用户让B对象引用H对象,并断开了D对象对H对象的链接。但是由于B对象是黑色对象,所以本轮GC不会被再次扫描,而只被B对象引用的H对象,仍然是白色对象,在清理阶段,就会被当场垃圾清除掉,导致程序出现问题。
  浮动垃圾的问题其实并不算严重,因为下次GC仍然可以正常回收,所以该问题可以不解决。但是错误标记对象导致对象被错误清理是不能接受的。要想解决错误标记的问题,就要让三色标记法满足三色不变式。

四、三色不变式

  三色不变式分为两种,强三色不变式和弱三色不变式。只要三色标记法满足其中一个,就不会出现错误标记的问题。

1.强三色不变式

  强三色不变式指的是,在三色标记法进行标记时,不允许黑色对象引用白色对象。
在这里插入图片描述

1.弱三色不变式

  弱三色不变式指的是,在三色标记法进行标记时,允许黑色对象引用白色对象,但是白色对象必须存在其他灰色对象对它的引用,或者有灰色对象对该白色对象是可达的。
在这里插入图片描述

五、插入写屏障

  要想在三色标记法中实现三色不变式,就必须要加入一些额外的操作。在golang中,主要是依靠写屏障来实现三色不变式。写屏障,就是在对变量进行赋值的时候编译器自动插入额外的操作,如下图所示。比如说,在把白色对象赋值给黑色对象时,自动把白色对象变为灰色对象,就可以满足强三色不变式。
在这里插入图片描述
  但是我们知道,大多数的情况下,我们都是操作栈上的变量,如果将所有的变量的赋值都加入写屏障,那么程序运行就会变慢。正常的赋值指令,翻译成汇编就是一条指令,如果加入写屏障维护三色不变式,那可能就要多加十几条汇编指令,导致程序运行GC时,将会耗费很多性能在写屏障的。所以,目前在golang实现的各个版本的写屏障,都是只对堆中变量使用写屏障,对栈中的变量则是不使用屏障。但是,如果只对堆中的变量使用写屏障,并发标记的时候毫无疑问会出问题,所以在写屏障的基础上优化出了两种解决方案,插入写屏障(incremental update)和删除写屏障(snapshot-at-the-beginning)。
  在golang v1.5中,golang使用的是插入写屏障(incremental update)。插入写屏障满足的是强三色不变式,在实际算法落地的过程中,插入写屏障实现的是,不管是黑色对象还是白色对象引用其它对象,都会把被引用的对象从白色对象变为灰色对象,如果被引用的对象是灰色或者黑色则不处理。插入写屏障GC的时候,步骤如下:

  1. 首先进行STW,开启GC,做完前置操作后退出STW。
  2. 然后并行将GC ROOTS中的对象标记为黑色,并将其引用的对象标记为灰色。
  3. 每当对象A引用一个对象X的时候,golang会调用IsStackAddr函数去判断对象A的地址是否是在栈上。如果地址位于堆中,golang就会将值标记为灰色,如果是地址位于栈中, 则不做处理。
  4. 等GC协程完成整个内存的标记。
  5. 再次进入STW,重新标记并扫描栈上的对象的所有可达的对象,防止栈上对象的可达对象被错误标记。
  6. 在STW中完成清理阶段的前置准备。
  7. 退出STW,开始并行清理垃圾。
    在这里插入图片描述

六、删除写屏障

  与插入写屏障不一样的地方在于,插入写屏障在GC开始时,并不需要把所有的栈上的对象标黑再开始。而删除写屏障需要在第一次STW需要将栈上的对象全部变为黑色对象才能开始,但是删除写屏障不需要在标记阶段结束时,再次去扫描栈上的对象。删除写屏障满足的是弱三色不变式。删除屏障GC的时候,步骤如下:

  1. 首先进行STW,开启GC。
  2. 将栈上的对象里面的对象全部标记为黑色,并将其引用的对象标记为灰色,然后退出STW。
  3. 然后并行将GC ROOTS中的对象标记为黑色,并将其引用的对象标记为灰色。
  4. 每次当用户改变一个对象X引用的对象时,golang会调用IsStackAddr函数去判断对象X的地址是否是在栈上。如果地址位于堆中,golang就会将对象X原来引用的对象标记为灰色,如果是地址位于栈中, 则不做处理。
  5. 等GC协程完成整个内存的标记。
  6. 再次进入STW,开始进行清理阶段的前置准备。
  7. 退出STW,开始并行清理垃圾。
    在这里插入图片描述

  删除写屏障的缺点在于:

  1. 初始STW时,需要将栈上的对象全部标记为黑色,并将其引用的对象标记为灰色,导致初始的STW时间变长。
  2. 所有对象断开的被引用对象都会标记为灰色对象,这就意味着,这些对象中的垃圾也能存活到下一次GC开始前。所以删除写屏障的浮动垃圾会多一些。

五、混合写屏障

  在golang v1.8中,综合了插入写屏障和删除写屏障,创造了一种新的写屏障,这种写屏障就被称为混合写屏障(hybrid write barrier)。混合写屏障在测试中显示,可以将STW的时间控制在50us以下。混合写屏障解决了插入写屏障扫描内存完成后需要额外重新标记栈区对象,并対栈区对象及其可达的对象进行可达性分析的缺点,也解决了删除写屏障需要在标记开始时,就必须把栈上的对象标记为黑色对象,并将其引用的对象标记为灰色的缺点。减少了GC过程中的STW时间。混合写屏障GC的工作流程如下:

  1. 首先进行STW,开启GC,做完前置操作后退出STW。
  2. GC开始后新分配的对象会被直接标记黑色。
  3. 然后并行将GC ROOTS中的对象标记为黑色,并将其引用的对象标记为灰色。注意在这个过程中,stack的标记是有先有后的,每个goroutine都有自己的stack,每当GC协程需要标记某个goroutine的stack时,就会停止goroutine的运行,然后在标记它对应的stack,最后在恢复goroutine。GC协程运行的过程中,除了当前正在被扫描的stack对应的goroutine不能运行以外,其它的goroutine都可以在其它的Processor上运行。
  4. 每当对象A引用一个对象X的时候,golang会调用IsStackAddr函数去判断对象A的地址是否是在栈上。如果是地址位于栈中, 则不做处理。如果地址位于堆中,golang就会将对象A原来引用的对象标记为灰色,如果当前调用它的goroutine的stack还没被扫描,就会将对象A新引用的对象标记为灰色。
  5. 等GC协程完成整个内存的标记。
  6. 再次进入STW,在STW中完成清理阶段的前置准备。
  7. 退出STW,开始并行清理垃圾。
    在这里插入图片描述

六、混合写屏障的思考

  混合写屏障的伪代码如下,其中slot是操作的变量的地址,*slot是该变量原来指向的对象的地址,ptr是将要赋值给slot的对象的地址。

writePointer(slot, ptr):
    shade(*slot)
    if current stack is grey:
        shade(ptr)
    *slot = ptr

  从代码中可以看到,混合写屏障对于每一个对象都会使用删除写屏障,而插入写屏障则只有在操作的goroutine的栈未被完全扫描之前才会启用。这也就意味着goroutine对应的栈一旦被扫描,GC认为只需要删除写屏障就能保证不会出现错误标记的情况了。
  首先,我们思考一下,为什么删除写屏障需要在最开始的时候将栈标记完毕。这是因为,如果删除写屏障的开始的时候没有将栈标记完毕,就会引发两个问题。

  1. 当把栈对象引用的白色对象赋值给黑色的堆对象,然后断开栈对象与白色对象的引用。由于不对栈对象使用删除写屏障,导致白色对象只有黑色对象引用,白色对象会被错误回收。
  2. 当把栈对象引用的白色对象赋值给黑色的栈对象,然后断开栈对象与白色对象的引用。由于不对栈对象使用删除写屏障,导致白色对象只有黑色对象引用,白色对象会被错误回收。

在这里插入图片描述
  所以,在第一次STW的时候就将整个栈区扫描,标记完毕,删除写屏障就不会出现上述问题。而混合写屏障没有扫描标记整个栈区这个环节,同样也会面临同样的问题。
  首先,我们分析一下第一个问题,这种情况下,只有对象的引用从栈区对象中,被复制到堆区对象才会有问题。因为堆区复制引用到栈区,或者堆区复制引用到堆区,当原来的堆对象断开引用都会触发混合写屏障,导致引用的对象被标记为灰色,而栈区复制引用到栈区,则可以看下面关于第二个问题的分析。问题1解决的方案就是伪代码中的

    if current stack is grey:
        shade(ptr)

  当对象的引用从栈区对象中,被复制到堆区对象时。如果该goroutine的栈区的对象还没被标记,则被复制的对象就会被标记为灰色对象,必然不会被错误回收。如果该goroutine的栈区的对象已经被全部标记,则复制的对象必然已经被标记为灰色或者黑色,也必然不会被回收,这就解决了问题1。
  然后再看下第2个问题,在golang语言,一个goroutine正常是无法访问到另一个goroutine的栈的,除非两种特殊的操作 —— channel和启动goroutine。所以在GC开启的情况下进行这两种操作的时候,就需要对栈对象启用混合写屏障来保证三色标记法能正确标记对象。在Proposal: Eliminate STW stack re-scanning中的Channel operations and go statements就对这个问题进行了解释。如果两个对象是同一个goroutine栈中的对象,则因为在GC协程标记某个栈时,对应的goroutine必定被挂起,所以当某个goroutine运行时,整个栈要么已经被标记为黑色,要么所有对象是白色的,不会出现这个goroutine的栈中一半栈对象是白色,而另一半栈对象是黑色。当栈对象互相复制自己的引用给同一个栈区的对象时,如果src栈对象和dst栈对象都是白色对象,那等下自然会被正常标记。如果都是黑色,那它们复制的引用对应的对象必然已被标记为灰色或者黑色,也不需要去特殊处理。

七、抢占试调度

  在golang早期的版本中(golang v1.0),使用的是协作试抢占。这就意味着,但GC需要STW的时候,需要等用户程序执行到safe-points(比如调用系统函数,读写channel,time.Sleep() ),但是如果用户程序一直没有执行到safe-points,那么GC永远不能完成STW。比如下面 这段代码

package main

import (
    "fmt"
)

func main() {
    for {
    }
}

  在golang v1.4之后,golang变为了抢占试调度。golang在启动后,会启动一个线程运行 /src/runtime/proc.go中的sysmon函数,一旦某个goroutine运行超过10ms,sysmon就会向Goroutine对应的Processor(GMP模型里的P)发送SIGURG,然后在一系列的处理后,将超时的goroutine停止,这样类似GC这样的STW操作就能正常完成,或者其它的goroutine就有机会在该Processor上运行。

七、官方源码注释和文章

  1. Proposal: Eliminate STW stack re-scanning
  2. mgc.go注释
  3. mbarrier.go注释
  4. Proposal: Non-cooperative goroutine preemption

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/391993.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《c++ primer笔记》第八章 IO库

前言 简单看一下就行 文章目录一、IO类1.1基本概念1.2管理输出缓冲二、文件输入输出2.1文件模式三、string流3.1istringstream3.2ostringstream一、IO类 1.1基本概念 ​ 我们常见的流有istream和ostream,这两个流都是有关输入和输出的,此外&#xff0c…

如何在SSMS中生成和保存估计或实际执行计划

在引擎数据库执行查询时执行的过程的步骤由称为查询计划的一组指令描述。​查询计划在SQL Server中也称为SQL Server执行计划,我们可以通过以下步骤来生成和保存估计或实际执行计划。 估计执行计划和实际执行计划是两种执行计划: 实际执行计划:当执行查询时,实际执行计划出…

Spring之实例化Bean(2)

Spring是非常复杂的一个框架,想要一篇博客就说完实例化Bean的全流程,那将注定会是是很长很长的篇博客,说实话,换做我自己是没有耐心看完的。 本章,我将会以一个最简单的例子来阐述Spring实例化Bean的过程,…

8.SpringSecurity中的核心过滤器-CsrfFilter

SpringSecurity中的核心过滤器-CsrfFilter Spring Security除了认证授权外功能外,还提供了安全防护功能。本文我们来介绍下SpringSecurity中是如何阻止CSRF攻击的。 一、什么是CSRF攻击 跨站请求伪造(英语:Cross-site request forgery&#…

uos 20 统信 fprintd 记录

uos 20 统信 fprintd 记录 sudo busctl deepin-authenticate.service /usr/lib/systemd/system/deepin-authenticate.service [Unit] DescriptionDeepin Authentication[Service] Typedbus BusNamecom.deepin.daemon.Authenticate ExecStart/usr/lib/deepin-authenticate/d…

Activty启动到显示的过程[二]

Activity的显示从handleResumeActivity()方法开始。 //ActivityThread.javaOverridepublic void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) {final ActivityClientRecord r performResumeActivity(token, finalStat…

LCD液晶段码驱动IC/LCD液晶驱动芯片VK2C22高抗干扰/抗噪,适用于汽车仪表/单相智能电表

产品型号:VK2C22A/B产品品牌:永嘉微电/VINKA封装形式:LQFP52/48、DICE(COB邦定片)、COG(邦定玻璃用)产品年份:新年份原厂,工程服务,技术支持!VK2C22A/B概述:VK2C22是一个点阵式存储映…

自然语言处理-基于预训练模型的方法-chapter3基础工具集与常用数据集

文章目录3.1NLTK工具集3.1.1常用语料库和词典资源3.1.2常见自然语言处理工具集3.2LTP工具集3.3pytorch基础3.3.1张量基本概念3.3.2张量基本运算3.3.3自动微分3.3.4调整张量形状3.3.5广播机制3.3.6索引与切片3.3.7降维与升维3.4大规模预训练模型3.1NLTK工具集 3.1.1常用语料库和…

2023年3月软考高项(信息系统项目管理师)报名走起!!!

信息系统项目管理师是全国计算机技术与软件专业技术资格(水平)考试(简称软考)项目之一,是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试,既属于国家职业资格考试,又是职称资…

图表控件LightningChart.NET 系列教程(十一):LightningChart 组件——添加至 Blend WPF 项目

LightningChart.NET 是一款高性能 WPF 和 Winforms 图表,可以实时可视化多达1万亿个数据点。可有效利用CPU和内存资源,实时监控数据流。同时,LightningChart使用突破性创新技术,以实时优化为前提,大大提升了实时渲染的效率和效果&…

网络编程 socket 编程(一)

1. C/S 架构 C/S 架构即客户端/服务端架构,B/S 架构(浏览器与服务端)也是 C/S 架构的一种。 C/S 架构与 socket 的关系:学习 socket 可以完成 C/S 架构的开发。 2. osi 七层 一个完整的计算机系统由硬件、操作系统以及应用软件…

Redis:主从同步

Redis:主从同步一. 概述二. 原理(1) 全量同步(2) 增量同步(3) 优化Redis主从集群三. 总结一. 概述 引入: Redis主从集群采用一个Master负责写,多个Slave负责读的方式(读多写少),那么如何让读取数据时多个从…

访问学者进入美国哪些东西不能带?

随着疫情的稳定,各国签证的逐步放开,成功申请到国外访问学者、博士后如何顺利的进入国外,哪些东西不能带,下面就随知识人网小编一起看一看。一、畜禽肉类(Meats, Livestock and Poultry)不论是新鲜的、干燥的、罐头的、真空包装的…

pycharm激活虚拟环境时报错:无法加载文件activate.ps1,因为在此系统上禁止运行脚本,Windows10系统

问题: ii_env\Scripts\activate : 无法加载文件 F:\gitlab\AutoFrame\ii_env\Scripts\Activate.ps1,因为在此系统上禁止运行脚本。 有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Policies。 所在…

34 openEuler使用LVM管理硬盘-创建并挂载文件系统

文章目录34 openEuler使用LVM管理硬盘-创建并挂载文件系统34.1 创建文件系统34.2 手动挂载文件系统34.3 自动挂载文件系统34 openEuler使用LVM管理硬盘-创建并挂载文件系统 在创建完逻辑卷之后,需要在逻辑卷之上创建文件系统并挂载文件系统到相应目录下。 34.1 创…

大型医院云HIS系统:采用前后端分离架构,前端由Angular语言、JavaScript开发;后端使用Java语言开发 融合B/S版电子病历系统

一套医院云his系统源码 采用前后端分离架构,前端由Angular语言、JavaScript开发;后端使用Java语言开发。融合B/S版电子病历系统,支持电子病历四级,HIS与电子病历系统均拥有自主知识产权。 文末卡片获取联系! 基于云计…

谷歌留痕霸屏平台有哪些?

谷歌留痕霸屏平台有哪些? 答案是:光算可以做谷歌留痕霸屏 我们要先了解什么是谷歌留痕霸屏平台这个概念。 很简单,就是你有哪些可以做排名的网站资源,一般情况下你不够专业,是没办法把这件事做好的。 通常你要做谷…

学python的第四天---基础(2)

一、三角形类型读入数组并排序的方法nlist(map(float,input().split())) c,b,asorted(n)list_1 list(map(float, input().split())) list_1.sort() list_1.reverse()lengthssorted(map(float,input().split(" ")),reverseTrue)二、动物写法一:d{" &…

css系统化学习

元素的语义化 SEO:搜索引擎优化 根据搜索引擎展示的规律,语义化的元素更容易被展示获得更多浏览量 字符编码 css历史 内联样式(inline) style"内容全写在等号后面,双引号里面,多个之间用;隔开" 内部样式(internal) style写在head里面,在title下面,不是在body内, …

Hadoop集群搭建,基于3.3.4hadoop和centos8【图文教程-从零开始搭建Hadoop集群】,常见问题解决

Hadoop集群搭建,基于3.3.4hadoop和centos8【小白图文教程-从零开始搭建Hadoop集群】,常见问题解决Hadoop集群搭建,基于3.3.4hadoop1.虚拟机的创建1.1 第一台虚拟机的创建1.2 第一台虚拟机的安装1.3 第一台虚拟机的网络配置1.3.1 主机名和IP映…