一起谈.NET技术,关于CLR内存管理一些深层次的讨论 [上篇]

  半年之前,PM让我在部门内部进行一次关于“内存泄露”的专题分享,我为此准备了一份PPT。今天无意中将其翻出来,觉得里面提到的关于CLR下关于内存管理部分的内存还有点意思。为此,今天按照PPT的内容写了一篇文章。本篇文章不会在讨论那些我们熟悉的话题,比如“值类型引用类型具有怎样的区别?”、“垃圾回收分为几个步骤?”、“Finalizer和Dispose有何不同”、等等,而是讨论一些不同的内容。整篇文章分上下两篇,上篇主要谈论的是“程序集(Assembly)和应用程序域(AppDomain)”。也许有的地方说的不是很正确,希望读者不吝赐教。

  一、程序集与应用程序域

  何谓程序集(Assembly)?它是一个托管应用的基本的部署单元。一个程序集是自描述的(通过元数据)、能够实施版本策略和部署策略。我倾向于这样的方式来定义程序集:“Assembly is a reusable, versionable, and self-describing building block of a CLR application.”从结构组成来看,一个程序集主要由三个部署组成:IL指令、元数据和资源。程序集的结构组成如下图所示。

  那么什么又是应用程序域呢?从功能上讲,通过应用程序域实现的隔离机制为托管代码的执行提供了一个安全的边界。从与程序集的关系来讲,我们可以将应用程序域看成是加载程序集的容器。只有相关的程序集被CLR加载到相应的应用程序域中,才谈得上代码的执行。

