GO进阶(5) 垃圾回收机制

news2024/11/17 5:56:38

      

一、前言


1、垃圾回收背景

      编程语言通常会使用手动和自动两种方式管理内存,C、C++ 以及 Rust 等编程语言使用手动的方式管理内存,工程师需要主动申请或者释放内存;而 Python、Ruby、Java 和 Go 等语言使用自动的内存管理系统,一般都是垃圾收集机制。这是Go语言成为高生产力语言的原因之一。将开发者从内存管理中释放出来,让开发者有更多的精力去关注软件设计,而不是底层的内存问题。

       学习java都知道,对垃圾收集器的印象都是暂停程序(Stop the world,STW),随着用户程序申请越来越多的内存,系统中的垃圾也逐渐增多;当程序的内存占用达到一定阈值时,整个应用程序就会全部暂停,垃圾收集器会扫描已经分配的所有对象并回收不再使用的内存空间,当这个过程结束后,用户程序才可以继续执行,Go 语言在早期也使用这种策略实现垃圾收集,但是今天的实现已经复杂了很多。

  2、什么是垃圾回收

      垃圾回收也称为GC (Garbage Collection),是一种自动内存管理机制

      在应用程序中会使用到两种内存,分别为堆(Heap)和栈(Stack) , GC负责回收堆内存,而不负责回收栈中的内存:栈是线程的专用内存,专门为了函数执行而准备的,存储着函数中的局部变量以及调用栈,函数执行完后,编译器可以将栈上分配的内存可以直接释放,不需要通过GC来回收。堆是程序共享的内存,需要GC进行回收在堆上分配的内存。

3、内存管理组件

垃圾回收器的执行过程被划分为两个半独立的组件:

1.赋值器(Mutator)∶这一名称本质上是在指代用户态的代码。因为对垃圾回收器而言,用户态的代码仅仅只是在修改对象之间的引用关系,也就是在对象图(对象之间引用关系的一个有向图)上进行操作。
2.回收器(Collector):负责执行垃圾回收的代码。

用户程序(Mutator)会通过内存分配器(Allocator)在堆上申请内存,而垃圾收集器(Collector)负责回收堆上的内存空间,内存分配器和垃圾收集器共同管理着程序中的堆内存空间。


 4、Go垃圾回收发展史

  • go1.1,提高效率和垃圾回收精确度。
  • go.13,提高了垃圾回收的精确度。
  • go1.4,之前版本的runtime大部分是使用C写的,这个版本大量使用Go进行了重写,让GC有了扫描stack的能力,进一步提高了垃圾回收的精确度。
  • go1.5,目标是降低GC延迟,采用了并发标记和并发清除,三色标记,write barrier写屏障,以及实现了更好的回收器调度,设计文档1,文档2,以及这个版本的[Go talk]。
  • go1.6,小优化,当程序使用大量内存时,GC暂停时间有所降低。
  • go1.7,小优化,当程序有大量空闲goroutine,stack大小波动比较大时,GC暂停时间有显著降低。
  • go1.8,write barrier写屏障切换到hybrid write barrier混合写屏障,以消除STW中的re-scan,把STW的最差情况降低到50us,设计文档。
  • go1.9,提升指标比较多,1)过去 runtime.GC, debug.SetGCPercent, 和 debug.FreeOSMemory都不能触发并发GC,他们触发的GC都是阻塞的,go1.9可以了,变成了在垃圾回收之前只阻塞调用GC的goroutine。2)debug.SetGCPercent只在有必要的情况下才会触发GC。
  • go.1.10,小优化,加速了GC,程序应当运行更快一点点。
  • go1.12,显著提高了堆内存存在大碎片情况下的sweeping性能,能够降低GC后立即分配内存的延迟。
  • 比较注意的是从go1.5开始使用的并发标记和三色标记法、写屏障为主要,再有就是从1.8之后的混合写屏障是比较重大的改动。
     

二、垃圾回收常见算法


引用计数:

