c# 托管堆和垃圾回收

news/2025/3/4 16:08:28/文章来源:https://www.cnblogs.com/aoximin/p/18750687

前言

我们已经理解了clr可以寄宿,然后宿主可以控制了,也就是说外部问题我们已经解决了,那么还有一件重要的事情。

那就是clr 是如何控制托管地的垃圾回收的,为什么有clr就是为了自动垃圾回收嘛,不然为啥不用c++这种是吧。

正文

首先我们要知道内存的分配呢?

在c语音中,内存分配直接调用操作系统的api,然后操作系统分配是吧。

如果也是直接调用操作系统的api,那么这个clr都不知道,那么怎么控制呢?

这里提前说一下,我们我们谈的是托管堆,而不谈栈呢?

那是因为栈不需要托管,用完就弹出,但是也有一套机制,这个不需要我们去管理。

看一下如何分配托管堆的:

  1. 调用 IL 执行 newobj,为代表资源的类型分配内存(一般使用 C# new 操作符来完成)。

  2. 初始化内存,设置资源的初始状态并使资源可用。类型的实例构造器负责设置初始状态。

  3. 访问类型的成员来使用资源(有必要可以重复)。

  4. 摧毁资源的状态以进行清理。

  5. 释放内存。垃圾回收器独自负责这一步。

按道理来说,这样相当于托管的内存都被管理了,是不是托管的内存就不存在内存泄漏呢?

其实不是,主要是这个内存泄漏的名词问题,对于内存泄漏,然后内存越来越多了, 然后漏出去了,或者说内存信息没有混淆,被暴露出去了。

实际含义:

内存泄漏是指程序在运行时不再需要使用的内存空间未被正确释放的现象。这可能导致系统中的可用内存减少,最终可能导致系统性能下降甚至崩溃。内存泄漏通常由程序错误、不完善的代码设计或内存管理问题导致。

举个例子:

public class LeakyClass
{private List<string> leaks = new List<string>();public LeakyClass(){// Simulate memory leak by continuously adding items to the listwhile (true){this.leaks.Add("Leaking memory...");}}
}public class Program
{public static void Main(){// Create an instance of LeakyClass to cause memory leakLeakyClass leaky = new LeakyClass();}
}

这里内存可能就会无限叠加。。。

总之我们就会记录下来的,那么接下来似乎不是那么难了。

然而并非我们去newobj的时候需要24k内存,然后我们就去申请24k内存。

事实上clr是这样的,人家可能去申请1m的内存,然后将24k给了newobj的这个操作。

这样避免了高频率的申请,同时利用局部性,比如说将连续申请的内存块,放在同一个地方。

因为大多数应用来说,如果是连续申请的,那么很有可能将会连续使用,那么cpu的缓存就可以大大利用了。

这些都是优点,那么因为内存不可能是无限的,那么呢,这里就有一个需要回收的概念。

那么怎么去回收垃圾呢,也就是用不上的东西。

至于对象生存期的管理,有的系统采用的是某种引用计数算法。

这里引用计数法呢?简单的说,就是如果一个对象被引用了,然后就+1呗,如果引用为0,那么就是可以释放。

这似乎是可行的,而且挺安全的呀,不被引用肯定不被使用,但是也有一个问题哇,那就是可能存在循环引用。

鉴于引用计数垃圾回收器算法存在的问题,CLR 改为使用一种引用跟踪算法。

引用跟踪算法只关心引用类型的变量,因为只有这种变量才能引用堆上的对象;值类型变量直接包含值类型实例。引用类型变量可在许多场合使用,包括类的静态和实例字段,或者方法的参数和局部变量。我们将所有引用类型的变量都称为根。

CLR 开始 GC 时,首先暂停进程中的所有线程。这样可以防止线程在 CLR 检查期间访问对象并更改其状态。然后,CLR 进入 GC 的 标记阶段。在这个阶段,CLR 遍历堆中的所有对象,将同步块索引字段中的一位设为 0。这表明所有对象都应删除。然后,CLR 检查所有活动根。查看它们引用了哪些对象。这正是 CLR 的 GC 称为引用跟踪 GC 的原因。如果一个根包含 null, CLR 忽略这个根并继续检查下个根。

任何根如果引用了堆上的对象,CLR 都会标记那个对象,也就是将该对象的同步块索引中对的位设为 1。一个对象被标记后, CLR 会检查那个对象中的根,标记它们引用的对象。如果发现对象已经标记,就不重新检查对象的字段。这就避免了因为循环引用而产生死循环。

应用程序的根直接引用对象 A, C, D 和 F。所有对象都已标记。标记对象 D 时,垃圾回收器发现这个对象含有一个引用对象 H 的字段,造成对象 H 也被标记。标记过程会持续,直至应用程序的所有根所有检查完毕。

检查完毕后,堆中的对象要么已标记,要么未标记。已标记的对象不能被垃圾回收,因为至少有一个根在引用它。我们说这种对象的可达(reachable)的,因为应用程序代码可通过仍在引用它的变量抵达(或访问)它。未标记的对象是不可达(unreachable)的,因为应用程序中不存在使对象能被再次访问的根。

CLR 知道哪些对象可以幸存,哪些可以删除后,就进入 GC 的压缩(compact)①阶段。在这个阶段,CLR 对堆中已标记的对象进行“乾坤大挪移”,压缩所有幸存下来的对象,使它们占用连续的内存空间。这样做有许多好处。首先,所有幸存对象在内存中紧挨在一起,恢复了引用的“局部化”,减小了应用程序的工作集,从而提升了将来访问这些对象时的性能。其实,可用空间也全部是连续的,所以整个地址空间区段得到了解放,允许其他东西进驻。最后,压缩意味着托管堆解决了本机(原生)堆的控件碎片化问题。

压缩好内存后,托管堆的 NextObjPtr 指针指向最后一个幸存对象之后的位置。下一个分配的对象将放到这个位置。

压缩阶段完成后,CLR 恢复应用程序的所有线程。这些线程继续访问对象,就好像 GC 没有发过一样。

如果 CLR 在一次 GC 之后回收不了内存,而且进程中没有空间来分配新的 GC 区域,就说明该进程的内存已耗尽。此时,试图分配更多内存的 new 操作符会抛出 OutOfMemoryException。应用程序可捕捉该异常并从中恢复。但大多数应用程序都不会这么做;相反,异常会成为未处理异常。

我们再来整理一下回收步骤:

  1. CLR 开始 GC 时,首先暂停进程中的所有线程

  2. 然后,CLR 进入 GC 的 标记阶段。在这个阶段,CLR 遍历堆中的所有对象,将同步块索引字段中的一位设为 0。

  3. CLR 检查所有活动根。查看它们引用了哪些对象。这正是 CLR 的 GC 称为引用跟踪 GC 的原因。如果一个根包含 null, CLR 忽略这个根并继续检查下个根。

  4. 进行compact压缩。

然后记得呢?检查的是活动根。 什么是根呢? 所以引用对象都是根。

举例一个活动根的例子:

一个实际的例子是,如果一个对象被一个全局变量引用,那么这个全局变量就是一个活动根,因为它直接引用了这个对象,从而阻止了垃圾回收器回收这个对象。相反,如果一个对象只被一个局部变量引用,而这个局部变量超出了其作用域,那么这个局部变量就是一个非活动根,因为它不再直接引用这个对象,垃圾回收器可以回收这个对象。

还有什么是活动根吗?

除了全局变量,还有其他情况会产生活动根,比如:

  1. 局部变量:在方法内部定义的局部变量如果引用了对象,这些局部变量也会成为活动根。
  2. 静态变量:静态变量也可以成为活动根,因为它们在整个应用程序生命周期内存在。
  3. 寄存器:寄存器中存储的引用也可以成为活动根。
  4. CPU 寄存器中的引用:在一些特定情况下,CPU 寄存器中存储的引用也可以成为活动根。

这里很多人就会疑问,局部变量的活动根是啥:

void MyMethod()
{var obj = new SomeObject();// 这里的 obj 是一个局部变量,它引用了 SomeObject 对象,因此 obj 是一个活动根// 在方法执行期间,obj 阻止了 SomeObject 对象被垃圾回收器回收
}

然而也不是离开了MyMethod才是非活动根了。

垃圾回收的例子:

using System;
using System.Threading;public static class Program {public static void Main() {// 创建每 2000 毫秒就调用一次 TimerCallback 方法的 Timer 对象Timer t = new Timer(TimerCallback, null, 0, 2000);// 等待用户按 Enter 键Console.ReadLine();}private static void TimerCallback(Object o) {// 当调用该方法时,显示日期和时间Console.WriteLine("In TimerCallback: " + DateTime.Now);// 出于演示目的,强制执行一次垃圾回收GC.Collect();}
}

回收开始时,垃圾回收器首先假定堆中的所有对象都是不可达的(垃圾);这自然也包括Timer对象。

然后,垃圾回收器检查应用程序的根,发现在初始化之后,Main方法再也没有用过变量t。

既然应用程序没有任何变量引用Timer对象,垃圾回收自然会回收分配给它的内存;这使计数器停止触发。并解释了为什么TimerCallback方法只被调用了一次。

现在,假定用调试器单步调试 Main,而且在将新 Timer 对象的地址赋给 t 之后,立即发生了一次垃圾回收。然后,用调试器的“监视”窗口查看t 引用的对象,会发生什么事情呢?因为对象已被回收,所以调试器无法显示该对象。大多数开发人员都没有料到这个结果,认为不合常理。所以,Microsoft 提出了一个解决方案。

使用 C# 编译器的 /debug 开关编译程序集时,编译器会应用 System.Diagnostics.DebuggableAttribute,并为结果程序集设置DebuggingModes 的 DisableOptimizations 标志。运行时编译方法时,JIT 编译器看到这个标志,会将所有根的生存期延长至方法结束。在我的例子中,JIT 编译器认为 Main 的 t 变量必须存活至方法结束。所以在垃圾回收时,GC 认为 t 仍然是一个根,t引用的Timer对象仍然“可达”。Timer对象会在回收中存活,TimerCallback 方法会被反复调用,直至Console.ReadLine 方法返回而且 Main 方法退出。

这很容易验证,只需在命令行中重新编译程序,但这一次指定 C# 编译器的 /debug 开关。运行可执行文件,会看到 TimerCallback 方法被反复代用。注意,C# 编译器的 /optimize+编译器开关会将 DisableOptimizations 禁止的优化重新恢复,所以实验时不要指定该开关。

那么这也就是在实验状态是吧,那么线上不可能去搞这种东西吧。

public static void Main() {// 创建每 2000 毫秒就调用一次 TimerCallback 方法的 Timer 对象Timer t = new Timer(TimerCallback, null, 0, 2000);// 等待用户按 Enter 键Console.ReadLine();// 在 ReadLine 之后引用 t(会被优化掉)t = null;
}

给大家实验一下:

release:

debug:

所以呢,有些时候,尤其是垃圾回收的时候,我们要多调试一下release环境,而不仅仅是debug环境。

下一节,描述一下优化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/893474.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

flash 钓鱼

访问真实的flash 网站页面,点击查看网页源代码并将所有代码复制下来保存到index.html2.检查并修改源代码链接。搜索href和src,将链接修改为可以访问的形式..新建 flash.js 文件,内容为(其中的 window.location.href 的值修改为 index.html的url地址):window.alert = functio…

Pandas 常用操作 (缺失值处理/排序/字符串处理/Index/Merge/合并)

处理示例:清洗成 -> Code:import pandas as pd# 读取Excel,跳过前面两行空行 studf = pd.read_excel(rstudent_excel.xlsx, skiprows=2) print(studf) # 删除掉全部是空值的列 studf.dropna(axis=columns, how=all, inplace=True) # 删除掉全部是空值的行 studf.dropn…

Flutter QQ聊天项目(2):消息与联系人界面实现

这里在上一篇博客:Flutter QQ聊天项目(1):登录界面实现 的基础上,进一步扩展实现了包含消息列表界面和联系人界面的主界面,在登录界面成功登录即可进入。先看下效果图:一、初步实现主界面 1.1 主界面(MainWidget.dart) 这里就初步实现了一个主界面框架,左侧是菜单按钮…

【域攻击】横向移动:PTH

免责声明 本文档所提供的信息旨在帮助网络安全专业人员更好地理解并维护他们负责的网站和服务器等系统。我们鼓励在获得适当授权的情况下使用这些信息。请注意,任何未经授权的使用或由此产生的直接或间接后果和损失,均由使用者自行承担。我们提供的资源和工具仅供学习和研究之…

【完整汇总】近 5 年 JavaScript 新特性完整总览

关于 JavaScript 近 5 年新特性完整总结,一篇文章带你全面掌握ES2019-ES2024所有实用功能Hey, 我是 沉浸式趣谈 本文首发于 【沉浸式趣谈】,我的个人博客 https://yaolifeng.com 也同步更新。 转载请在文章开头注明出处和版权信息。 如果本文对您有所帮助,请 点赞、评论、转…

No.30 JavaScript--字符串

一、字符串 1.定义字符串就是零个或多个排在一起的字符,放在单引号或双引号之中。 单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号。如果要在单引号字符串的内部,使用单引号,就必须在内部的单引号前面加上反斜杠,用来转义。双引号字符串内部使用…

使用DeepSeek搭建个人知识库教程

背景 为什么你需要一个个人知识库? 在日常工作和学习中,我们常常会积累大量的文档、代码、笔记等资料。如果没有一个统一的存储和检索系统,这些资料很容易变得杂乱无章,查找起来费时费力。 别慌,这次我们用DeepSeek快速搭建自己的个人知识库,实现本地上传文档,量化成知识…

Java的各种内部类

对Java的各种内部类进行梳理和总结17-各种内部类https://www.cnblogs.com/kxxiaomutou/p/15646878.html 2022-11-23 21:51:14一、介绍 可以将一个类的定义放在另一个类的定义内部,这就是内部类 内部类可以分为几种具体的类型:成员内部类(常说的内部类) 匿名内部类 局部内部…

无源晶振测试仪测试参数详解

晶振测试仪 GDS-80 是一款高性价比的晶振测试系统,采用微处理器技术,实现智能化测量,符合 IEC-444 标准。其测量频率范围为 20KHz-100MHz,能够对晶振的多种参数进行精确测量,广泛应用于晶体行业、邮电、通信、广播电视、学校、研究所及工矿企业等生产和科研领域。晶振测试…

FormCreate设计器v5.6发布—AI加持的低代码表单设计器正式上线!

近期DeepSeek可谓是刷遍全网,当然,在DeepSeek等AI技术的推动下,人工智能正以惊人的速度改变着各行各业。AI不仅是一种技术趋势,更是未来生产力的核心驱动力。 如今,FormCreate设计器也正式迈入AI时代🎉🎉,推出v5.6版本,搭载AI智能表单助理,让表单设计从繁琐的手动操…

清华大学第7弹 | 亿万家长福音 -《DeepSeek赋能家庭教育》 | 免费下载

《DeepSeek赋能家庭教育》是由清华大学张诗瑶博士出品的教程,帮助中国家长解放自己的同时,提高孩子的自主学习能力和核心竞争力。下载地址:https://pdfs.top/book/清华大学-DeepSeek赋能家庭教育.html直到听了清华博士后张诗瑶的直播,我才发现: “不是孩子笨,而是我们用错…

揭秘Chrome DevTools:从原理到自定义调试工具

作者:京东科技 杜强强 引言Chrome DevTools 是前端开发者的必备工具,不仅可以用于调试 Chrome 网页,还支持 Android WebView、Roma (跨平台开发框架) 安卓&鸿蒙端等平台的调试。 作为最常用的调试工具之一,DevTools 不仅能快速定位问题,还能让我们深入了解调试的内部机…