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

简介:   半年之前,PM让我在部门内部进行一次关于“内存泄露”的专题分享,我为此准备了一份PPT。今天无意中将其翻出来,觉得里面提到的关于CLR下关于内存管理部分的内存还有点意思。为此,今天按照PPT的内容写了一篇文章。

  半年之前,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内存管理一些深层次的讨论[下篇]

目录
相关文章
|
11天前
|
人工智能 物联网 C语言
SVDQuant:MIT 推出的扩散模型后训练的量化技术,能够将模型的权重和激活值量化至4位,减少内存占用并加速推理过程
SVDQuant是由MIT研究团队推出的扩散模型后训练量化技术,通过将模型的权重和激活值量化至4位,显著减少了内存占用并加速了推理过程。该技术引入了高精度的低秩分支来吸收量化过程中的异常值,支持多种架构,并能无缝集成低秩适配器(LoRAs),为资源受限设备上的大型扩散模型部署提供了有效的解决方案。
38 5
SVDQuant:MIT 推出的扩散模型后训练的量化技术,能够将模型的权重和激活值量化至4位,减少内存占用并加速推理过程
|
1月前
|
自然语言处理 物联网 图形学
.NET 技术凭借其独特的优势和特性,为开发者们提供了一种高效、可靠且富有创造力的开发体验
本文深入探讨了.NET技术的独特优势及其在多个领域的应用,包括企业级应用、Web应用、桌面应用、移动应用和游戏开发。通过强大的工具集、高效的代码管理、跨平台支持及稳定的性能,.NET为开发者提供了高效、可靠的开发体验,并面对技术更新和竞争压力,不断创新发展。
54 7
|
1月前
|
开发框架 安全 .NET
在数字化时代,.NET 技术凭借跨平台兼容性、丰富的开发工具和框架、高效的性能及强大的安全稳定性,成为软件开发的重要支柱
在数字化时代,.NET 技术凭借跨平台兼容性、丰富的开发工具和框架、高效的性能及强大的安全稳定性,成为软件开发的重要支柱。它不仅加速了应用开发进程,提升了开发质量和可靠性,还促进了创新和业务发展,培养了专业人才和技术社区,为软件开发和数字化转型做出了重要贡献。
27 5
|
1月前
|
传感器 人工智能 供应链
.NET开发技术在数字化时代的创新作用,从高效的开发环境、强大的性能表现、丰富的库和框架资源等方面揭示了其关键优势。
本文深入探讨了.NET开发技术在数字化时代的创新作用,从高效的开发环境、强大的性能表现、丰富的库和框架资源等方面揭示了其关键优势。通过企业级应用、Web应用及移动应用的创新案例,展示了.NET在各领域的广泛应用和巨大潜力。展望未来,.NET将与新兴技术深度融合,拓展跨平台开发,推动云原生应用发展,持续创新。
35 4
|
1月前
|
开发框架 .NET C#
.NET 技术凭借高效开发环境、强大框架支持及跨平台特性,在软件开发中占据重要地位
.NET 技术凭借高效开发环境、强大框架支持及跨平台特性,在软件开发中占据重要地位。从企业应用到电子商务,再到移动开发,.NET 均展现出卓越性能,助力开发者提升效率与项目质量,推动行业持续发展。
32 4
|
1月前
|
机器学习/深度学习 人工智能 物联网
.NET 技术:引领未来开发潮流
.NET 技术以其跨平台兼容性、高效的开发体验、强大的性能表现和安全可靠的架构,成为引领未来开发潮流的重要力量。本文深入探讨了 .NET 的核心优势与特点,及其在企业级应用、移动开发、云计算、人工智能等领域的广泛应用,展示了其卓越的应用价值和未来发展前景。
61 5
|
1月前
|
机器学习/深度学习 人工智能 Cloud Native
在数字化时代,.NET 技术凭借其跨平台兼容性、丰富的类库和工具集以及卓越的性能与效率,成为软件开发的重要平台
在数字化时代,.NET 技术凭借其跨平台兼容性、丰富的类库和工具集以及卓越的性能与效率,成为软件开发的重要平台。本文深入解析 .NET 的核心优势,探讨其在企业级应用、Web 开发及移动应用等领域的应用案例,并展望未来在人工智能、云原生等方面的发展趋势。
39 3
|
1月前
|
敏捷开发 缓存 中间件
.NET技术的高效开发模式,涵盖面向对象编程、良好架构设计及高效代码编写与管理三大关键要素
本文深入探讨了.NET技术的高效开发模式,涵盖面向对象编程、良好架构设计及高效代码编写与管理三大关键要素,并通过企业级应用和Web应用开发的实践案例,展示了如何在实际项目中应用这些模式,旨在为开发者提供有益的参考和指导。
25 3
|
1月前
|
开发框架 安全 Java
.NET技术的独特魅力与优势,涵盖高效的开发体验、强大的性能表现、高度的可扩展性及丰富的生态系统等方面,展示了其在软件开发领域的核心竞争力
本文深入探讨了.NET技术的独特魅力与优势,涵盖高效的开发体验、强大的性能表现、高度的可扩展性及丰富的生态系统等方面,展示了其在软件开发领域的核心竞争力。.NET不仅支持跨平台开发,具备出色的安全性和稳定性,还能与多种技术无缝集成,为企业级应用提供全面支持。
36 3
|
1月前
|
人工智能 开发框架 前端开发
C#/.NET/.NET Core技术前沿周刊 | 第 12 期(2024年11.01-11.10)
C#/.NET/.NET Core技术前沿周刊 | 第 12 期(2024年11.01-11.10)