go的垃圾回收机制

GC垃圾回收机制: 浅析与理解

对垃圾回收进行分析前,我们先来了解一些基本概念

基本概念

  • 内存管理:内存管理对于编程语言至关重要。汇编允许你操作所有东西,或者说要求你必须全权处理所有细节更合适。C
    语言中虽然标准库函数提供一些内存管理支持,但是对于之前调用 malloc 申请的内存,还是依赖于你亲自 free
    掉。从C++、Python、Swift 和 Java 开始,才在不同程度上支持内存管理。
  • 内存压缩:对内存碎片进行压缩。(和win10的那个“内存压缩”不太一样啦)
  • win10内存压缩:物理内存已经见底,将一部分不常使用的内存数据打包压缩起来,等到有程序需要访问那些数据的时候,再解压缩出来。
  • 引用与指针:

    这是非常有害的,毫无疑问。结果将是不确定的(编译器能产生一些输出,导致任何事情都有可能发生),应该躲开写出这样代码的人除非他们同意改正错误。如果你担心这样的代码会出现在你的软件里,那么你最好完全避免使用引用,要不然就去让更优秀的程序员去做。

  1. 最后上附图,帮助理解
  1. 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
  2. 不能有NULL 引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
  3. 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
  4. 引用只是某块内存的别名。
  5. 实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用” 这东西? 答案是“用适当的工具做恰如其分的工作”。比如说,某人需要一份证明,本来在文件上盖                上公章的印子就行了,如果把取公章的钥匙交给他,那么他就获得了不该有的权利。(什么情况下,就用什么对策)
  6. 为什么还要说“只有指针,没有引用是一个重要改变?”?答案是虽然引用在某些情况下好用,但他也会导致致命错误。如下:
    char *pc = 0; // 设置指针为空值 char& rc = *pc; // 让引用指向空值

堆(heap)和栈(stack)

  1. 平常说的“堆栈”其实是栈。
  2. 栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
  3. 堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控    制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

程序的栈结构2.临时变量:包括函数的非静态局部变量以及编译器自动生成的其它临时变量  3.保存的上下文:包括在函数调用前后需要保存不变的寄存器值1.返回地址:一个main函数中断执行的执行点.  2.ebp:指向函数活动记录的一个固定位置,ebp又被称为帧指针.固定位置是,这样在函数返回的时候,ebp就可以通过这个恢复到调用前的值。  3.esp始终指向栈顶,因此随着函数的执行,它总是变化的。  4.入栈顺序:先压此次调用函数参数入栈,接着是main函数返回地址,然后是ebp等寄存器。

  1. Link:C程序的函数栈作用机理(这个讲得好,有实例,所以不再熬述)
  1. 就是它,先上图
    ]
  1. 程序的地址空间布局:
    程序运行靠四个东西:代码、栈、堆、数据段。代码段主要存放的就是可执行文件(通常可执行文件内,含有以二进制编码的微处理器指令,也因此可执行文件有时称为二进制文件)中的代码;数据段存放的就是程序中全局变量和静态变量;堆中是程序的动态内存区域,当程序使用malloc或new得到的内存是来自堆的;栈中维护的是函数调用的上下文,离开了栈就不可能实现函数的调用。
  2. 栈帧: 也叫活动记录,保存的是一个函数调用所需要维护的所有信息。如下:  1.函数的返回地址和参数


这里我们对比了解不同的 “找到需要标记的对象”的方法

可回收对象的判定

  • 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,   计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。如下图所示:

优点:引用计数收集器可以很快地执行,交织在程序的运行之中。这个特性对于程序不能被长时间打断的实时环境很有利。
缺点:很难处理循环引用,比如图中相互引用的两个对象则无法释放。
应用:Python 和 Swift 采用引用计数方案。
  • 可达性分析算法(根搜索算法)

