草庐IT

漫谈垃圾回收算法

thirteenAnimation 2023-03-28 原文

GC简介:垃圾回收(Garbage Collection)也被称为自动内存管理技术,在现代编程语言中使用得相当广泛,常见的 Java、Go、C# 均在语言的 runtime 中集成了相应的实现。

对象创建,内存分配

观察对象分配时,主要有三个参与者,Application,allocator,grabage collector ,接下来看一下这三者如何工作的

 

 

Application指的是我们的应用,我们将堆上的对象看作一个图,应用代码分配变量时就是在不停地修改这张堆对象图里的指向关系。

下图可以帮我们理解分配对象时堆上的操作

 

 

allocator 就很好理解了,指的是内存分配器,应用需要内存的时候都要向 allocator 申请。

allocator 要维护好内存分配的数据结构,在多线程场景下工作的内存分配器还需要考虑高并发场景下锁的影响,并针对性地进行设计以降低锁冲突。

collector 是垃圾回收器。

死掉的堆对象、不用的堆内存都要由 collector 回收,最终归还给操作系统。当 GC 扫描流程开始执行时,collector 需要扫描内存中存活的堆对象,扫描完成后,未被扫描到的对象就是无法访问的堆上垃圾,需要将其占用内存回收掉。

内存分配器

    内存分配器在应用和操作系统之间工作,当应用需要分配内容是,应用将一个结构或者一个分片申请内存分配,内存分配器判断当前已经分配的内存是否足够,足够时将当前内存start和offset返回给应用程序,不足够时向应用程序申请一块足够大小的内存。内存分配器的存在:1.隔离了应用程序多次向操作系统申请内存。2.内存碎片化管理。

栈内存由操作系统分配,堆内存由人为手动分配和释放,下表是总结的栈和堆的区别

 
速度
空间管理 高效,不会产生碎片 会产生内存碎片
访问权限 只能局部变量 可以访问全局变量
空间大小限制 操作系统限制 没有特定的限制
内存分配 连续 随机分配
分配和释放 编译器指令自动管理 手动管理
开销
主要问题 空间小 内存碎片
灵活性 固定大小 可以resize

堆内存手动管理,创建一个堆内存时,也需要将堆内对象的指针在栈中存储。比如string对象内部由char[]组成数据部分,内部由char*进行操作,新建一个string对象在内存堆中手动分配一块char[]大小的数据,在栈中创建了一个指向这块内存的指针。

栈内存系统管理,系统最底层对象都会有系统直接分配内存,比如:int,bool,char,float等,还有方法指针,当一个方法需要执行时,在栈中弹出该方法指针,执行完毕之后删除该指针。

垃圾回收算法

垃圾回收,也就是在内存中不用的内存回收,垃圾是字义。垃圾回收算法演变至今主要有三种

  • 引用计数法
  • 标记清楚法
  • 分代回收

引用计数

当对象E指向A时,A的对象计数器的ref_count+1;

       优点:

  • 回收时只需要判断ref_count为0时即可回收,回收简单
  • 最大暂停时间短,每次通过指向mutator生成垃圾时,这部分垃圾都会被回收,大幅削减了mutator的最大暂停时间

       缺点:

  • 计数器的值增减处理繁重每次指针更新时,计数器的值会被更新
  • 计数器要占很多位,假如32位的机器,就有可能2的32次方个对象同时引用一个对象,所以必须确保各对象的计数器有32位大小,也就是对于所有的对象,必须留有32位的空间,使内存使用大大降低
  • 循环引用无法回收,就是两个对象相互引用的情况,在相互引用时无法释放ref_count,导致两个对象都无法释放。

标记清除法

标记清除法,主要分为两部分,1.标记,2.清除

阶段1:Mark-Sweep 标记清除阶段
先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的。

阶段2:Compact 压缩阶段
对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使他们重新从heap基地址开始连续排列,类似于磁盘空间的碎片整理。

