热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

《CLRviaC#》读书笔记之自动内存管理(垃圾回收)

第二十一章自动内存回收(垃圾回收)2013-03-19本章讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以
第二十一章 自动内存回收(垃圾回收)

2013-03-19

本章讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存。

21.1 理解垃圾回收平台的基本工作原理
  从托管堆分配资源
  托管堆和C运行时堆比较
21.2 垃圾回收算法
21.3 垃圾回收与调试
21.4 使用终结器操作来释放本地资源
  21.4.1使用CriticalFinalizerObject类型确保终结
21.6 什么会导致Finalize被调用
21.7 终结操作揭秘
21.8 Dispose模式:强制对象清理资源
21.10 C#using语句

21.14 代

21.1 理解垃圾回收平台的基本工作原理


返回

每个程序都要使用资源,比如文件、内存缓冲、网络连接、数据库资源等。事实上,在面向对象的环境里,每个类型都代表可供程序使用的一种资源。要使用这些资源,必须为代表资源的类型分配内存。以下是访问资源所需的具体步骤:

(1)调用IL指令newobj,为代表资源的类型分配内存。在C#中是new操作符;

(2)初始化内存,设置资源的初始状态,是资源可用。由类型的实例构造器负责;

(3)访问类型的成员来使用资源;

(4)摧毁资源状态来进行清理。(21.8 Dispose模式:强制对象清理资源);

(5)释放内存。由垃圾回收器负责。

在进行非托管编程时,常发生两种bug:程序员忘记释放不再需要的内存造成(内存泄露);试图调用已被释放的内存造成(对象损坏)。内存泄露、内存溢出以及解决方法

垃圾回收器(garbage collection)就是为了处理以上问题二产生的。

注意:(参考第四章 的 托管堆的内存分配机制)托管堆又根据存储信息的不同划分为多个区域,其中最重要的是垃圾回收堆(GC Heap)和加载堆(Loader Heap),GC Heap用于存储对象实例,受GC管理;Loader Heap又分为High-Frequency Heap、Low-Frequency Heap和Stub Heap,不同的堆上又存储不同的信息。Loader Heap最重要的信息就是元数据相关的信息,也就是Type对象,每个Type在Loader Heap上体现为一个Method Table(方法表),而Method Table中则记录了存储的元数据信息,例如基类型、静态字段、实现的接口、所有的方法等等。Loader Heap不受GC控制,其生命周期为从创建到AppDomain卸载。

从托管堆分配资源

IL指令newobj用于创建一个对象时,就会从托管堆调拨可用空间,步骤如下:

(1)计算类型(及所有基类型)的字段需要的字节数;

(2)加上对象两个附加字段:一个是类型对象指针,一个是同步索引块。

(3)CLR检查托管堆是否有足够的可用空间。若有,放入。注意:是在NextObjPtr指针后放入的,并且为它分配的字节清零。接着,调用类型的实例构造器(为this参数传递NextObjPtr)。而后,NextObjPtr的指针的值会加上对象占据的字节数,得到一个新值,如下图所示:

图1 构造对象B后的状态

托管堆和C运行时堆比较:

C运行时对是链表来组织的。托管堆是连续的,只加了个指针来分割可用空间和不可用空间。

因此,托管堆的分配速度几乎可以跟线程栈的分配速度相媲美。

另外,连续分配的对象可以确保他们在内存中是连续的。由于差不多同时分配的对象彼此间常常有更紧密的联系,经常会在同一时间被访问。这样他们同时驻留在cpu缓存中的概率就更高,进而不会因为“缓存未命中”而被迫访问较慢的RAM。

21.2 垃圾回收算法


返回

如何保证指针NextObjPtr右边的有足够可用空间呢?那就需要一个算法,来回收那些不再使用的对象。

回收算法分两步:

第一步:确定那些对象可用,并标记(marking)。

每个应用程序都包含一组根(root)。每个根都是一个存储位置,其中包含指向引用类型对象的指针。该指针要么引用托管堆中的一个对象,要么为null。

图2 回收之前托管堆

 上图中,D对象引用了对象F,所有F也标记了。在标记过程中,为避免陷入循环引用的死循环,对先前标记过的对象不再检查下去。

第二步:压缩(compact)

压缩过程中较小的内存快回忽略。

在移动内存中对象之后,包含“指向对象的指针”的变量和cpu寄存器会变得无效,垃圾回收器会在移动后重新设置。

21.3 垃圾回收与调试


返回

