前提:

本文参考和借鉴相关博客,相关版权归其所有,我只是做一个归纳整理,所以本文没有任何版权

参考文献和书籍:

CLR和.Net对象生存周期:   https://www.cnblogs.com/Wddpct/p/5547765.html

c#Finalize 和Dispose的区别:  https://www.cnblogs.com/Jessy/articles/2552839.html

《Lua设计与实现》——codedump 著

一、概要

本次对常见使用的c#和lua语言的gc操作原理和过程进行一次归类整理,加深对语言的理解,也为后续写出更优性能更好的代码做相关知识储备。

二、c#的垃圾回收

2.1 基本概念

1. CLR

CLR: Common Language Runtime, 公共语言运行时,是一种可以支持多种语言的运行时,其基本的核心功能包含:

  • 内存管理
  • 程序集加载和卸载
  • 类型安全
  • 异常处理
  • 线程同步

2. 托管模块

CLR并不关心是使用何种语言进行编程开发,只要编译器是面向CLR而进行编译的即可,这个中间的结果,就是IL(Intermediate Language), 最终面向CLR编译得到的结果是:IL语句以及托管数据(元数据)组成的托管模块
PS:

  • 元数据: 元数据的本质就是一种描述数据的数据

借鉴相关文章的图,其基本的过程为:

托管模块的基本组成:

  • PE32/PE32+(64位)
  • CLR头
  • 元数据
  • IL代码(托管代码)

3. 引用类型和值类型

这部分略过,基本都有相关的认识,本质是看其分配的内存位于内存堆上还是栈上。

  • 每个进程会分配一个对应的进程堆,这就是我们常说的程序内存申请区域,不同进程是不会有交叉的。在堆上还是在栈上进行内存分配,是没有速度差异的,都很快。

4. 垃圾回收器(Garbage Collector)

在CLR中的自动内存管理,就会使用垃圾回收器来执行内存管理,其会定时执行,或者在申请内存分配是发现内存不足时触发执行,也可以手动触发执行(System.GC.Collect)

垃圾回收的几种基本算法
  • 标记清除算法(Mark-Sweep)
    关键点是,清除后,并不会执行内存的压缩

  • 复制算法(Copying) 内存等额划分,每次执行垃圾回收后,拷贝不被回收的内存到没有被使用的内存块,自带内存压缩,弊端是内存浪费大(每次只能使用部分,预留部分给拷贝使用)

  • 标记整理算法(Mark-Compact)
    关键点,清除后,会执行内存压缩,不会有内存碎片

  • 分代收集算法(Generational Collection)
    对内存对象进行分代标记,避免全量垃圾回收带来的性能消耗。下文会详细讲解。

2.2 垃圾回收模型

1. 垃圾回收的目的

缘由: 内存是有限的,为了避免内存溢出,需要清理无效内存

2. 触发时机

  • 申请分配内存时内存不足(本身不足或者内存碎片过多没有足够大小的内存片)
  • 强制调用System.GC.Collect
  • CLR卸载应用程序域(AppDomain)
  • CLR正在关闭(后面2种在进程运行时不会触发)

3. 垃圾回收的流程

  • GC准备阶段
    暂停进程中的所有线程,避免线程在CLR检测根期间访问堆内存

  • GC的标记阶段
    首先,会默认托管堆上所有的对象都是垃圾(可回收对象),然后开始遍历根对象并构建一个由所有和根对象之间有引用关系的对象构成的对象图,然后GC会挨个遍历根对象和其引用对象,如果根对象没有任何引用对象(null)GC会忽略该根对象。
    对于含有引用对象的根对象以及其引用对象,GC将其纳入对象图中,如果发现已经处于对象图中,则换一个路径遍历,避免无限循环。

PS: 所有的全局和静态对象指针是应用程序的根对象。

  • 垃圾回收阶段 完成遍历操作后,对于没有被纳入对象图中的对象,执行清理操作

  • 碎片整理阶段
    如果垃圾回收算法包含这个阶段,则会对剩下的保留的对象进行一次内存整理,重新归类到堆内存中,相应的引用地址也会对应的整理,避免内存碎片的产生。