从GC Roots(每种具体实现对GC Roots有不同的定义)作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。如下图所示:

  • 接下来补充几个概念帮助理解(以java为例):
  1. GC Roots对象:

    虚拟机栈(帧栈中的本地变量表)中引用的对象。
    方法区中静态属性引用的对象。
    方法区中常量引用的对象。
    本地方法栈中JNI引用的对象。
    
    本地方法栈则为虚拟机所使用的Native方法服务。
    Native方法是指本地方法,当在方法中调用一些不是由java语言写的代码或者在方法中用java语言直接操纵计算机硬件。
    JNI:Java Native Interface缩写,允许Java代码和其他语言写的代码进行交互。

    上述如图,关于root区域的详细解释参考这里



这里我们介绍几种不同的 “标记对象”的方法

可回收对象的标记

  • 保守法将所有堆上对齐的字都认为是指针,那么有些数据就会被误认为是指针。于是某些实际是数字的假指针,会背误认为指向活跃对象,导致内存泄露(假指针指向的对象可能是死对象,但依旧有指针指向——这个假指针指向它)同时我们不能移动任何内存区域。
  • 编译器提示法如果是静态语言,编译器能够告诉我们每个类当中指针的具体位置,而一旦我们知道对象时哪个类实例化得到的,就能知道对象中所有指针。这是JVM实现垃圾回收的方式,但这种方式并不适合JS这样的动态语言
  • 标记指针法标记指针法:这种方法需要在每个字末位预留一位来标记这个字段是指针还是数据。这种方法需要编译器支持,但实现简单,而且性能不错。V8采用的是这种方式。
  • 位图标记(Go语言为例)
  1. 非侵入式标记位定义
      既然垃圾回收算法要求给对象加上垃圾回收的标记,显然是需要有标记位的。一般的做法
      会将对象结构体中加上一个标记域,一些优化的做法会利用对象指针的低位进行标记,这
      都只是些奇技淫巧罢了。Go没有这么做,它的对象和C的结构体对象完全一致,使用的是
      非侵入式的标记位。
  2. 具体实现
      堆区域对应了一个标记位图区域,堆中每个字(不是byte,而是word)都会在标记位区域
      中有对应的标记位。每个机器字(32位或64位)会对应4位的标记位。因此,64位系统中
      相当于每个标记位图的字节对应16个堆中的字节。

    虽然是一个堆字节对应4位标记位,但标记位图区域的内存布局并不是按4位一组,而是
      16个堆字节为一组,将它们的标记位信息打包存储的。每组64位的标记位图从上到下依
      次包括:

    16位的 特殊位 标记位
    16位的 垃圾回收 标记位
    16位的 无指针/块边界 的标记位
    16位的 已分配 标记位

    这样设计使得对一个类型的相应的位进行遍历很容易。

    前面提到堆区域和堆地址的标记位图区域是分开存储的,其实它们是以        
      mheap.arena_start地址为边界,向上是实际使用的堆地址空间,向下则是标记位图区
      域。以64位系统为例,计算堆中某个地址的标记位的公式如下:

    偏移 = 地址 - mheap.arena_start
    标记位地址 = mheap.arena_start - 偏移/16 - 1移位 = 偏移 % 16标记位 = *标记位地址 >> 移位

    然后就可以通过 (标记位 & 垃圾回收标记位),(标记位 & 分配位),等来测试相应的位。

    (也就是说,本来64位是一个字,需要4位标记位。但是,为了与字长相对,16个标记位
      放一起(刚好一个字长)一起表示16个字。并且每类标记位都放在一起
      AA..AABB...BB)

  • 接下来补充几个概念帮助理解:
  1. 为什么要判断哪些是数据,哪些是指针?
      假设堆中有一个long的变量,它的值是8860225560。但是我们不知道它的类型是
      long,所以在进行垃圾回收时会把个当作指针处理,这个指针引用到了0x2101c5018位
      置。假设0x2101c5018碰巧有某个对象,那么这个对象就无法被释放了,即使实际上已
      经没任何地方使用它。

    由于没有类型信息,我们并不知道这个结构体成员不包含指针,因此我们只能对结构体
      的每个字节递归地标记下去,这显然会浪费很多时间。
      (能不能清除 变成了概率事件)。

  2. 垃圾收集器(CMS收集器为例)   几个阶段:

    初始标记
      并发标记
      最终标记
      筛选回收

    初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行
      GC Roots Trancing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继
      续运行而导致标记产生变动那一部分对象的标记记录,这个阶段的停顿时间比初始标记稍
      长一些,但远比并发标记时间短。

  3. stop the world
      因为垃圾回收的时候,需要整个的引用状态保持不变,否则判定是垃圾,等我稍后回
      收的时候它又被引用了,这就全乱套了。所以,GC的时候,其他所有的程序执行处于暂停
      状态,卡住了。
      这个概念提前引入,在这里进行对比,效果会更好些。与标记阶段对比,stop the world发生在回收阶段。