每个对象维护一个引用计数,当被引用对象被创建或被赋值给其他对象时引用计数自动 +1。如果这个对象被销毁,那么计数-1,当计数为0时,回收该对象。

代表语言:Python、PHP、Swift
优点:对象可以很快被回收,不会出现内存耗尽或者达到阈值才回收。
缺点:不能很好的处理循环引用。


标记-清除:

从根变量开始遍历所有引用的对象,引用的对象标记“被引用”,没有标记的则进行回收。

代表语言:Golang(三色标记法) 。
优点:解决了引用计数的缺点。
缺点:需要 STW(stop the world),暂时停止程序运行。


分代收集:

按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。

代表语言:Java。
优点:回收性能好
缺点:算法复杂

1、go1.3使用的是标记清除法

标记清除(Mark-Sweep)算法是最常见的垃圾收集算法,标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:

标记阶段 — 从根对象出发查找并标记堆中所有存活的对象;

清除阶段 — 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表;

主要流程:

  • 进行STW(stop the worl即暂停程序业务逻辑),然后从main函数开始找到不可达的内存占用和可达的内存占用
  • 开始标记,程序找出可达内存占用并做标记
  • 标记结束清除未标记的内存占用
  • 结束STW停止暂停,让程序继续运行,循环该过程直到main生命周期结束。

用户程序在垃圾收集需要STW 。

2、三色标记法(go1.5垃圾回收原理)


1)为什么需要三色标记?

三色标记的目的,主要是利用Tracing GC做增量式垃圾回收,降低最大暂停时间。原生Tracing GC只有黑色和白色,没有中间的状态,这就要求GC扫描过程必须一次性完成,得到最后的黑色和白色对象。在前面增量式GC中介绍到了,这种方式会存在较大的暂停时间。

三色标记增加了中间状态灰色增量式GC运行过程中,应用线程的运行可能改变了对象引用树,只要让黑色对象直接引用白色对象,GC就可以增量式的运行,减少停顿时间。

2)、什么是三色标记?

黑色 Black:表示对象是可达的,即使用中的对象,黑色是已经被扫描的对象。
灰色 Gary:表示被黑色对象直接引用的对象,但还没对它进行扫描。
白色 White:白色是对象的初始颜色,如果扫描完成后,对象依然还是白色的,说明此对象是垃圾对象。
三色标记规则:黑色不能指向白色对象。即黑色可以指向灰色,灰色可以指向白色。

3)三色标记的主要流程:

、标记灰色对象:在垃圾收集器开始工作时,初始所有对象被标记为白色,寻找所有的Root根对象(比如被线程直接引用的对象)并标记成灰色。

  垃圾收集器只会从灰色对象集合中取出对象开始扫描,当灰色集合中不存在任何对象时,标记阶段就会结束。

三色标记垃圾收集器的工作原理很简单,我们可以将其归纳成以下几个步骤:

② 标记黑色、从灰色对象的集合中选择一个灰色对象并将其标记成黑色;

③标记灰色、将黑色对象指向的所有对象都标记成灰色,保证该对象和被该对象引用的对象都不会被回收;

④重复 、重复上述两个步骤直到对象图中不存在灰色对象;

       当三色的标记清除的标记阶段结束之后,应用程序的堆中就不存在任何的灰色对象,我们只能看到黑色的存活对象以及白色的垃圾对象,垃圾收集器可以回收这些白色的垃圾,下面是使用三色标记垃圾收集器执行标记后的堆内存,堆中只有对象 D 为待回收的垃圾:

因为用户程序可能在标记执行的过程中修改对象的指针,所以三色标记清除算法本身是不可以并发或者增量执行的,它仍然需要 STW,在如下所示的三色标记过程中,用户程序建立了从 A 对象到 D 对象的引用,但是因为程序中已经不存在灰色对象了,所以 D 对象会被垃圾收集器错误地回收。

 本来不应该被回收的对象却被回收了,这在内存管理中是非常严重的错误,我们将这种错误称为悬挂指针,即指针没有指向特定类型的合法对象,影响了内存的安全性, 想要并发或者增量地标记对象还是需要使用屏障技术。

