《Effective C#》读书笔记——了解.NET内存管理机制<.NET资源管理>

简介:

 我们知道C#是一门虚拟机语言,在C#编译器首先将C#代码编译成IL代码,运行程序时CLR(Common Language Runtime,公共语言运行时)通过调用JIT(just-in-time Compiler即时编译器)来将IL代动态即时编译成可执行的机器码。在CLR中有一个非常重要的概念:CLR GC(Garbage Collector,垃圾收集器),GC自动为我们的应用程序进行内存管理的分配和释放,所以在.NET应用程序中一般很少出现内存泄露、悬挂指针、未初始化指针等问题。同时这也是C#这样的虚拟机语言和C/C++等本地语言最大的不同之处。

  但是GC也不是万能的,我们也需要自己做一些清理工作,管理那些非托管的对象,它们通常是包装操作系统资源的对象,例如,文件句柄、窗口句柄或网络连接

这是因为外部的资源已经超出了运行时(虚拟机)的能力范围,而这些非托管的资源是由操作系统来掌控的。

同时如果使用了事件处理函数和委托在对象之间建立了链接,也可能会造成对象超过你预期的长时间驻留在内存中同时类似于LINQ的这样的在只有请求结果是才真正执行的查询也会导致同样的问题(因为查询将把需要绑定的局部变量使用闭包封装起来,而绑定对象只有在查询结果离开作用域时才会被释放掉)。可以看出在托管环境中开发应用程序,了解CLR内存的管理机制,对于我们写出更富有效率和健壮的代码是非常有益的。

阅读目录

  1.标记并压缩算法(Mark and Compact)

  2.清理非托管资源

      2.1 实现终结器

      2.2 实现IDisposable接口

  3.小节

  进一步阅读和参考

 

1.标记并压缩算法(Mark and Compact)

   GC使用"标记并压缩"(Mark and Compact)算法分析程序运行对象的关系,将不可达部分对象从内存中清理掉。GC会从应用程序的根对象开始,通过对对象树形结构遍历来判定一个对象是否可达,而并不是向COM那样跟踪每个对象的引用。

  我们可以假设有一个EntitySet,是从数据库中读取得到的一系列对象的集合。每个Entity对象都可能包含对其他Entity对象的引用,还可能包含对其他Entity的连接。类似于关系型数据库中的实体集合一样,这些链接和引用也可能是循环的。每个EntitySet中都包含了一系列对象图之间的复杂引用。但是这些并不需要我们担心,GC会替我们很好的完成这个任务。GC通过判断某一个对象(图)是否为垃圾来简化这一操作——当应用程序不需要某一个对象时,将不会保留这个对象的引用。GC会将那些没有被任何对象之间或间接引用的对象判定为垃圾。

  GC在其专门的线程中运行,GC不但自动为程序清理不在使用的内存,还会在每次运行时压缩托管堆。因此内存中空余的空间会是一片连续的内存。

下面图为一个托管堆在一次垃圾收集前后的状态:

我们可以看到GC不但清理不在使用的内存,还会移动内存中的其他对象,以便压缩正在使用的内存,并提高可用的内存空间。

 

2.清理非托管资源

  现在我们知道:托管堆上的内存管理是GC的职责。但是,非托管资源的内存管理则需要由我们开发者自己来负责。.NET为我们提供了两种控制非托管资源生命周期的机制:终结器(finalizer)和IDisposable接口。

 

2.1 (终结器)finalizer

  通过调用Finalize()方法通知GC回收该对象使用的内存时同时清理其非托管资源。终结器是一种防御性手段,让你的对象不管如何都可以被释放掉其中使用的非托管资源。终结器由GC调用,调用将发生在对象成为垃圾之后的某个时间(我们并不能准确的知道其发生的具体时间,只是知道这将发生在对象不可达之后)。这和C++相比区别很大,有经验的C++开发者往往会在构造函数中分配关键资源,然后在析构函数中释放掉。