当执行代码GC.Collect强制执行一次垃圾回收,若有对象不可达,就会被回收掉,看以下代码。

View Code

1 using System;
2 using System.Threading;
3 public sealed class Progam {
4 public static void Main() {
5 // Create a Timer object that knows to call our TimerCallback method once every 2000 milliseconds.
6 var t = new System.Threading.Timer(TimerCallback, null, 0, 2000);
7
8 // Wait for the user to hit
9 Console.ReadLine();
10 }
11
12 private static void TimerCallback(Object o) {
13 // Display the date/time when this method got called.
14 Console.WriteLine("In TimerCallback: " + DateTime.Now);
15
16 // Force a garbage collection to occur for this demo.
17 GC.Collect();
18 }
19 }

观察上述代码,你可能认为TimerCallback会每个2秒调用一次。但请注意,在TimerCallback方法中调用了GC.Collect()。垃圾回收器会检查哪些对象不可达,发现变量t在初始化后,再没有被调用过,所以,垃圾回收器回收了分配给t的Timer对象。

解决上述问题,你可以通过在Console.ReadLine();后加上t.Dispose();来告诉垃圾回收器它仍然被调用;而不是加t=null;因为这样代码对编译器来说毫无意义,而被优化掉

但你若不是用C#编译器的Release开关,而是用Debug开关,会发现TimerCallback仍被重复调用。因为编译器会让变量t存活到它所在方法结束。 

注意:读完本节后,不必担心你的对象被过早的回收。这里的Timer类非常特殊,它不会静静的像其他对象一个呆在托管堆中,而是定期调用一个方法。这里使用它只是为了更好的展示根的工作原理与对象生存周期的关系。所有非Timer对象都会根据应用程序的需要而自动存活。

21.4 使用终结器操作来释放本地资源


返回

有些类型只需内存就可以正常工作;有些类型,除了内存,还要使用本地资源,如:文件、网络连接、套接字、互斥体等。

终结(Finalization)是CLR提供的一种机制,允许对象在垃圾回收器回收器内存之前执行一些得体的清理工作。

Finalize方法在编程语言中需要特殊的语法,如下代码:

View Code

1 class SomeType
2 {
3 ~SomeType()
4 {
5 //这里的代码会进入Finalize方法
6 }
7 }

你用反编译器看Finalize方法是,会发现方法主体放在try块中,finally块则放入了一个对base.Finalize的调用。

 

 

实现Finalize方法时,一般都会调用Win32 CloseHandle函数,并向该函数传递本地资源的句柄。如果包装了本地资源的类型没有定义Finalize方法,本地资源就得不到关闭,导致资源泄露,直至进程终止。

21.4.1使用CriticalFinalizerObject类型确保终结

它的主要功能是它的派生类字构造过程中,就对其继承层次中所有对象的Finalize进行JIT编译,确保对象被回收之前,本地资源被释放。

21.6 什么会导致Finalize被调用


返回

有5种事件:

(1)第0代满

(2)调用静态方法System.GC.Collect

(3)Windows报告内存不足

(4)CLR卸载AppDomain

(5)CLR关闭 一个进程正常终止,CLR就会关闭。

对于前4种事件,当前CLR使用一个特殊、专用的线程来调用Finalize方法,如果一个Finalize方法进入无线循环,这个特殊线程会被阻塞,其他Finalize得不到调用。这种情况很糟糕,因为不能回收可终结对象所占的内存。

对于第5种事件,当前CLR的限制时间是每个Finalize方法2秒,超过,直接杀死进程。

21.7 终结操作揭秘


返回

若一个对象的类型定义了Finalize方法,那么在该类型的实例构造函数被调用之前,将会指向该对象的一个指针放到终结列表(Finalization list)中。它是由GC控制的一个内部数据结构。

 

图3 托管堆的终结列表包含了指向对象的指针

注意:虽然System.Object定义了一个Finalize方法,但CLR知道忽略它。你要在派生类重写Object的Finalize方法。

上图中,当垃圾回收开始时,垃圾回收器会扫描终结列表看那些对象要回收。上图中F对象没有被引用到,且在终结列表中,因此,F指针会移到freachable(发音f-reachable)列队中,表示finalize已准备好调用的一个对象。

图4 托管堆的终结列表包含了指向对象的指针

结果如上图所示,对象B占用的内存被回收,因为它没有Finalize方法。但是对象F占用的内存暂时不能回收,因为他们还没有调用Finalize方法。一个特殊的高优先级CLR线程专门负责调用Finalize方法。这样,可以避免潜在的线程同步问题。CLR未来可能使用多个终结器线程。