4)、写屏障(屏障机制分为插入屏障和删除屏障)

插入屏障实现的是强三色不变式,

删除屏障则实现了弱三色不变式。

值得注意的是为了保证栈的运行效率,屏障只对堆上的内存对象启用,栈上的内存会在GC结束后启用STW重新扫描。

我们结合一段用户代码介绍写屏障,也是对上述Bug的一种代码表示:

A.Next = B
A.Next = &C{}

三色标记的扫描线程是跟用户线程并发执行的,考虑这种情况:

​ 用户线程执行完 A.Next = B 后,扫描线程把A标记为黑色,B标记为灰色,用户线程执行 A.Next = &C{} ,C是新对象,被标记为白色,由于A已经被扫描,不会重复扫描,所以C不会被标记为灰色,造成了黑色对象指向白色对象的情况,这种三色标记中是不允许的,结果是C被认为是垃圾对象,最终被清扫掉,当访问C时会造成非法内存访问而Panic。

​    

写屏障可以解决这个问题,当对象引用树发生改变时,即对象指向关系发生变化时,将被指向的对 象标记为灰色,维护了三色标记的约束:黑色对象不能直接引用白色对象,这避免了使用中的对象被释放。

​ 有写屏障后,用户线程执行 A.Next = &C{} 后,写屏障把C标记为灰色。

4、增量收集器:Go1.8三色标记 + 混合写屏障

       基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,所带来的性能瓶颈,Go在1.8引入了混合写屏障的方式实现了弱三色不变式的设计方式,混合写屏障分下面四步

  • GC开始时将栈上可达对象全部标记为黑色(不需要二次扫描,无需STW)
  • GC期间,任何栈上创建的新对象均为黑色
  • 被删除引用的对象标记为灰色
  • 被添加引用的对象标记为灰色
     

增量垃圾收集器:

增量式(Incremental)的垃圾收集是减少程序最长暂停时间的一种方案,它可以将原本时间较长的暂停时间切分成多个更小的 GC 时间片,虽然从垃圾收集开始到结束的时间更长了,但是这也减少了应用程序暂停的最大时间:

需要注意的是,增量式的垃圾收集需要与三色标记法一起使用,为了保证垃圾收集的正确性,我们需要在垃圾收集开始前打开写屏障,这样用户程序修改内存都会先经过写屏障的处理,保证了堆内存中对象关系的强三色不变性或者弱三色不变性。虽然增量式的垃圾收集能够减少最大的程序暂停时间,但是增量式收集也会增加一次 GC 循环的总时间,在垃圾收集期间,因为写屏障的影响用户程序也需要承担额外的计算开销,所以增量式的垃圾收集也不是只带来好处的,但是总体来说还是利大于弊。

并发垃圾收集器

并发(Concurrent)的垃圾收集不仅能够减少程序的最长暂停时间,还能减少整个垃圾收集阶段的时间,通过开启读写屏障、利用多核优势与用户程序并行执行,并发垃圾收集器确实能够减少垃圾收集对应用程序的影响:


 

虽然并发收集器能够与用户程序一起运行,但是并不是所有阶段都可以与用户程序一起运行,部分阶段还是需要暂停用户程序的,不过与传统的算法相比,并发的垃圾收集可以将能够并发执行的工作尽量并发执行;当然,因为读写屏障的引入,并发的垃圾收集器也一定会带来额外开销,不仅会增加垃圾收集的总时间,还会影响用户程序,这是我们在设计垃圾收集策略时必须要注意的。

三、GC触发和参数调节


1、触发GC(三种方式)

辅助GC

在分配内存时,会判断当前的Heap(堆内存)内存分配量是否达到了触发一轮GC的阈值(每轮GC完成后,该阈值会被动态设置,一般是之后的堆内存达到上一次垃圾收集的2倍时才会触发GC),如果超过阈值,则会启动一轮GC。