2.1.1 使用终结器的问题

  在C#中,终结器迟早都会执行,不过其执行时间却无法预料(也不能)。终结器仅仅能够保证给定类型的对象所分配的非托管资源最终会被释放,但其执行时间确实不可预料的,因此:在我们的代码中最好避免使用终结器,也应该尽量少让代码逻辑使用到终结器

  依赖终结器还对带来性能上的损失。需要执行终结器的对象会给GC带来额外的性能开销。当GC发现某个对象属于垃圾,而该对象又需要执行终结器时,就不能将其直接从内存中移除:首先,GC将调用终结器,而终结器并不在执行垃圾收集的线程上执行。GC将把所有需要执行终结器的对象放在专门的队列中,然后让另一个线程来执行这些对象的终结器。用 Finalize 方法回收对象使用的内存需要至少两次垃圾回收。所有我们知道使用终结器释放非托管资源,资源对象不但会在内存中停留更长时间,GC也需要额外的线程来运行

2.1.2 GC中"代"(Generation)的概念

  .NET的GC为了优化其执行,引入了“代”(Generation)的概念。这概念可以让GC更快速的找到那些更有可能是垃圾的对象。

自上一次垃圾收集以后,新创建的对象属于第0代对象。而如果经过上一次垃圾收集之后仍旧存活的对象,将成为第1代对象。两次以及两次以上的垃圾收集仍没有被销毁的对象就变成了第2代对象。

  这样的分代方式是为了能将局部变量和应用程序生命周期中一直使用的对象分开对待。第0代大多属于局部变量。而成员变量和全局变量则会变成第1代对象,直至变成第2代对象。而GC通过减少检查第1代和第2代的次数来优化执行过程。大概10个周期的GC中,只有一次会同时检查第0代和第1代对象;大概100个周期的GC中,会有一次同时检查所有对象。所有,一个需要终结器的对象可能会比普通的对象多停留9个GC周期。而对于第2代对象,甚至需要100次以上的GC周期才有机会被销毁。

 

2.2 IDisposable接口

   相较于终结器IDisposable接口能够以一种影响更小的方式及时的释放非托管资源。使用IDisposable接口释放非托管资源可以避免执行终结器给GC带来性能上的影响。实现IDisposable接口的 Dispose 方法应该释放它拥有的所有资源。 它还应该释放其基类型拥有的所有资源通过调用其父类型的 Dispose 方法。

 

小节:

  GC自动为我们的应用程序进行内存管理的分配和释放,所以在.NET应用程序中一般很少出现内存泄露、悬挂指针、未初始化指针等问题。非托管的资源要使用终结器来保证释放。即使终结器会对程序的性能带来很大的影响,我们也必须提供终结器这是,这是为了保证不出现资源泄露的问题。但是,使用使用IDisposable接口释放非托管资源可以避免执行终结器给GC带来性能上的影响。

 

参考&进一步阅读

MSDN:自动管理内存

MSDN:托管执行过程

MSDN:清理非托管资源

MSDN:深入探讨IDisposable

蔡学镛:.NET的自动内存管理

维基百科:公共语言运行时

《Effective C#》