4. 分代垃圾回收的过程

分代的基本设计思路:

  • 对象越新,生命周期越短,反之也成立
  • 回收托管堆的一部分,性能和速度由于回收整个托管堆

基本的分代: 0/1/2:

  • 0代: 从未被标记为回收的新分配对象
  • 1代: 上一次垃圾回收中没有被回收的对象
  • 2代: 在一次以上的垃圾回收后任然未被回收的对象

操作图解释分代的过程:

  • 低一代的GC触发,移动到高一代后,未必会触发高一代的GC,只有高一代的内存不足时才会触发高一代的GC
  • 不同代的自动GC频率是可以设置的,一般0:1:2的频率为100:10:1

2.3 非托管对象的回收

对于非托管对象的管理,不受CLR的自动内存管理操作,这部分需要借鉴CLR的自动管理或者手动执行内存回收,这就是两种非托管对象的管理方式: Finalize和Dispose

非托管资源: 原始的操作系统文件句柄,原始的非托管数据库连接,非托管内存或资源

1.Finalize

System.Object定义了Finalize()虚方法,不能用override重写,其写法类似c++的析构函数:

class Finalization{
~Finalization()
{
//这里的代码会进入Finalize方法
Console.WriteLine("Enter Finalize()");
}
}

转换的IL:

基类方法放入到Finally中,其本质还是交给GC进行处理,只是其执行的时间不确定,是在GC完后在某个时间点触发执行Finalize方法,使用这个方法的唯一好处就是: 非托管资源是必然会被释放的。

2. IDisposable

继承了该接口,则需要实现Disposable接口,需要手动调用,这就确保了回收的及时性,对应的问题是如果不显示调用Dispose方法,则这部分非托管资源是不会被回收的。
c#中的using关键字,转换成IL语句,就是内部实现了IDispoable方法,最终的try/finally中,会在finally中调用dispose方法。

2.4 Unity中的C# GC

目前unity2018.4还是 Boehm–Demers–Weiser garbage collector, unity2019.1 中已经开始引入: Incremental Garbage Collection增量式垃圾回收功能,
相关链接: https://www.gamefromscratch.com/post/2018/11/27/unity-add-incremental-garbage-collection-in-20191.aspx

三、lua语言的垃圾回收

3.1 基本数据结构

lua的基本数据结构: union + type

typedef union Value{
GCObject* gc; //gc object
void* p; // light userdata
int b; // booleans
lua_CFunction f; // light c functions
lua_Integer i; //integer number 5.1为double,5.3为long long 8个字节
lua_Number n; // double number 5.3 为double 8个字节
} Value; struct lua_Value{
Value value_;
int tt_;
} TValue;

对于所有的需要被GC的对象,都会放在GCObject组成的链表中

3.2 GC算法和流程

1. 双色标记清除算法

在Lua5.0中的GC,是一次性不可被打断的操作,执行的算法是Mark-and-sweep算法,在执行GC操作的时候,会设置2种颜色,黑色和白色,然后执行gc的流程,大体的伪代码流程如下:

每个新创建的对象为白色

//初始化阶段
遍历root链表中的对象,并将其加入到对象链表中 //标记阶段
当前对象链表中还有未被扫描的元素:
从中取出对象并将其标记为黑色
遍历这个对象关联的其他所有对象:
标记为黑色 //回收阶段
遍历所有对象:
如果为白色:
这些对象没有被引用,则执行回收
否则:
这些对象仍然被引用,需要保留

整个过程是不能被打断的,这是为了避免一种情况:
如果可以被打断,在GC的过程中新创建一个对象
那么如果标记为白色,此时处于回收阶段,那么这个对象没有被扫描就会被回收;
如果标记为黑色,此时处于回收阶段,那么这个对象没有被扫描就会被保留
两种情况都不适合,所以只有让整个过程不可被打断,带来的问题就是造成gc的时候卡顿

2. 三色标记清除算法

虽然是三色,本质是四色,颜色分为三种:

白色: 当前对象为待访问状态,表示对象还未被gc标记过,也就是对象创建的初始状态; 同理,如果在gc完成后,仍然为白色,则说明当前对象没有被引用,则可以被清除回收