基于应用程序域的隔离,归根结底是内存的隔离。一个基本的反映就是:在一个应用程序域中创建的对象,不能直接在另一个应用程序域中使用。这中间需要有一个基本的跨应用程序域传递的机制,我们将这种机制称之为“封送(Marshaling)”。具体来讲,又具有两种不同的封送方式:按值封送(MBV:Marshaling By Value )和按引用封送(MBR:Marshaling By Reference)。MBV主要采用序列化的方式,而MBR最典型的就是.ENT Remoting。

  二、系统程序域、共享程序域和默认程序域

  当托管应用被启动后,在执行第一句代码之前,CLR会先后为我们创建三个应用程序域:系统程序域(System Domain)、共享程序域(Shared Domain)和默认程序域(Default Domain),它们分别具有不同的作用。

  • 系统程序域:系统程序域是第一个被创建的应用程序域,同时也是其他两个应用程序域的创建者。在该程序域初始化过程中,由它将msCorLib.dll这个程序集(这是一个很重要的程序集,.NET类型系统最基本的类型定义其中)加载到共享程序域中。此外,驻留的字符串也被保存在此系统程序域中。系统程序域的一个主要的任务是追踪其他所有应用程序域的状态,并负责加载和卸载它们;
  • 共享程序域:共享程序域主要用于保存以“中立域(Domain-neutral Domain )”加载的程序集容器。所谓“中立域 ”方式加载的程序集,就是说程序集并不被加载到当前的程序域中并被该程序域专用,而是加载到一个公共的程序域中被所有程序域共享。
  • 默认程序域:我们的托管程序最终就运行在该程序域中,默认程序域可以通过System.AppDomain表示。

  三、字符串的驻留

  上面的文字描述实际上透露一些重要的信息,其中一个就是字符串的驻留(String Interning)。关于字符串的驻留,我想大家都不陌生,所以在这里我就不作重复的介绍了。在这里,我只想讨论一个问题:字符串的驻留是基于整个进程的,而不是仅仅基于某个应用程序域。

  从上面的描述我们知道,字符串对象和一般的引用类型对象具有很大的不同:字符串对象直接被保存到系统程序域中,而一般的引用类型对象我们都是最终保存在GC堆中。从某种意义上讲,在字符串驻留机制下,字符串也是以“中立域”的方式被加载的,被驻留的字符串能够被同一个进程下所有应用程序域所共享。

  那么,我们是否可以通过一些比较直观的方式来验证这一点。但是,我们不能直接编写程序来比较两个应用程序域中字符串是否是相同的引用,但是我们有一些间接的机制。我个人喜欢采用的方式是:加锁。我们在运行于不同的应用程序域的代码中对两个字符串变量进行加锁,如果程序运行的结果和对相同的对象加锁一样,那么就可以证明被枷锁的两个对象实际上是同一个对象。

  为了便于演示,我写一个如下一个AppDomainContext,表示某个AppDomain对应的执行上下文。AppDomainContext具有一个只读的类型为AppDomain的属性,该属性通过构造函数执行,最终在静态方法NewContext被创建。我们调用Invoke方法让指定的方法对应的应用程序域中执行。

   1: public class AppDomainContext
   2: {
   3:     public AppDomain AppDomain { get; private set; }
   4:     private AppDomainContext(AppDomain appDomain)
   5:     {
   6:         this.AppDomain = appDomain;
   7:     }
   8:     public static AppDomainContext NewContext(string friendlyName)
   9:     {
  10:         return new AppDomainContext(AppDomain.CreateDomain(friendlyName));
  11:     }
  12:  
  13:     public void Invoke<T>(Action<T> action) where T : MarshalByRefObject
  14:     {
  15:         T instance = (T)this.AppDomain.CreateInstanceAndUnwrap(typeof(T).Assembly.FullName, typeof(T).FullName);
  16:         action.Invoke(instance);
  17:     }
  18: }

  我们接着在定义一个辅助类ObjectLock方便进行加锁,以及确认对象是否被所住。ObjectLock比如继承自MarshalByRefObject,因为我们需要该对象以MBR的方式进行传递。在Lock方法中对指定的对象进行加锁,并指定加锁的时间。在CheckLock中通过时间间隔判断指定的对象是否已经被锁住,相应的结果会在控制台中被输出。为了让大家能够确定相应的操作是在哪个应用程序域中执行的,在枷锁和检查锁定的时候将应用程序域的名称(AppDomain.FriendlyName属性)打印出来。

   1: public class ObjectLock : MarshalByRefObject
   2: {
   3:     public void Lock(object objectToLock, int millisecondsTimeout)
   4:     {
   5:         lock (objectToLock)
   6:         {
   7:             Console.WriteLine("[{0}] Successfully lock the object.", AppDomain.CurrentDomain.FriendlyName);
   8:             Thread.Sleep(millisecondsTimeout);
   9:         }
  10:     }
  11:     public void CheckLock(object objectToLock)
  12:     {
  13:         if (Monitor.TryEnter(objectToLock, 10))
  14:         {
  15:             Console.WriteLine("[{0}] The object is not  locked.", AppDomain.CurrentDomain.FriendlyName);
  16:         }
  17:         else
  18:         {
  19:             Console.WriteLine("[{0}] The object is locked .", AppDomain.CurrentDomain.FriendlyName);
  20:         }
  21:     }
  22: }

  然后我再一个控制台应用中的Main方法中,编写了如下简单的代码。通过AppDomainContext在一个的应用程序域(Foo)中锁定一个值为“Hello World!”的字符串,并在另一个应用程序域(Bar)中确认同值得字符串是否已经被锁定。结果表示在应用程序域Bar中指定的字符串已经被锁定,从而证明了应用程序域Foo和Bar中两个值为“Hello World!”的字符串对象实际上是同一个。

   1: static void Main(string[] args)
   2: {
   3:     Action<ObjectLock> lockObj = objLock => objLock.Lock("Hello World!", 2000);
   4:     Action<ObjectLock> checkLock = objLock => objLock.CheckLock("Hello World!");
   5:  
   6:     Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
   7:     Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
   8:  
   9:     lockObjThread.Start();
  10:     Thread.Sleep(500);
  11:     checkLockThread.Start();           
  12: }

  输出结果:

   1: 1: [Foo] Successfully lock the object.
   2: 2: [Bar] The object is locked.

  上面的介绍同时说明一个问题:千万不要对一个字符串对象加锁。

  四、程序集加载的方式

  虽然我们说CLR在启动托管应用的时候,以中立域的方式加载msCorLib.dll这个程序集,但是这不是程序集默认采用的加载方式。在默认的情况下,程序集被加载到当前的程序域中,供该程序集独占使用。我个人将这两种不同的程序集加载方式称为:独占加载(Exclusive Loading )和共享加载(Shared Loading)。如右图所示:如果某个类型被定义在程序集中Foo.Dll,当AppDomain1和AppDomain2需要使用该类型的时候,它们会分别以独占的方式加载程序集Foo.Dll。但是,如果它们使用一些基元类型,比如System.Object、System.Int32、System.DateTime等,则不会加载定义它们的msCorLib.dll程序集,而是直接使用已经被以中立域方式加载到共享程序域中的msCorLib.dll。

  我们同样可以借助上面定义的AppDomainContext来证明这一点。在这之前我需要说明一点:程序集的加载包括对定义在程序集中类型系统的加载,我们可以通过类型对象的加锁情况来推断程序集的加载方式。为此我在上面创建的解决方案中添加了一个类库项目Lib,ConsoleApp引用Lib项目,并在Lib中定义了一个空的Foo类型。

   1: namespace Artech.MemAllocation
   2: {
   3:     public class Foo
   4:     {}
   5: }

  然后我们修改之前的程序,将对字符串加锁替换在对Foo类型(typeof(Foo))加锁。从输出结果我们可以看出,在Bar程序域中使用的Foo类型并没有被锁住,从而证明两个程序域(Foo和Bar)使用的同一个类型并不是Type对象,因为对应的程序集是以独占的方式加载的。

   1: static void Main(string[] args)
   2: {
   3:     Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(Foo), 2000);
   4:     Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(Foo));
   5:  
   6:     Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
   7:     Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
   8:  
   9:     lockObjThread.Start();
  10:     Thread.Sleep(500);
  11:     checkLockThread.Start();
  12: }

  输出结果:

   1: [Foo] Successfully lock the object.  
   2: [Bar] The object is not locked.

  但是,如果我们将加锁和锁定检验的typeof(Foo)替换成typeof(int),结果就完全不一样了。不同的结果说明了msCorLib.dll采用了不同于上面的程序集加载方式,以中立域方法的加载方式决定在任何应用程序域中使用的类型都是同一个Type对象。

   1: static void Main(string[] args)
   2: {
   3:     Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(int), 2000);
   4:     Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(int));
   5:  
   6:     Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
   7:     Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
   8:  
   9:     lockObjThread.Start();
  10:     Thread.Sleep(500);
  11:     checkLockThread.Start();
  12: }

  输出结果:

   1: [Foo] Successfully lock the object.
   2: [Bar] The object is locked.

  五、我们自己的程序集也可以采用中立域的方式加载吗?

  我想到这里有人会问一个问题:“我们自定义的程序集可以像msCorLib.dll一样以中立域的方式共享加载吗?”。对于控制台应用,你只需要在Main方法上应用LoaderOptimizationAttribute特性,并指定LoaderOptimization为MultiDomain即可。比如,还是采用对Foo类型Foo类型(typeof(Foo))对象加锁,这次我们在Main方法上应用了这样的特性:[LoaderOptimization(LoaderOptimization.MultiDomain)]。输出的结果就与对Int32类型对象加锁一样。

   1: [LoaderOptimization(LoaderOptimization.MultiDomain)]
   2: static void Main(string[] args)
   3: {
   4:     Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(Foo), 2000);
   5:     Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(Foo));
   6:  
   7:     Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
   8:     Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
   9:  
  10:     lockObjThread.Start();
  11:     Thread.Sleep(500);
  12:     checkLockThread.Start();
  13: }

  输出结果:

   1: [Foo] Successfully lock the object.
   2: [Bar] The object is locked.

  又一个关于加锁的注意:谨慎地对Type对象进行加锁。

  关于CLR内存管理一些深层次的讨论[上篇]