当第二次垃圾回收时,对象F执行完Finalize调用,内存会被释放,F指针会从freachable移除。

21.8 Dispose模式:强制对象清理资源

 


返回

Finalize方法确保本地资源的清理,但它的问题是调用时间不确定。另外,由于它不是公共方法,类的用户不能显式调用它。Dispose模式提供了显示进行资源清理的能力。

注意:Dispose只是为了能在确定的时间强迫对象执行清理;并不能控制托管堆中对象所占用内存的生存期。这意味着,即使对象已完成清理,仍可以在它上面调用方法,只不过会抛出System.ObjectDisposedException

System.Runtime.InteopServices.SafeHandle类实现了Dispose模式,但为了更清楚了展现Dispose模式,看如下代码: 

View Code

1 using System;
2 using System.Runtime.InteropServices;
3 public class AnotherResource:IDisposable {
4 public void Dispose() { }
5 }
6 public class SampleClass : IDisposable
7 {
8 //演示创建一个非托管资源
9 private IntPtr nativeResource = Marshal.AllocHGlobal(100);
10 //演示创建一个托管资源
11 private AnotherResource managedResource = new AnotherResource();
12 private bool disposed = false;
13
14 ///


15 /// 实现IDisposable中的Dispose方法
16 ///

17 public void Dispose()
18 {
19 //对象被显式地Dispose,而非终结,所以要清理与该对象关联的托管资源
20 Dispose(true);
21 //通知垃圾回收机制不再调用终结器(析构器)
22 GC.SuppressFinalize(this);
23 }
24
25 ///
26 /// 必须,以备程序员忘记了显式调用Dispose方法
27 ///

28 ~SampleClass()
29 {
30 //必须为false
31 Dispose(false);
32 }
33
34 ///
35 /// 非密封类修饰用protected virtual
36 /// 密封类修饰用private
37 ///

38 ///
39 protected virtual void Dispose(bool disposing)
40 {
41 if (disposed)
42 {
43 return;
44 }
45 if (disposing)
46 {
47 // 清理托管资源
48 if (managedResource != null)
49 {
50 managedResource.Dispose();
51 managedResource = null;
52 }
53 }
54 // 清理非托管资源
55 if (nativeResource != IntPtr.Zero)
56 {
57 Marshal.FreeHGlobal(nativeResource);
58 nativeResource = IntPtr.Zero;
59 }
60 //让类型知道自己已经被释放
61 disposed = true;
62 }
63
64 //若对象已释放后被调用,抛异常
65 public void SamplePublicMethod()
66 {
67 if (disposed)
68 {
69 throw new ObjectDisposedException("SampleClass", "SampleClass is disposed");
70 }
71 //省略
72 }
73 }

21.10 C#using语句


返回

如果决定显式调用 类型的Dispose或Close方法(有些类用Close代替Dispose方法),建议把它们放在异常处理的finally块中。令人惊喜的是C#的using语句提供了相同的功能,代码如下:

View Code

1 public static class Program
2 {
3 public static void Main()
4 {
5 byte[] bytesToWrite = new byte[] { 1, 2, 3, 4, 5 };
6 const string fileName = "Temp.dat";
7 //put dispose in finally block
8 OperateFile(bytesToWrite, fileName);
9 File.Delete("Temp.dat");
10
11 //use 'using' sentence instead
12 OperateFileinUsing(bytesToWrite, fileName);
13 File.Delete("Temp.dat");
14 }
15 private static void OperateFile(byte[] fileContent,string fileName)
16 {
17 FileStream fs = new FileStream("Temp.dat", FileMode.Create);
18 try
19 {
20 fs.Write(fileContent, 0, fileContent.Length);
21 }
22 finally
23 {
24 if (fs != null) fs.Dispose();
25 }
26 }
27 private static void OperateFileinUsing(byte[] fileContent, string fileName)
28 {
29 using(FileStream fs = new FileStream("Temp.dat", FileMode.Create))
30 {
31 fs.Write(fileContent, 0, fileContent.Length);
32 }
33 }
34 }

注意:using语句只能用于实现了IDisposable接口的类型。 

21.14 代


返回

代(generation)是垃圾回收器采用的一种机制,它唯一的目的是提升应用程序的性能。一个基于代得垃圾回收器做出了以下几点假设:

  • 对象越新,生存期越短。
  • 对象越老,生存期越长。
  • 回收堆的一部分,速度快于回收整个堆。