调用runtime.GC()强制启动一轮GC

sysmon是运行时的守护进程,当超过runtime.forcegcperiod(默认值是2分钟)没有运行GC会启动一轮GC
 

2、GC调节参数

Go垃圾回收不像Java垃圾回收那样,有很多参数可供调节,Go为了保证使用GC的简洁性,只提供了 一个参数GOGC。

​ GOGC代表了占用中的内存增长比率,达到该比率时应当触发1次GC,该参数可以通过环境变量设置。

该参数取值范围为0~100,默认值是100,单位是百分比。

​ 假如当前的heap占用内存时为3MB,GOGC = 75

5 * (1 + 75%) = 8.75MB

​ 等heap占用内存大小达到8.75MB会触发1轮GC。

​ GOGC还有两个特殊值:
1、“off”:代表关闭GC。
2、0:代表持续进行垃圾回收,只用于调试。

GOGC 设置很大,有的时候又容易触发 OOM

设置 GOGC 的弊端

1. GOGC 设置比率的方式不精确

设置 GOGC 基本上我们比较常用的 Go GC 调优的方式,大部分情况下其实我们并不需要调整 GOGC 就可以,一方面是不涉及内存密集型的程序本身对内存敏感程度太低,另外就是 GOGC 这种设置比率的方式不精确,我们很难精确的控制我们想要的触发的垃圾回收的阈值。

2. GOGC 设置过小

GOGC 设置的非常小,会频繁触发 GC 导致太多无效的 CPU 浪费,反应到程序的表现就会特别明显。举个例子,对于 API 接口来说,导致的结果的就是接口周期性的耗时变化。这个时候你抓取 CPU profile 来看,大部分的耗时都集中在 GC 的相关处理上。

如上图,这是一次 prometheus 的查询操作,我们看到大部分的 CPU 都消耗在 GC 的操作上。这也是生产环境遇到的,由于 GOGC 设置的过小,导致过多的消耗都耗费在 GC 上。

3. 对某些程序本身占用内存就低,容易触发 GC

对 API 接口耗时比较敏感的业务,如果  GOGC 置默认值的时候,也可能也会遇到接口的周期性的耗时波动。这是为什么呢?

因为这种接口本身占用内存比较低,每次 GC 之后本身占的内存比较低,如果按照上次 GC 后的 heap 的一倍的 GC 步调来设置 GOGC 的话,这个阈值其实是很容易就能够触发,于是就很容出现接口因为 GC 的触发导致额外的消耗。

4. GOGC 设置很大,有的时候又容易触发 OOM

那如何调整呢?是不是把 GOGC 设置的越大越好呢?这样确实能够降低 GC 的触发频率,但是这个值需要设置特别大才有效果。这样带来的问题,GOGC 设置的过大,如果这些接口突然接受到一大波流量,由于长时间无法触发 GC 可能导致 OOM。

 

四、GC调优


 GC 调优,主要是两方面:一方面减少用户代码分配内存的数量(即对程序的代码行为进行调优),另一方面最小化 Go 的 GC 对 CPU 的使用率(即调整 GOGC)。

     GC 的调优是在特定场景下产生的,并非所有程序都需要针对 GC 进行调优。只有那些对执行延迟非常敏感、当 GC 的开销成为程序性能瓶颈的程序,才需要针对 GC 进行性能调优,几乎不存在于实际开发中 99% 的情况。

1.控制内存分配的速度,限制Goroutine的数量,提高赋值器mutator的CPU利用率(降低GC的CPU利用率)

2.少量使用+连接string,避免重复扩容!

3.slice提前分配足够的内存来降低扩容带来的拷贝

4.避免map key对象过多,导致扫描时间增加

5.变量复用,减少对象分配,例如使用sync.Pool来复用需要频繁创建临时对象、使用全局变量等

6.增大GOGC的值,降低GC的运行频率
 

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

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