本文转自gyzhao博客园博客,原文链接:http://www.cnblogs.com/IPrograming/archive/2012/10/22/Effective_CSharp_Unit2.html,如需转载请自行联系原作者
相关文章
|
10天前
|
数据可视化 网络协议 C#
C#/.NET/.NET Core优秀项目和框架2024年3月简报
公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(每周至少会推荐两个优秀的项目和框架当然节假日除外),公众号推文中有项目和框架的介绍、功能特点、使用方式以及部分功能截图等(打不开或者打开GitHub很慢的同学可以优先查看公众号推文,文末一定会附带项目和框架源码地址)。注意:排名不分先后,都是十分优秀的开源项目和框架,每周定期更新分享(欢迎关注公众号:追逐时光者,第一时间获取每周精选分享资讯🔔)。
|
1月前
|
SQL 数据库 C#
C# .NET面试系列十一:数据库SQL查询(附建表语句)
#### 第1题 用一条 SQL 语句 查询出每门课都大于80 分的学生姓名 建表语句: ```sql create table tableA ( name varchar(10), kecheng varchar(10), fenshu int(11) ) DEFAULT CHARSET = 'utf8'; ``` 插入数据 ```sql insert into tableA values ('张三', '语文', 81); insert into tableA values ('张三', '数学', 75); insert into tableA values ('李四',
61 2
C# .NET面试系列十一:数据库SQL查询(附建表语句)
|
1月前
|
开发框架 算法 搜索推荐
C# .NET面试系列九:常见的算法
#### 1. 求质数 ```c# // 判断一个数是否为质数的方法 public static bool IsPrime(int number) { if (number < 2) { return false; } for (int i = 2; i <= Math.Sqrt(number); i++) { if (number % i == 0) { return false; } } return true; } class Progr
58 1
|
1月前
|
并行计算 安全 Java
C# .NET面试系列四:多线程
<h2>多线程 #### 1. 根据线程安全的相关知识,分析以下代码,当调用 test 方法时 i > 10 时是否会引起死锁? 并简要说明理由。 ```c# public void test(int i) { lock(this) { if (i > 10) { i--; test(i); } } } ``` 在给定的代码中,不会发生死锁。死锁通常是由于两个或多个线程互相等待对方释放锁而无法继续执行的情况。在这个代码中,只有一个线程持有锁,且没有其他线程参与,因此不
102 3
|
3月前
|
Linux C# 开发工具
C#开源的一款友好的.NET SDK管理器
C#开源的一款友好的.NET SDK管理器
|
3天前
|
开发框架 前端开发 JavaScript
采用C#.Net +JavaScript 开发的云LIS系统源码 二级医院应用案例有演示
技术架构:Asp.NET CORE 3.1 MVC + SQLserver + Redis等 开发语言:C# 6.0、JavaScript 前端框架:JQuery、EasyUI、Bootstrap 后端框架:MVC、SQLSugar等 数 据 库:SQLserver 2012
|
1月前
|
SQL 存储 关系型数据库
C# .NET面试系列十:数据库概念知识
#### 1. 为什么要一定要设置主键? 设置主键是数据库设计中的一个重要概念,有几个主要原因: 1、唯一性 ```c# 主键必须保证表中的每一行都有唯一的标识。这样可以避免数据冗余和不一致性。如果没有主键或者主键不唯一,就可能出现数据混乱或错误。 ``` 2、查询性能 ```c# 数据库系统通常会使用主键来加速数据检索。主键通常会被索引,这样可以更快速地找到特定行的数据,提高查询效率。 ``` 3、关联性 ```c# 主键常常用于建立表与表之间的关系。在关系数据库中,一个表的主键通常与其他表中的外键建立关联,这种关系对于数据的一致性和完整性非常重要。 ``` 4、数据完
131 1
C# .NET面试系列十:数据库概念知识
|
1月前
|
XML 开发框架 .NET
C# .NET面试系列八:ADO.NET、XML、HTTP、AJAX、WebService
## 第二部分:ADO.NET、XML、HTTP、AJAX、WebService #### 1. .NET 和 C# 有什么区别? .NET(通用语言运行时): ```c# 定义:.NET 是一个软件开发框架,提供了一个通用的运行时环境,用于在不同的编程语言中执行代码。 作用:它为多语言支持提供了一个统一的平台,允许不同的语言共享类库和其他资源。.NET 包括 Common Language Runtime (CLR)、基础类库(BCL)和其他工具。 ``` C#(C Sharp): ```c# 定义: C# 是一种由微软设计的面向对象的编程语言,专门为.NET 平台开发而创建。 作
173 2
|
1月前
|
开发框架 中间件 .NET
C# .NET面试系列七:ASP.NET Core
## 第一部分:ASP.NET Core #### 1. 如何在 controller 中注入 service? 在.NET中,在ASP.NET Core应用程序中的Controller中注入服务通常使用<u>依赖注入(Dependency Injection)</u>来实现。以下是一些步骤,说明如何在Controller中注入服务: 1、创建服务 首先,确保你已经在应用程序中注册了服务。这通常在Startup.cs文件的ConfigureServices方法中完成。例如: ```c# services.AddScoped<IMyService, MyService>(); //
60 0
|
1月前
|
开发框架 前端开发 .NET
C# .NET面试系列六:ASP.NET MVC
<h2>ASP.NET MVC #### 1. MVC 中的 TempData\ViewBag\ViewData 区别? 在ASP.NET MVC中,TempData、ViewBag 和 ViewData 都是用于在控制器和视图之间传递数据的机制,但它们有一些区别。 <b>TempData:</b> 1、生命周期 ```c# TempData 的生命周期是短暂的,数据只在当前请求和下一次请求之间有效。一旦数据被读取,它就会被标记为已读,下一次请求时就会被清除。 ``` 2、用途 ```c# 主要用于在两个动作之间传递数据,例如在一个动作中设置 TempData,然后在重定向到另
95 5

热门文章

最新文章