这里我们介绍几种不同的垃圾回收算法

垃圾回收算法

  • 标记清除算法 (Mark-Sweep)

标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。

优点是简单,容易实现。缺点是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。(因为没有对不同生命周期的对象采用不同算法,所以碎片多,内存容易满,gc频率高,耗时,看了后面的方法就明白了)

  • 分代回收算法

根据对象存活的生命周期将内存划分为若干个不同的区域。不同区域采用不同算法(复制算法,标记整理算法),这就是分代回收算法。

一般情况下将堆区划分为老年代(Old Generation)和新生代(Young
Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

1.新生代回收

新生代使用Scavenge算法进行回收。在Scavenge算法的实现中,主要采用了Cheney算法。

Cheney算法是一种采用复制的方式实现的垃圾回收算法。
它将内存一分为二,每一部分空间称为semispace。在这两个semispace中,一个处于使用状态,另一个处于闲置状态。
简而言之,就是通过将存活对象在两个semispace空间之间进行复制。

复制过程采用的是BFS(广度优先遍历)的思想,从根对象出发,广度优先遍历所有能到达的对象

优点:时间效率上表现优异(牺牲空间换取时间)
缺点:只能使用堆内存的一半

新生代的空间划分比例为什么是比例为8:1:1(不是按照上面算法中说的1:1)?

新创建的对象都是放在Eden空间,这是很频繁的,尤其是大量的局部变量产生的临时对
象,这些对象绝大部分都应该马上被回收,能存活下来被转移到survivor空间的往往不
多。所以,设置较大的Eden空间和较小的Survivor空间是合理的,大大提高了内存的使
用率,缓解了Copying算法的缺点。

8:1:1就挺好的,当然这个比例是可以调整的,包括上面的新生代和老年代的1:2的
比例也是可以调整的。

Eden空间和两块Survivor空间的工作流程是怎样的?

具体的执行过程是怎样的?

假设有类似如下的引用情况:
          +----- A对象
          |
根对象----+----- B对象 ------ E对象
          |
          +----- C对象 ----+---- F对象 
                           |
                           +---- G对象 ----- H对象

    D对象
在执行Scavenge之前,From区长这幅模样:
+---+---+---+---+---+---+---+---+--------+| A | B | C | D | E | F | G | H |        |
+---+---+---+---+---+---+---+---+--------+
那么首先将根对象能到达的ABC对象复制到To区,于是乎To区就变成了这个样子:
     allocationPtr
             ↓ 
+---+---+---+----------------------------+| A | B | C |                            |
+---+---+---+----------------------------+
 ↑
scanPtr
接下来进入循环,扫描scanPtr所指的A对象,发现其没有指针,于是乎scanPtr移动,变成如下这样
          allocationPtr
             ↓ 
+---+---+---+----------------------------+| A | B | C |                            |
+---+---+---+----------------------------+
     ↑
  scanPtr
接下来扫描B对象,发现其有指向E对象的指针,且E对象在From区,那么我们需要将E对象复制到allocationPtr所指的地方并移动allocationPtr指针:
                allocationPtr
                 ↓ 
+---+---+---+---+------------------------+| A | B | C | E |                        |
+---+---+---+---+------------------------+
     ↑
  scanPtr
中间过程省略,具体参考[新生代的垃圾回收具体的执行过程][3]

From区和To区在复制完成后的结果:
//From区
+---+---+---+---+---+---+---+---+--------+| A | B | C | D | E | F | G | H |        |
+---+---+---+---+---+---+---+---+--------+//To区
+---+---+---+---+---+---+---+------------+| A | B | C | E | F | G | H |            |
+---+---+---+---+---+---+---+------------+

最终当scanPtr和allocationPtr重合,说明复制结束。   注意:如果指向老生代我们就不必考虑它了。(通过写屏障)

对象何时晋升?

1.当一个对象经过多次新生代的清理依旧幸存。
2.如果To空间已经被使用了超过25%(后面还要进来许多新对象,不敢占用太多)
3.大对象
(其实这部分,包括次数,比例等,是视情况设置的。)

2.老生代回收

Mark-Sweep(标记清除)

标记清除分为标记和清除两个阶段。
主要是标记清除只清除死亡对象,而死亡对象在老生代中占用的比例很小,所以效率较高。

Mark-Compact(标记整理)

标记整理正是为了解决标记清除所带来的内存碎片的问题。

大体过程就是 双端队列标记黑(邻接对象已经全部处理),白(待释放垃圾),灰(邻
接对象尚未全部处理)三种对象.
 
标记算法的核心就是深度优先搜索.
  • 补充概念方便理解

1.触发GC(何时发生垃圾回收?)

一般都是内存满了就回收,下面列举几个常见原因:
GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。
GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发GC操作来释放内存。
GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。
GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。

2.写屏障(一个老年代的对象需要引用年轻代的对象,该怎么办?)

如果新生代中的一个对象只有一个指向它的指针,而这个指针在老生代中,我们如何判断
这个新生代的对象是否存活?为了解决这个问题,需要建立一个列表用来记录所有老生代
对象指向新生代对象的情况。每当有老生代对象指向新生代对象的时候,我们就记录下
来。
当垃圾回收发生在年轻代时,只需对这张表进行搜索以确定是否需要进行垃圾回收,而不
是检查老年代中的所有对象引用。

3.深度、广度优先搜索(为什么新生代用广度搜索,老生代用深度搜索)

深度优先DFS一般采用递归方式实现,处理tracing的时候,可能会导致栈空间溢出,所以一般采用广度优先来实现tracing(递归情况下容易爆栈)。
广度优先的拷贝顺序使得GC后对象的空间局部性(memory locality)变差(相关变量散开了)。
广度优先搜索法一般无回溯操作,即入栈和出栈的操作,所以运行速度比深度优先搜索算法法要快些。
深度优先搜索法占内存少但速度较慢,广度优先搜索算法占内存多但速度较快。

结合深搜和广搜的实现,以及新生代移动数量小,老生代数量大的情况,我们可以得到了解答。
时间: 2024-10-07 09:41:19

go的垃圾回收机制的相关文章

Java性能优化之JVM GC(垃圾回收机制)

Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我们需要记住一个单词:stop-the-world .它会在任何一种GC算法中发生.stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行.当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成.GC优化很多时候就是减少stop-the-world 的发生. JVM GC回收哪个区域内的垃圾? 需要注意的是,JV

JavaGC专家(1)—深入浅出Java垃圾回收机制

在学习GC之前,你首先应该记住一个单词:"stop-the-world".Stop-the-world会在任何一种GC算法中发生.Stop-the-world意味着 JVM 因为要执行GC而停止了应用程序的执行.当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态,直到GC任务完成.GC优化很多时候就是指减少Stop-the-world发生的时间. 按代的垃圾回收机制 在Java程序中不能显式地分配和注销内存.有些人把相关的对象设置为null或者调用Sy

CMS垃圾回收机制

详解CMS垃圾回收机制 原创不易,未经允许,不得转载~~~ 什么是CMS? Concurrent Mark Sweep. 看名字就知道,CMS是一款并发.使用标记-清除算法的gc. CMS是针对老年代进行回收的GC. CMS有什么用? CMS以获取最小停顿时间为目的. 在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景. CMS如何执行?  总体来说CMS的执行过程可以分为以下几个阶段: 3.1 初始标记(STW) 3.2 并发标记 3.3 并发预清理

Java 垃圾回收机制(早期版本)

Java 垃圾回收机制在我们普通理解来看,应该视为一种低优先级的后台进程来实现的,其实早期版本的Java虚拟机并非以这种方式实现的. 先从一种很简单的垃圾回收方式开始. 引用计数 引用计数是一种简单但是速度很慢的垃圾回收技术. 每个对象都含有要给引用计数器,当有引用连接至对象时,引用计数+1. 当引用离开作用域或者被置为null时,引用计数-1. 当发现某个对象的引用计数为0时,就释放其占用的空间.   这种方法开销在整个程序生命周期中持续发生,并且该方法有个缺陷,如果对象之间存在循环引用,可能

python的垃圾回收机制

进程空间 进程运行时需要在内核中占据一段内存空间,用以存储程序和数据. 每个进程空间分布如下所示: 进程空间的结构 text段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域.在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等. data段:数据段(data segment)通常用来存放程序中已初始化的全局变量数据段属于静态内存分配. bss段:bss(Block Started by Symbol) 通常用来存放程序中未初始化的

垃圾回收机制GC知识再总结兼谈如何用好GC(其他信息: 内存不足)

来源 一.为什么需要GC 应用程序对资源操作,通常简单分为以下几个步骤: 1.为对应的资源分配内存 2.初始化内存 3.使用资源 4.清理资源 5.释放内存 应用程序对资源(内存使用)管理的方式,常见的一般有如下几种: 1.手动管理:C,C++ 2.计数管理:COM 3.自动管理:.NET,Java,PHP,GO- 但是,手动管理和计数管理的复杂性很容易产生以下典型问题: 1.程序员忘记去释放内存 2.应用程序访问已经释放的内存 产生的后果很严重,常见的如内存泄露.数据内容乱码,而且大部分时候,

java JVM垃圾回收机制

Java语言出来之前,大家都在拼命的写C或者C++的程序,而此时存在一个很大的矛盾,C++等语言创建对象要不断的去开辟空间,不用的时候有需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的allocated,然后不停的~析构.于是,有人就提出,能不能写一段程序在实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢? 1960年 基于MIT的Lisp首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,而这时Java还没有出世呢!所以实际上GC并不是Jav

Java垃圾回收机制的工作原理

Java垃圾回收机制的工作原理 [博主]高瑞林 [博客地址]http://www.cnblogs.com/grl214 一.Java中引入垃圾回收机制的作用 当我们建完类之后,创建对象的同时,进行内存空间的分配,为了防止内存空间爆满,java引入了垃圾回收机制,将不再引用的对象进行回收,释放内存,循环渐进,从而防止内存空间不被爆满. 1.垃圾回收机制的工作原理 创建的对象存储在堆里面,把堆比喻为院子中的土地,把对象比喻为土地的管理者,院子比喻为java虚拟机,当创建一个对象时,java虚拟机将给

java语言及其垃圾回收机制简单概述

 一.java 语言概述 Java 语言是一门纯粹的面向对象编程语言,它吸收了c++语言的各种优点.又摈弃了c++里难以理解的多继承,指针等概念因此Java语言具有功能强大和简单易用两个特征. Java语言的几个重要概念如下: J2ME:主要用于控制移动设备和信息家电等有限存储设备 J2SE:整个java技术的核心和基础, J2EE:java技术中应用最最广泛的部分,它提供了企业应用开发相关的完整的解决方案. API: 核心类库 JRE:运行Java程序所必须的环境的集合,包含JVM标准实现及J

JVM垃圾回收机制入门

前言 数据库是大家会普遍重视的一个领域,异步通信一般用不到,虚拟机在大部分时候不会出问题,常被人忽视,所以我打算先学习虚拟机,从零单排Java高性能问题. 堆内存存储结构 Java6是以年代来规划内存的,而Java7的G1收集器则相反,这里以Java6为准. Survivor1和Survivor2是一样大的,必有一个始终为空,容量小于Eden. 垃圾回收机制 年轻代采用复制算法,当回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor上,然后清理掉Eden和刚才