相关文章

Java八股——wait、sleep与park

sleep()、wait()、park()都可以使线程进入等待状态,但是3种方式在使用上和功能上都有些不同。 共同点: wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态它们都可以被打断唤醒都是native方法执行sleep…

java四种线程池(基本使用)

标题java四种线程池及使用示例 1、线程工厂 1、我们先来写ThreadFactory,在创建线程池时候可以传入自定义的线程工厂,线程工厂说白了就是用来定制线程的一些属性:名字、优先级、是否为守护线程。直接看代码即可。 当然创建线程池的时候可以…

控制台运行java

控制台执行java 新建java代码 新建一个记事本文件,将文件名改为HelloWorld.java,注意:后缀是.java。 若没有显示文件后缀,可以在资源管理器打开显示后缀,然后再次修改文件名,一定要修改成文件类型是java…

缺陷及缺陷管理

今日目标能够说出缺陷的判定标准能够说出描述缺陷的6大核心内容能够描述缺陷状态、严重程度、优先级的作用能够按照提供的缺陷模版完成一个缺陷的提交能够说出缺陷的跟踪流程能够在禅道中提交测试用例能够在禅道中提交缺陷1. 缺陷1.1 缺陷的定义(重点)产…

Python3.8.8-Django3.2-Redis-连接池-数据类型-字符串-list-hashmap-命令行操作

文章目录1.认识Redis1.1.优点1.2.缺点2.在Django中Redis的连接3.Redis的基础用法3.1.hashmap结构3.2.list结构4.命令行查看数据库5.作者答疑1.认识Redis Remote DIctionary Server(Redis) 是一个key-value 存储系统,是跨平台的非关系型数据库。是一个开源的使用 AN…

Linux入门篇-Linux目录结构

简介 简单介绍Linux目录。 Linux的⽬录结构 “/”表示根⽬录,根⽬录是Linux⽬录结构中的最顶级的⽬录,类似于windows的C:\ D:\ /boot⽬录:存放的是系统的启动配置⽂件和内核⽂件 /dev⽬录:存放的是Linux的设备⽂件 /etc⽬录&…

Nacos新手详细知识大全

官网&#xff1a;Spring Cloud Alibaba一、Nacos快速入门1.1 服务注册到Nacos1.1.1 配置**引入com.alibaba.cloud&#xff0c;**以后的版本就不用操心了<!-- com.alibaba.cloud--><!-- com.alibaba.cloud--><dependency><groupId>com.alibaba.cloud<…

SpringCloud之 LoadBalancer负载均衡

文章目录LoadBalancer 负载均衡一、LoadBalanced 负载均衡二、自定义负载均衡三、OpenFeign 实现负载均衡①添加依赖②启动类添加 EnableFeignClients③创建客户端接口 UserClient④service业务中调用客户端接口提示&#xff1a;以下是本篇文章正文内容&#xff0c;SpringCloud…

pv和pvc

一、PV和PVC详解当前&#xff0c;存储的方式和种类有很多&#xff0c;并且各种存储的参数也需要非常专业的技术人员才能够了解。在Kubernetes集群中&#xff0c;放了方便我们的使用和管理&#xff0c;Kubernetes提出了PV和PVC的概念&#xff0c;这样Kubernetes集群的管理人员就…

【Python】torch.einsum()解析

【Python】torch.einsum()解析 文章目录【Python】torch.einsum()解析1. 介绍2. 示例2.1 Pytorch矩阵乘法2.2 Numpy高阶张量3. 参考1. 介绍 爱因斯坦简记法&#xff1a;是一种由爱因斯坦提出的&#xff0c;对向量、矩阵、张量的求和运算 ∑\sum∑ 的求和简记法。在该简记法当中…

链表面试题

链表面试题1. 删除链表中等于给定值 val 的所有结点。2. 反转一个单链表。3. 给定一个带有头结点 head 的非空单链表&#xff0c;返回链表的中间结点。如果有两个中间结点&#xff0c;则返回第二个中间结点。4. 输入一个链表&#xff0c;输出该链表中倒数第k个结点。5. 将两个有…

