Go-基础回顾-GC回收算法原理篇

345 阅读7分钟

Go GC 知识点概要

  • 存在Gc
  • 1.5之前采用标记清除,之后采用三色标记加写屏障
  • 并行Gc
  • gc不分代
  • 无vm

内存模型

  • code area 方法区
  • static area 静态变量区域
  • heap 堆区域
  • stack 栈区域

image.png

Go 进程管理模型

  • 一个进程对应多个OS线程(与cpu数相同的活跃线程数)

www.php.cn/be/go/45616…

  • 线程模型是MPG模型:一个线程对应多个协程(goroutine)
  • 对于所有的goroutine来说,heap可以看作是共享的区域
  • 每个goroutine有自己的stack

一般程序的内存管理

stack会随着线程执行code area的stack frame,自动pop、push、remove;

stack里的变量、调用完毕,就随着出栈,自动销毁;

但是heap不会,heap内的对象,通常是用stack内或者其它heap对象内的指针变量,对heap内的对象进行操作;

这个也叫做对象引用;

垃圾回收,garbage collect,回收的就是heap的空间;

Gc方式

引用计数

过程

每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。

优点

实现简单

缺点

无法解决循环引用的问题。

image.png

①当出现左边绿色的情况时,假设外部对A的引用消除,此时因为A引用计数从1减为0,A将被清除。从而对B的引用也消除,B的计数减为0,GC将正确回收对象{A, B}

②然而若出现右边橙色状况,假设外部对E的引用消除,外部对于对象集{C,D,E}不再有引用,但他们之间出现循环引用现象,计数始终保持为1,导致{C,D,E}无法被回收。

参考

blog.csdn.net/m0_37860933…

标记/清除

过程

  • 标记 :就是遍历gcRoot,找到存活的对象,并给他们打上标记。

我理解就是沿着入口方法的调用链路遍历堆内的引用对象,然后打上标记。

  • 清除:在标记之后执行,将之前未标记上的对象给清除,然后将之前标记上的对象的标记置空。
    • 未标记即未被引用所以要删除

触发时机

通俗的讲就是程序发现内存不够的时候,gc线程就会触发将当前应用程序暂停,然后进行遍历打上标记,然后清除未打上标记的对象再清除之前的标记,最后程序恢复运行。

优点

占用空间比较小,但是效率比较低,而且会导致之前的排列杂乱无章,而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。

复制

过程

复制算法就是将内存一分为二,分为空闲区和活动区,当活动区的内存达到上限时,则将活动区的所有存活对象按照原来的顺序复制到空闲区,然后把活动区和空闲区相互置换,同时也把之前的活动区的不活跃对象(通过标记法寻找)清除。

优点

在于效率很高,但是效率高的代价就是占用两倍的内存。而且当对象存活率非常高的时候,这种开销是不可忽视的。

参考

www.cnblogs.com/ttylinux/p/…

标记/整理(标记/压缩)

过程

  • 标记:就是遍历gcRoot,找到存活的对象,并给他们打上标记。
  • 整理:移动所有之前标记的存活对象,且按照内存地址值排列,然后将具有内存地址值之后的所有内存清空。

解释

此算法标记过程同上(标记/清除算法)一样,而整理过程和复制算法一样,但是没有内存划分这么一说,少了不必要的内存开销。记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法

分代回收

Java 中采用分代回收,每一代采用以上不同的垃圾回收算法。

  • 新生代: 存活时间较短,复制效率高,虽然用了较多内存但是因为大部分存活时间较短所以会释放很多。
    • 复制算法
  • 老年代:存活时间均较长,复制的话也释放不了多少,反而造成拷贝开销。
    • 就地回收(标记清除/整理)

Go的Gc

要点

  • 三色标记: gray black white三色标记
    • 黑色:对象在这次GC中已标记,且这个对象包含的子对象也已标记
    • 灰色:对象在这次GC中已标记, 但这个对象包含的子对象未标记
    • 白色:对象在这次GC中未标记
  • 写屏障 write barrier

juejin.cn/post/684490…

  • gc-root可达性分析
  • 并发的标记/扫描
  • STW(start/stop the world)(开启/暂停所有用户协程)

参考

  • 原文

github.com/golang/go/b…

  • 翻译版本

www.jianshu.com/p/6dab8f25c…

Go的栈内存管理

Go由于协程的数量可以无限多,需要的栈内存也很多,所以,设计了stack cache pool的机制;

栈内存用来存储函数内变量;在golang函数、协程退出后,其占用的栈内存也会一同被释放。

栈内存管理的核心思想和堆内存很像;在分配时,首先查找线程内stackcache是否有足够的空间,如果有足够的空间,则进行分配,避免了线程间竞争,提高了效率;若线程内stackcache内存不足,则会向全局stackpool中申请一批stack,按照规格进行切分后,放入到线程的stackcache中,然后再次进行分配。

GC触发条件

自动垃圾回收的触发条件有两个:

  • 超过内存大小阈值
  • 达到定时时间

阈值是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发。比如一次回收完毕后,内存的使用量为5M,那么下次回收的时机则是内存分配达到10M的时候。也就是说,并不是内存分配越多,垃圾回收频率越高。 如果一直达不到内存大小的阈值呢?这个时候GC就会被定时时间触发,比如一直达不到10M,那就定时(默认2min触发一次)触发一次GC保证资源的回收。

注 - 其他 Java - ZGC

ZGC

JDK11之后Java可以采用的垃圾回收器,同时将原来的分代回收改为分页回收。

疑问:为什么分代回收改为分页回收会提升效率?

开启要求

  • 64位机器
    • 通过高位区间标记是否为垃圾,一般为44位之上或者42之上,节省空间。
      • 传统方式为扫描之后存储在单独开辟的空间里。
  • JDK11之后的版本。

阶段

  • 标记阶段
    • 初始标记阶段- Stop The World(暂停所有执行线程) 主要针对GC Roots(主线程入口的局部变量/静态变量等)
    • 并发标记 - Start The World
    • 再标记阶段 - Stop The World - 解决漏标问题(从GC Roots开始再次遍历一遍,由于之前并发标记过所有记录应该是有缓存,因此再次标记速度会很快)
      • 再标记解决漏标问题及速度问题需要再思考?
      • 漏标问题是由于并发标记阶段可能会发生对象引用关系的变更。
  • 并发转移阶段
    • 转移准备
      • 这个阶段主要做了什么?
        • 如图所示,我理解应该是寻找合适的空间
    • 初始化转移
      • Stop The World 我理解主要针对GC Roots进行复制转移
    • 并发转移
      • 遍历GC Roots进行全量转移
        • 会存在调用寻址变更问题(引入转发表)
        • 通过第二次ZGC对上次ZGC中记录进转发表中的数据进行修复,清空转发表。

如下图一次ZGC全流程示意图 image.png

参考

java 版垃圾回收 - 各类算法

blog.51cto.com/u_3664660/3…

GcRoot

blog.csdn.net/leishenop/a…

搞懂Go垃圾回收

juejin.cn/post/684490…

golang gc| go语言gc详解

zhuanlan.zhihu.com/p/115143370

总结描述

Go主进程启动,呼起线程池,创建与线程池数量对应的MP

垃圾回收时,垃圾回收协程呼起和P数量对应的回收线程,扫描前STW(stop the world),开启写屏障(记录扫描过程中引用关系发生改变的对象),标记(start the world) 关闭写屏障,执行清除。