当前位置: 首页 > news >正文

C#性能优化基础:垃圾回收机制

  相信很多C#开发者都没有关注过内存问题,毕竟我们有垃圾自动回收机制,不用像C/C++那样,需要手动去释放。

  其实关于内存是自动还是手动回收释放,一直也是有争议的,像C/C++这样的开发者认为,内存这么珍贵,就应该让人去操作,怎么能让没有思维的机器去操作呢,而支持垃圾自动回收的开发者者认为,就是因为内存这么珍贵,才不能让人手动干预,机器更可靠!额,嗯,说的都对,都有道理,反正要么就是人靠谱,要么就是机器靠谱...

  好了,言归正传,在C#中,垃圾回收(Garbage Collection, GC)是一个自动内存管理机制,用于回收应用程序中不再被使用的对象的内存,因此,使用托管代码的开发人员无需编写执行内存管理任务的代码。C#中的垃圾回收是基于 .NET Framework 或 .NET Core/.NET 5+的公共语言运行时(CLR)的一部分。

  常用的垃圾分类方法有三种:引用计数法、标记删除法、分代回收法。

  引用计数法

    引用计数法,就是堆每个对象维护一个count字段,用来记录此对象被引用的次数1、当有新的引用指向时,引用计数+12、当该对象的引用减少时,引用计数 -13、如果对象的引用计数为0,该对象将被回收,空间将被释放

  一般的,引用技术法存在循环引用的问题,比如A引用B,B又引用A,那么A和B的引用计数都不为0,但是我们可以采用一些方法来解决这个问题。

  标记删除法

    标记清除是一种基于追踪回收的算法:1、第一步从跟对象出发,一直往后遍历,将那些可以被引用的对象标记为活动对象,那么剩下没有引用标记为非活动对象2、第二步就是将非活动对象删除回收了

  一般的,标记删除法存在性能问题,因为它需要扫描所有使用的内存。

  分代回收法

    分代回收法就是根据回收次数,将内存垃圾氛围3类:1、第0代(Generation 0):这是新分配的对象所在的代,由于新分配的对象很可能很快变为不可达(即不再被使用),因此第0代是垃圾回收最频繁检查的代。2、第1代(Generation 1):当第0代中的对象在一次垃圾回收后仍然存活时,它们会被提升到第1代。第1代中的对象在垃圾回收中的检查频率低于第0代,因为它们存活的时间更长,但是数量一般是最少的。3、第2代(Generation 2):类似地,如果第1代中的对象在另一次垃圾回收后仍然存活,它们会被提升到第2代。第2代是检查频率最低的代,因为其中的对象存活时间最长。

  C#的GC(Garbage Collection)

  C#的代码运行在CLR中,CRL负责资源申请、释放、异常监控等,内存属于资源的一种,所以,除非必须,C#代码不应该直接申请内存资源,而内存的释放交由GC(Garbage Collection)来控制,GC是分代回收器

  程序启动加载 CLR 时,GC 分配两个初始堆段:一个用于小型对象(小型对象堆或SOH),一个用于大型对象(大型对象堆或LOH)。

    大对象:对象的大小大于或等于 85,000 字节,运行时会将其分配到大型对象堆。

  对于大对象,它在第0代创建后,将会延续到第2代,放到LOH中,第 2 代垃圾回收未处理的对象仍是第 2 代对象,LOH 未处理的对象仍是 LOH 对象,所以LOH的回收是在第2代回收的时候进行的,第2代执行的垃圾回收被称为完整回收(Full GC),大对象堆(LOH)是我们开发者需要重点关注的

    GC回收的整个过程过程大概是这样:1、分配:申请内存,存放数据用于计算,此时数据可能在SOH或者LOH中2、晋升:根据引用打标记,应用程序中不再被使用的对象视为垃圾,按回收次数分为0、1、2代,数据在不同代中回收3、回收:清理数据,释放内存4、压缩:整理数据,退回多余的内存(LOH没有此过程)

  内存压缩

  这里解释一下内存压缩,就是GC的最后一步,我们内存是一个连续的块,但是在经历回收之后,它就变得断断续续的了,为了保证性能和资源的更好利用,GC可能会对内存做个压缩。

  对于SOH会压缩内存,这样所有的内存均可重新使用,借用官网的图说明一下:

    1、开始创建有四个对象obj0、obj1、obj2、obj3,这个时候它们在第0代2、现在obj1和obj3被释放了,那么它们的内存就空出来了,GC就会做个压缩,把obj2的位置就变到原来obj1开始的位置,同事它被提升到第1代3、如果又有obj4、obj5、obj6被提升到第1代,那么它们就会放在obj2后面,这样空间就被利用起来了4、如果obj2和obj5又被释放,那么又被压缩,obj4、obj6会向前移

  image

  而对于LOH由于压缩成本太高,因此一般是不会进行压缩(代码可以配置),在需要内存时,查看是否有可用的片段,没有则申请新的内存,但是用于申请的内存往往不一定刚好是空闲的长度,所以会流出很多Free的空间。借用官网的图说明一下:

    1、开始我们有obj0、obj1、obj2、obj3几个大对象,注意他们属于第2代2、当obj1、obj2被回收后,中间就会留下空闲片段(Free),GC一般不会去压缩3、当我们创建obj4需要新的空间时,会先看空闲片段是否满足需要,满足则使用,不满足就重新申请内存,但是哪怕满足,大概率也会有小的空闲片段

  image

  注意:GC执行垃圾回收时是阻塞操作,它会将所以线程挂起来,直至GC执行完成后继续执行。

  所以常说的GC会影响程序性能,其实就是只线程挂起来阻塞的时间,要缩短这个时间,就要尽可能少的产生垃圾,要尽可能快的执行回收,而且,如果频繁的执行GC,会发现CPU的使用率偏高,这可能对系统中的所有服务都回有一些影响。

  GC何时执行?

  GC的执行是个很复杂的过程,我们程序代码无法控制GC的执行,但是一般的,我们可以通过GC.Collect()方法来手动触发执行(虽然也不一定会执行),但是对于LOH,它往往在以下三种情况下执行:

    1、分配超出第0代或大型对象阈值2、调用 GC.Collect() 方法3、系统处于内存不足的状况

  

  总结

  在开发的过程中,为了避免GC影响性能,我们应该多注意一下。

    1、尽可能少的产生垃圾* 数据类型上,尽可能少的使用字符串、结构体之类的* 字符串使用上,注重使用字符串池(字符串暂存区),以内存的开销,对于碎片化的字符串,应采用StringBuilder优化* 注重复用,采用一些池技术优化,比如对象池2、尽可能快的执行回收,第2代回收频率是最低的,所以要尽量避免垃圾延续到第2代,特别是LOH,LOH存在的意义就是存放必须的大数据,所以非必须下不要使用LOH,LOH有着非常高的分配、回收成本。LOH中存放的数据往往是:大型集合、大字符串* 对于大型集合,比如大型的对象数据,我们需要在使用完成之后,将他们清空,以此取消他们之间的引用关系,从而可以让GC回收* 大字符串往往来自于三个地方:* 文件读取: var content = File.ReadAllText(filePath);* 格式化,如JSON格式: var json = value.ToJSON();* 数据库

 

  参考资料:

  https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/large-object-heap

 