灰色: 当前对象为待扫描状态,当前对象已经被扫描过,但是其引用的其他对象没有被扫描

黑色: 当前对象已经扫描过,并且其引用的其他对象也被扫描过

其流程伪代码:

每个新创建的对象为白色

//初始化阶段
遍历root阶段中引用的对象,从白色设置为灰色,并放入到灰色节点列表中 //标记阶段
当灰色链表中还有未被扫描的元素:
从中去除一个对象并将其标记为黑色
遍历这个对象关联的其他所有对象:
如果是白色:
标记为灰色,并加入灰色链表中 //回收阶段
遍历所有对象:
如果为白色:
这些对象没有被引用,需要被回收
否则:
重新加入对象链表中等待下次gc

整个标记过程是可以被打断的,被打断后回来只需要接着执行标记过程即可,回收阶段是不可被打断的。

如何解决在标记阶段之后创建的对象为白色的问题?
分裂白色为两种白色,一种为当前白色 currentwhite, 一种为非当前白色 otherwhite,新创建的对象都为otherwhite,则在执行回收的时候,如果为otherwhite则不执行回收操作,等待下次gc的时候,会执行白色的轮换,则新创建的对象会进入下一轮gc。

3.3 lua gc的一些关键点

1. 初始化阶段的操作原理

以前我一直理解这个root就是将gcobject的链表进行转换到灰色链表中,其实并不是,而是去对当前虚拟机中的mainthread表, G表, registry表进行操作,其函数为:

static void markroot(lua_State * L)
{
global_State *g = G(L);
g->gray = NULL;
g->grayagain = NULL;
g->weak = NULL;
//标记几个入口
markobject(g, g->mainthread);
markvalue(g, gt(g->mainthread));
markvalue(g, registry(L));
markmt(g);
g->gcstate = GCSpropagte;
}

markobject/markvalue都是将对象从白色标记为灰色,所以这里面还有效的数据,就会最终进行扫描标记,如果最终不是白色,则会被保留,而执行回收操作的时候,是对gclist进行操作的,只要是currentwhite,那么就是可以被回收的。

2. 对于中途创建的对象的颜色处理

这儿会分为两种,前向操作和后退操作:
前向操作: 新创建对象为白色,被一个黑色对象引用,则将当前新创建对象标记为灰色
后退操作: 新创建对象为白色,被黑色对象引用,该黑色对象退回到灰色,塞入到grayagain表中,后续一次性扫描处理

对大部分数据,都是前向操作,对于table类型数据,则如果其新创建对象,该table会回退到灰色塞入到grayagain表中。
本质没区别,主要是table属于频繁操作的对象,如果反复将table中新创建的对象都设置成灰色,则灰色链表会容易变得很大,所以为了提高性能,就将table塞入到grayagain表中,后续一次性处理即可。