关于CLR内存管理一些深层次的讨论[下篇]

时间: 2024-10-25 09:38:55

一起谈.NET技术,关于CLR内存管理一些深层次的讨论 [上篇]的相关文章

关于CLR内存管理一些深层次的讨论[上篇]

半年之前,PM让我在部门内部进行一次关于"内存泄露"的专题分享,我为此准备了一份PPT.今天无意中将其翻出来,觉得里面提到的关于CLR下关于内存管理部分的内存还有点意思.为此,今天按照PPT的内容写了一篇文章.本篇文章不会在讨论那些我们熟悉的话题,比如"值类型引用类型具有怎样的区别?"."垃圾回收分为几个步骤?"."Finalizer和Dispose有何不同".等等,而是讨论一些不同的内容.整篇文章分上下两篇,上篇主要谈论的是

一起谈.NET技术,关于CLR内存管理一些深层次的讨论 [下篇]

<上篇>中我们主要讨论的是程序集(Assembly)和应用程序域(AppDomain)的话题,着重介绍了两个不同的程序集加载方式--独占方式和共享方式(中立域方式):以及基于进程范围内的字符串驻留.这篇将关注点放在托管对象创建时内存的分配和对大对象(LO:Large Object)的回收上,不对之处,还望各位能够及时指出. 一.从类型(Type)与实例(Instance)谈起 在面向对象的世界中,类型和实例是两个核心的要素.不论是类型和实例,相关的信息比如加载到内存中,对应着某一块或者多块连续