CLR的托管对只支持三代:第0代、第1代和第2代。CLR初始化是,会为每一代做预算。预算的大小以提升性能为宜。预算越大,垃圾回收的频率越低。

假设第0代预算容量为256KB(顺便说一句,之所以第0代预算容量为256KB,是因为这些象都能装入CPU的L2缓存,使内存压缩能非常快的速度完成。),第1代预算容量为2M,第2代预算容量为10M。

  1. 一个新的初始化的堆,其中包含了一些对象,所有的对象都是第0代,垃圾回收尚未发生;
  2. 当第0代得对象刚好占用256K,又要分配对象时,垃圾回收器启动。垃圾回收器先判断哪些对象为垃圾(参考21.7),压缩可达对象。垃圾回收后,这些存活的对象被认为是第1代对象;
  3. 在第1代未满,第0代满时,垃圾回收器只垃圾回收器只回收第0代,见第2步;
  4. 当第0代满,第一代也满了,垃圾回收器会回收第1代,第0代。垃圾回收后,第0代的可达对象被提升至第1代,第1代的可达对象被提升至第2代.

CLR的垃圾回收器是自调节的。这意味着垃圾回收器会在执行垃圾回收的过程中了解应用程序的行为。如果垃圾回收期发现在回收0代存活下来的对象很少,就可能将第0代的预算从256KB减少至128KB。少的预算意味着垃圾回收更频繁的发生,但垃圾回收器需要做的工作会减少,从而减少进程的工作集。

转:https://www.cnblogs.com/Ming8006/archive/2013/03/19/2969187.html



推荐阅读
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 第四章高阶函数(参数传递、高阶函数、lambda表达式)(python进阶)的讲解和应用
    本文主要讲解了第四章高阶函数(参数传递、高阶函数、lambda表达式)的相关知识,包括函数参数传递机制和赋值机制、引用传递的概念和应用、默认参数的定义和使用等内容。同时介绍了高阶函数和lambda表达式的概念,并给出了一些实例代码进行演示。对于想要进一步提升python编程能力的读者来说,本文将是一个不错的学习资料。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • YOLOv7基于自己的数据集从零构建模型完整训练、推理计算超详细教程
    本文介绍了关于人工智能、神经网络和深度学习的知识点,并提供了YOLOv7基于自己的数据集从零构建模型完整训练、推理计算的详细教程。文章还提到了郑州最低生活保障的话题。对于从事目标检测任务的人来说,YOLO是一个熟悉的模型。文章还提到了yolov4和yolov6的相关内容,以及选择模型的优化思路。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 无损压缩算法专题——LZSS算法实现
    本文介绍了基于无损压缩算法专题的LZSS算法实现。通过Python和C两种语言的代码实现了对任意文件的压缩和解压功能。详细介绍了LZSS算法的原理和实现过程,以及代码中的注释。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • 本文介绍了2020年计算机二级MSOffice的选择习题及答案,详细解析了操作系统的五大功能模块,包括处理器管理、作业管理、存储器管理、设备管理和文件管理。同时,还解答了算法的有穷性的含义。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 服务器上的操作系统有哪些,如何选择适合的操作系统?
    本文介绍了服务器上常见的操作系统,包括系统盘镜像、数据盘镜像和整机镜像的数量。同时,还介绍了共享镜像的限制和使用方法。此外,还提供了关于华为云服务的帮助中心,其中包括产品简介、价格说明、购买指南、用户指南、API参考、最佳实践、常见问题和视频帮助等技术文档。对于裸金属服务器的远程登录,本文介绍了使用密钥对登录的方法,并提供了部分操作系统配置示例。最后,还提到了SUSE云耀云服务器的特点和快速搭建方法。 ... [详细]
  • 本文介绍了Redis中RDB文件和AOF文件的保存和还原机制。RDB文件用于保存和还原Redis服务器所有数据库中的键值对数据,SAVE命令和BGSAVE命令分别用于阻塞服务器和由子进程执行保存操作。同时执行SAVE命令和BGSAVE命令,以及同时执行两个BGSAVE命令都会产生竞争条件。服务器会保存所有用save选项设置的保存条件,当满足任意一个保存条件时,服务器会自动执行BGSAVE命令。此外,还介绍了RDB文件和AOF文件在操作方面的冲突以及同时执行大量磁盘写入操作的不良影响。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
author-avatar
宝泉岭_白饭如霜些_350
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有