浅谈c#和lua的gc的更多相关文章

  1. 浅谈Java中的System.gc()的工作原理

    很多人把Java的“效率低下”归咎于不能自由管理内存,但我们也知道将内存管理封装起来的好处,这里就不赘述. Java中的内存分配是随着new一个新的对象来实现的,这个很简单,而且也还是有一些可以“改进 ...

  2. 【Unity游戏开发】浅谈Lua和C#中的闭包

    一.前言 目前在Unity游戏开发中,比较流行的两种语言就是Lua和C#.通常的做法是:C#做些核心的功能和接口供Lua调用,Lua主要做些UI模块和一些业务逻辑.这样既能在保持一定的游戏运行效率的同 ...

  3. 【转】.NET(C#):浅谈程序集清单资源和RESX资源 关于单元测试的思考--Asp.Net Core单元测试最佳实践 封装自己的dapper lambda扩展-设计篇 编写自己的dapper lambda扩展-使用篇 正确理解CAP定理 Quartz.NET的使用(附源码) 整理自己的.net工具库 GC的前世与今生 Visual Studio Package 插件开发之自动生

    [转].NET(C#):浅谈程序集清单资源和RESX资源   目录 程序集清单资源 RESX资源文件 使用ResourceReader和ResourceSet解析二进制资源文件 使用ResourceM ...

  4. Android性能优化的浅谈

    一.概要: 本文主要以Android的渲染机制.UI优化.多线程的处理.缓存处理.电量优化以及代码规范等几方面来简述Android的性能优化 二.渲染机制的优化: 大多数用户感知到的卡顿等性能问题的最 ...

  5. 浅谈线程池(中):独立线程池的作用及IO线程池

    原文地址:http://blog.zhaojie.me/2009/07/thread-pool-2-dedicate-pool-and-io-pool.html 在上一篇文章中,我们简单讨论了线程池的 ...

  6. 浅谈线程池(上):线程池的作用及CLR线程池

    原文地址:http://blog.zhaojie.me/2009/07/thread-pool-1-the-goal-and-the-clr-thread-pool.html 线程池是一个重要的概念. ...

  7. 浅谈Android应用性能之内存

    本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! 文/ jaunty [博主导读]在Android开发中,不免会遇到许多OOM现象,一方面可能是由于开 ...

  8. 浅谈开源项目Android-Universal-Image-Loader(Part 3.1)

    本文转载于:http://www.cnblogs.com/osmondy/p/3266023.html 浅谈开源项目Android-Universal-Image-Loader(Part 3.1) 最 ...

  9. 浅谈JS中的闭包

    浅谈JS中的闭包 在介绍闭包之前,我先介绍点JS的基础知识,下面的基础知识会充分的帮助你理解闭包.那么接下来先看下变量的作用域. 变量的作用域 变量共有两种,一种为全局变量,一种为局部变量.那么全局变 ...

随机推荐

  1. spring boot 中@Mapper和@Repository的区别

    0--前言 @Mapper和@Repository是常用的两个注解,两者都是用在dao上,两者功能差不多,容易混淆,有必要清楚其细微区别: 1--区别 @Repository需要在Spring中配置扫 ...

  2. 微信小程序——页面栈

    刚开始用小程序的时候没怎么在意页面的跳转,也没仔细看文档中说的页面栈的内容.只要能跳转就行,wx.navigateTo,wx.redirectTo 这些方法一顿乱用.最后在做一个十层页面(以前页面栈是 ...

  3. Spring Boot2 系列教程(三十)Spring Boot 整合 Ehcache

    用惯了 Redis ,很多人已经忘记了还有另一个缓存方案 Ehcache ,是的,在 Redis 一统江湖的时代,Ehcache 渐渐有点没落了,不过,我们还是有必要了解下 Ehcache ,在有的场 ...

  4. 关于token你需要知道的【华为云技术分享】

    版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/devcloud/article/detai ...

  5. IoT开发精英实战营招募啦!速来报名!

    具有了向上的力量,才能一眼望到山外的大地,蜿蜒的长河,人类精神的进步. --罗曼·罗兰<爱与死的搏斗> 七月流火,八月未央,九月授衣.说是长长长长的夏天,眨眼间,也早过了立秋而迎来处暑.距 ...

  6. 转:Java transient关键字使用小记

    哎,虽然自己最熟的是Java,但很多Java基础知识都不知道,比如transient关键字以前都没用到过,所以不知道它的作用是什么,今天做笔试题时发现有一题是关于这个的,于是花个时间整理下transi ...

  7. luogu P2296 寻找道路 |最短路

    题目描述 在有向图 G 中,每条边的长度均为 1,现给定起点和终点,请你在图中找一条从起点到终点的路径,该路径满足以下条件: 路径上的所有点的出边所指向的点都直接或间接与终点连通. 在满足条件 1 的 ...

  8. 基于 SOA 架构,创建 ego-search-web 项目-solr集群-zookeeper集群

    项目架构 Ego-search-web 服务的消费者,ego-rpc 服务提供者 建立 ego-search-web 项目 继承:ego 依赖:ego-common   ego-rpc-service ...

  9. LightOJ1199 Partition Game

    Alice and Bob are playing a strange game. The rules of the game are: Initially there are n piles. A ...

  10. python学习笔记—DataFrame和Series的排序

    更多大数据分析.建模等内容请关注公众号<bigdatamodeling> ################################### 排序 ################## ...