关于CLR内存管理一些深层次的讨论[下篇]

<上篇>中我们主要讨论的是程序集(Assembly)和应用程序域(AppDomain)的话题,着重介绍了两个不同的程序集加载方式--独占方式和共享方式(中立域方式):以及基于进程范围内的字符串驻留.这篇将关注点放在托管对象创建时内存的分配和对大对象(LO:Large Object)的回收上,不对之处,还望各位能够及时指出. 目录 一.从类型(Type)与实例(Instance)谈起 二.实例内存分配不仅限于GC堆 三.实例对类型的引用 四.LOH中的对象如何被回收 一.从类型(Type)与实例

一起谈.NET技术,C#权限管理和设计浅谈

权限管理是很多软件中相当重要的一个模块它的设计的好坏直接影响到软件的安全性.权限管理的可扩展性和易操作性 以及代码中权限判断的复杂程度和效率等方面.此文主要想和大家分享的是这段时间,对权限管理和设计的断断续续的思考学习,和个人的一些软件开发等方面的看法. 提到'权限管理和设计',大家可能会第一时间想到这园子里的吉日嘎拉,在这方面他可以算是'大牛'或专家 他的'通用权限管理系统',究竟做的怎样,看看他的博客就差不多可以知道了(貌似我在给他做推广,呵呵...,but in fact,is not),

[CLR via C#]21. 自动内存管理(垃圾回收机制)

原文:[CLR via C#]21. 自动内存管理(垃圾回收机制) 目录 理解垃圾回收平台的基本工作原理 垃圾回收算法 垃圾回收与调试 使用终结操作来释放本地资源 对托管资源使用终结操作 是什么导致Finalize方法被调用 终结操作揭秘 Dispose模式:强制对象清理资源 使用实现了Dispose模式的类型 C#的using语句 手动监视和控制对象的生存期 对象复活 代 线程劫持 大对象 一.理解垃圾回收平台的基本工作原理 值类型(含所有枚举类型).集合类型.String.Attribute

Linux内核中的内存管理浅谈

 [十月往昔]--Linux内核中的内存管理浅谈 为什么要叫做"十月往昔"呢?是为了纪念我的原博客. 不知道为什么,突然想来一个新的开始--而那个博客存活至今刚好十个月,也有十个月里的文档. 十月往昔,总有一些觉得珍贵的,所以搬迁到这里来. 而这篇文章是在09.04.20-09.04.21里写的. Jason Lee   ------------–cut-line   1.基本框架(此处主要谈页式内存管理) 4G是一个比较敏感的字眼,早些日子,大多数机器(或者说操作系统)支持的内存上限

Windows Server 2008 Hyper-V虚拟化内存管理技术

如果说即将到来的Windows 6.1 SP1的RemoteFX多少还和桌面操作系统有些关系的话,那么另一个功能Dynamic Memory就几乎和Windows 7没什么关系了.Dynamic Memory功能的全称是Hyper-V Dynamic Memory,实际上,它是对Hyper-Vhttp://www.aliyun.com/zixun/aggregation/13883.html">虚拟化技术的内存管理能力的一个增强扩展,结合之前的RemoteFX,和Dynamic Memor

应用协同的进程组内存管理支撑技术

应用协同的进程组内存管理支撑技术 陈鲍孜 吴庆波 谭郁松 云计算进行资源聚合的一种重要方式是将不同用户.不同特征的应用聚合起来进行混合部署.同时运行.相比之下,用户态应用的垃圾回收器对服务个体的内存管理针对性更好,而操作系统对整体内存资源分配能力更强.现有内核的机制仅能保证服务在全局内存或进程组内存使用达到上限时被动地进行垃圾回收.结合Linux内核中的进程控制组机制以及eventfd事件通知机制,设计实现了一个简单高效的应用协同分组内存管理的内核支撑机制.通过在内核中增加应用协同的内存管理机制

一起谈.NET技术,C#中字符串的内存分配与驻留池

刚开始学习C#的时候,就听说CLR对于String类有一种特别的内存管理机制:有时候,明明声明了两个String类的对象,但是他们偏偏却指向同一个实例.如下: String s1 = "Hello";String s2 = "Hello"; //s2和s1的实际值都是Hellobool same = (object) s1 == (object) s2;//这里比较s1.s2是否引用了同一个对象实例//所以不能写作bool same = s1 == s2; //因为