http://www.gsyq.cn/news/12504.html

相关文章:

  • 安装python解释器 - Jun
  • 深入解析MySQL InnoDB锁机制 - 教程
  • 【A】杂题选将
  • Django 搭配数据库开发智慧园区系统全攻略 - 详解
  • 客服系统源码二次开发
  • PostgreSQL 和 MySQL两个数据库的索引的区别 - 详解
  • Lynx:新一代个性化视频生成模型,单图即可生成视频,重新定义身份一致性与视觉质量 - 教程
  • 2025权威排行榜:公众号编辑器Top 6深度测评,哪款最适合你
  • 在Debian系统上修改开源软件源代码制作patch - 教程
  • 需求的系统规划 3
  • 区别:Modbus RTU 和 Modbus TCP
  • python组合类型和组合可空类型
  • 数学草稿
  • vue3 + vite Cannot access ‘xxx‘ before initialization
  • 华米运动步数修改,每天自动修改并同步 微信运动/支付宝运动 步数
  • C++ placement new
  • Spring Boot接入邮箱,完成邮箱验证码
  • HyperWorks许可与网络安全
  • 研发项目管理系统哪个好?十款热门工具全面测评
  • L4 vs L7 负载均衡:彻底理解、对比与实战指南 - 实践
  • 你好 博客园!
  • 2025无人机林业行业场景解决方案
  • 实用指南:Spring Boot集群 集成Nginx配置:负载均衡+静态资源分离实战
  • 常用API biginteger和biddecimal
  • SI3933低频唤醒接收芯片完整指南:结构框图、PCB布局与选型要点芯片概述与主要特性
  • 在本地服务器创建RAID5磁盘阵列和RAID10磁盘阵列
  • RAGAS大模型评估框架
  • 新手入门需要掌握多少种大模型才行
  • docker容器怎么查看最后一些行日志
  • MAC idea 环境变量设置失效