Heap内存经过回收、压缩之后,可以继续采用前面的heap内存分配方法,即仅用一个指针记录heap分配的起始地址就可以。主要处理步骤:将线程挂起=>确定roots=>创建reachable objectsgraph=>对象回收=>heap压缩=>指针修复

  • roots:heap中对象的引用关系错综复杂(交叉引用、循环引用),形成复杂的graph,roots是CLR在heap之外可以找到的各种入口点。GC搜索roots的地方包括全局对象、静态变量、局部对象、函数调用参数、当前CPU寄存器中的对象指针(还有finalizationqueue)等。主要可以归为2种类型:已经初始化了的静态变量、线程仍在使用的对象(stack+CPU register)
  • Reachable objects:指根据对象引用关系,从roots出发可以到达的对象。例如当前执行函数的局部变量对象A是一个rootobject,他的成员变量引用了对象B,则B是一个reachable object。从roots出发可以创建reachable objectsgraph,剩余对象即为unreachable,可以被回收
  • 指针修复:指针修复是因为compact过程移动了heap对象,对象地址发生变化,需要修复所有引用指针,包括stack、CPUregister中的指针以及heap中其他对象的引用指针
    Debug和release执行模式之间稍有区别,release模式下后续代码没有引用的对象是unreachable的,而debug模式下需要等到当前函数执行完毕,这些对象才会成为unreachable,目的是为了调试时跟踪局部对象的内容
    传给了COM+的托管对象也会成为root,并且具有一个引用计数器以兼容COM+的内存管理机制,引用计数器为0时这些对象才可能成为被回收对象
  • Pinnedobjects:指分配之后不能移动位置的对象,例如传递给非托管代码的对象,GC在指针修复时无法修改非托管代码中的引用指针,因此将这些对象移动将发生异常。
    pinnedobjects会导致heap出现碎片,但大部分情况来说传给非托管代码的对象应当在GC时能够被回收掉

STW 问题

标记清除算法开始时会将所有工作线程挂起,这就是“stop the world”将整个代码运行停止。假想一下,你的程序运行时必须要停止那么1~2s,要做垃圾回收,这是个很差的体验。

为什么一定需要挂起所有线程?

如果在获取到root Object之后,一边标记关联对象,一边运行程序,标记过的对象需要回收,未标记的对象重新恢复了引用,回收时直接就把内存数据搞乱了。导致程序奔溃。压缩和指针修复阶段,被回收的堆内存中存储了新创建的对象,压缩之后对象指针就指不到该对象内存块。

 