延时任务的四种实现方式

什么是延迟任务&#xff1f;顾明思议&#xff0c;我们把需要延迟执行的任务叫做延迟任务。延迟任务的使用场景有以下这些&#xff1a;红包 24 小时未被查收&#xff0c;需要延迟执退还业务&#xff1b;每个月账单日&#xff0c;需要给用户发送当月的对账单&#xff1b;订单下单…

计算机网络之MAC和IP地址

MAC地址 在局域网中&#xff0c;硬件地址&#xff0c;又称为物理地址或MAC地址。 目前现在的局域网中实际上使用的都是6字节的MAC地址&#xff0c;所以每一个以太网设备都具有唯一的MAC地址。 MAC地址的格式 假设传输使用的是IP数据&#xff0c;V2的MAC帧较为简单&#xff…

Sonar:VSCode配置SonarLint/SonarLint连接SonarQube

需求描述 公司为项目代码配置了Sonar检测&#xff0c;希望在VSCode中开发项目时能够同步检测结果。 注意事项 SonarQube版本必须为7.9&#xff0c;否则SonarLint无法连接&#xff08;GitHub-SonarLint-Wiki第一行就有说明&#xff09;&#xff01;&#xff01;&#xff01;S…

【前端】一个更底层库-React基础知识点第2篇

目录属性状态PROPSPROP VALIDATIONSTATEFORMCONTROLLED COMPONENTSMIXINCOMPONENT APICOMPONENT LIFECYCLETOP API上一篇文章也是React基础知识点&#xff0c;了解到了React是什么&#xff1f;为什么要使用React&#xff1f;还知道了JSX概述&#xff0c;JSX嵌入变量&#xff0c…

python Django中的cookies和session会话保持技术

cookies和session都是为了保持会话状态而诞生的两个存储技术会话定义&#xff1a; 从打开浏览器访问一个网站&#xff0c;到关闭浏览器结束此次访问&#xff0c;称之为一次会话HTTP协议是无状态的&#xff0c;导致会话状态难以保持Cookies-定义 cookies是保存在客户端浏览器上的…

超简单的卷积和加法融合

神经网络的优化除了之前提到的一些硬件优化手段(AI硬件加速拾遗)之外&#xff0c;还有很多图层方面的优化手段。大家好啊&#xff0c;我是董董灿。 而且图层方面的优化&#xff0c;有时效果更佳。往往一个有效的优化&#xff0c;甚至可以“消除”掉一个算子的存在。 这里的“…

Vue组件-$refs、$nextTick和name属性的使用

Vue组件-$refs和$nextTick使用一、获取DOM二、$refs获取组件对象三、$nextTick异步更新DOM四、组件name属性的使用一、获取DOM 通过id或ref属性获取原生DOM 在mounted生命周期 – 2种方式获取原生DOM标签 目标标签 – 添加id / ref恰当时机, 通过id / 通过ref属性 获取目标标签…

Ubuntu系统新硬盘挂载

Ubuntu系统新硬盘挂载 服务器通常会面临存储不足的问题&#xff0c;大部分服务器都是ubuntu系统&#xff0c;该篇博客浅浅记载一下在ubuntu系统上挂载新硬盘的步骤。本篇博文仅仅记载简单挂载一块新的硬盘&#xff0c;而没有对硬盘进行分区啥的。如果需要更加完善的教程&#…

【C++】平衡二叉搜索(AVL)树的模拟实现

一、 AVL树的概念 map、multimap、set、multiset 在其文档介绍中可以发现&#xff0c;这几个容器有个共同点是&#xff1a;其底层都是按照二叉搜索树来实现的&#xff0c;但是二叉搜索树有其自身的缺陷&#xff0c;假如往树中插入的元素有序或者接近有序&#xff0c;二叉搜索树…