分代回收算法(C#垃圾回收算法)

分代算法的假设前提条件:
1、大量新创建的对象生命周期都比较短,而较老的对象生命周期会更长
2、对部分内存进行回收比基于全部内存的回收操作要快
3、新创建的对象之间关联程度通常较强。heap分配的对象是连续的,关联度较强有利于提高CPU cache的命中率

.NET将heap分成3个代龄区域: Gen 0、Gen 1、Gen 2

垃圾回收时,G0到达阈值后触发回收;剩下的对象进入到G1,G1到达阈值后触发G0、G1回收;剩下的对象进入到G2,G2到达阈值之后触发G0,G1,G2回收,也就是所谓的full GC。

G0与G1加起来总是保持在16M左右;Gen2的大小由应用程序确定,可能达到几G。回收时G0与G1在几毫秒至几十毫秒,G2回收一般需要几秒或者更久。

大致上来讲.NET应用运行期间2代、1代和0代GC的频率应当大致为1:10:100。如果该对象小于 85,000 字节,则将它置于 SOH 的段上,否则,将它置于 LOH 段。LOH是大对象段,超过85000字节之后在内存复制代价相对比较大,所以超过85000字节之后直接分配在G2,在触发FullGC时,才会判断是否要压缩优化大对象,不用从G0复制到G2。

 

分代回收从假设中成立,实际操作也是将不同大小的对象和使用时间不同的对象分配到不同的代中,回收时尽量缩短线程挂起的时间,在回收时实际使用的还是标记清除算法。

何时收集大型对象?

通常情况下,出现以下三种情形中的任一情况,都会执行 GC:

  • 分配超出第 0 代或大型对象阈值。

    阈值是某代的属性。 垃圾回收器在其中分配对象时,会为代设置阈值。 超出阈值后,会在该代上触发 GC。 因此,分配小型或大型对象时,需要分别使用第 0 代和 LOH 的阈值。 当垃圾回收器分配到第 1 代和第 2 代中时,将使用它们的阈值。 运行此程序时,会动态调整这些阈值。

    这是典型情况,大部分 GC 执行都因为托管堆上的分配。

  • 调用 GC.Collect 方法。

    如果调用无参数 GC.Collect() 方法,或另一个重载作为参数传递到 GC.MaxGeneration,将会一起收集 LOH 和剩余的托管堆。

  • 系统处于内存不足的状况。

    垃圾回收器收到来自操作系统 的高内存通知时,会发生以上情况。 如果垃圾回收器认为执行第 2 代 GC 会有效率,它将触发第 2 代。

 

 

C# 实战

public abstract class Animal
{
    public Animal(string name, int foot)
    {
        Name = name;
        Foot = foot;
    }
    /// <summary>
    /// 名字
    /// </summary>
    public  string Name { get; set; }
 
    /// <summary>
    ///脚
    /// </summary>
    public  int Foot { get; set; }
 
    /// <summary>
    /// 叫声
    /// </summary>
    public abstract void Call();
}
 
public class Dog:Animal
{
    public override void Call()
    {
        Console.WriteLine("汪汪汪");
    }
 
    public Dog(string name) : base(name, 4)
    {
 
    }
}
 
public static void Demo_With_ManualGC()
    {
        try
        {
            Animal animal = new Dog("小黑");
            var dog = animal;
            animal = null;
            System.GC.Collect();
            dog.Call();
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }
    }

运行上边的代码,发现animal并没有马上回收,是因为animal还有一个应用dog。

 

手动GC,不会回收还有引用的对象

将dog置为null,手动触发GC回收没有回收,这是为什么?

使用using+dispose手动设置回收时操作

使用using创建对象,调用了对象的Dispose方法,这个也就是IDisposable接口中对象释放使用的方法,在dog对象中没有释放对象,在using之外还可以使用对象。

 

使用析构函数,在对象被回收时进行操作

当对象释放时,会经过析构函数,析构函数和构造函数正好相反,一个是对象创建时,一个是对象销毁时。(这里模拟回收所以重复创建了多个对象)

手动指定对象是否支持回收

使用System.GC.SuppressFinalize(this);告诉标准语言库,不用回收这个对象。

有关漫谈垃圾回收算法的更多相关文章

  1. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

  2. 100个python算法超详细讲解:画直线 - 2

    1.问题描述使用Python的turtle(海龟绘图)模块提供的函数绘制直线。2.问题分析一幅复杂的图形通常都可以由点、直线、三角形、矩形、平行四边形、圆、椭圆和圆弧等基本图形组成。其中的三角形、矩形、平行四边形又可以由直线组成,而直线又是由两个点确定的。我们使用Python的turtle模块所提供的函数来绘制直线。在使用之前我们先介绍一下turtle模块的相关知识点。turtle模块提供面向对象和面向过程两种形式的海龟绘图基本组件。面向对象的接口类如下:1)TurtleScreen类:定义图形窗口作为绘图海龟的运动场。它的构造器需要一个tkinter.Canvas或ScrolledCanva

  3. ruby - 在 Ruby 中实现 Luhn 算法 - 2

    我一直在尝试用Ruby实现Luhn算法。我一直在执行以下步骤:该公式根据其包含的校验位验证数字,该校验位通常附加到部分帐号以生成完整帐号。此帐号必须通过以下测试:从最右边的校验位开始向左移动,每第二个数字的值加倍。将乘积的数字(例如,10=1+0=1、14=1+4=5)与原始数字的未加倍数字相加。如果总模10等于0(如果总和以零结尾),则根据Luhn公式该数字有效;否则无效。http://en.wikipedia.org/wiki/Luhn_algorithm这是我想出的:defvalidCreditCard(cardNumber)sum=0nums=cardNumber.to_s.s

  4. Ruby 斐波那契算法 - 2

    下面是我写的一个计算斐波那契数列中的值的方法:deffib(n)ifn==0return0endifn==1return1endifn>=2returnfib(n-1)+(fib(n-2))endend它工作到n=14,但在那之后我收到一条消息说程序响应时间太长(我正在使用repl.it)。有人知道为什么会这样吗? 最佳答案 Naivefibonacci进行了大量的重复计算-在fib(14)fib(4)中计算了很多次。您可以将内存添加到您的算法中以使其更快:deffib(n,memo={})ifn==0||n==1returnnen

  5. ruby-on-rails - 为什么 Devise/Omniauth 会向 URL 添加垃圾? - 2

    使用facebook登录后,我被重定向到/#_=_,其中显示主页。这种垃圾也出现在其他URL中,例如当注册失败并被重定向到/users/sign_in#_=_为什么会发生这种情况,我该如何解决? 最佳答案 如果你真的不想要它,一些简单的javascript就可以了:if(window.location.hash=="#_=_"){window.location.hash="";} 关于ruby-on-rails-为什么Devise/Omniauth会向URL添加垃圾?,我们在StackO

  6. ruby-on-rails - ActionMailer HTML 编码 hell - 特殊字符替换为垃圾 - 2

    我有UTF-8字符串:Website•Facebook那是中间的一颗子弹又名•或0xE20x800xA2此值已正确存储在数据库中,并使用默认设置使用Rails3和ruby​​1.9.3正确显示在屏幕上。我正在尝试通过HTML电子邮件发送此邮件,但是当一切都说完之后,接收端看到的是垃圾:这背后的代码很简单,我有一个ActionMailer子类(默认使用UTF-8)设置以在布局中发送带有UTF-8内容编码的HTML电子邮件:email.html.erb布局文件:"all"%>内容使用与呈现网页相同的View,重要的一行是:我已经尝试了很多很多force_encoding的排列,e

  7. ruby-on-rails - Rails add_index 算法 : :concurrently still causes database lock up during migration - 2

    为了防止在迁移到生产站点期间出现数据库事务错误,我们遵循了https://github.com/LendingHome/zero_downtime_migrations中列出的建议。(具体由https://robots.thoughtbot.com/how-to-create-postgres-indexes-concurrently-in概述),但在特别大的表上创建索引期间,即使是索引创建的“并发”方法也会锁定表并导致该表上的任何ActiveRecord创建或更新导致各自的事务失败有PG::InFailedSqlTransaction异常。下面是我们运行Rails4.2(使用Acti

  8. ruby - 趋势算法 - 2

    我正在开发一个类似微论坛的项目,其中一个特殊用户发布一条快速(接近推文大小)的主题消息,订阅者可以用他们自己的类似大小的消息来响应。直截了当,没有任何形式的“挖掘”或投票,只是每个主题消息的响应按时间顺序排列。但预计会有很高的流量。我们想根据它们引起的响应嗡嗡声来标记主题消息,使用0到10的等级。在谷歌上搜索了一段时间的趋势算法和开源社区应用示例,到目前为止已经收集到两个有趣的引用资料,但我还没有完全理解它们:Understandingalgorithmsformeasuringtrends,关于使用基线趋势算法比较维基百科页面浏览量的讨论,在SO上。TheBritneySpearsP

  9. Ruby - 不支持的密码算法 (AES-256-GCM) - 2

    我收到错误:unsupportedcipheralgorithm(AES-256-GCM)(RuntimeError)但我似乎具备所有要求:ruby版本:$ruby--versionruby2.1.2p95OpenSSL会列出gcm:$opensslenc-help2>&1|grepgcm-aes-128-ecb-aes-128-gcm-aes-128-ofb-aes-192-ecb-aes-192-gcm-aes-192-ofb-aes-256-ecb-aes-256-gcm-aes-256-ofbRuby解释器:$irb2.1.2:001>require'openssl';puts

  10. ruby - 符号的垃圾收集 Ruby 2.2.1 - 2

    所以从Ruby2.2+版本开始引入了符号垃圾回收。我在irb中编写了以下代码片段:before=Symbol.all_symbols.size#=>3331100_000.timesdo|i|"sym#{i}".to_symendSymbol.all_symbols.size#=>18835GC.startSymbol.all_symbols.size#=>3331因此,正如预期的那样,它收集了使用to_sym动态生成的所有符号。那么GC是如何知道收集哪些符号的呢?即使它们在程序中被引用,它会收集符号吗?符号垃圾回收是如何工作的?如果我创建的其中一个符号在程序中被引用,它还会收集它吗?

随机推荐