我的编程空间,编程开发者的网络收藏夹
学习永远不晚

C#内存管理CLR深入讲解(上篇)

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

C#内存管理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方法让指定的方法对应的应用程序域中执行。

public class AppDomainContext
{
    public AppDomain AppDomain { get; private set; }
    private AppDomainContext(AppDomain appDomain)
    {
        this.AppDomain = appDomain;
    }
    public static AppDomainContext NewContext(string friendlyName)
    {
        return new AppDomainContext(AppDomain.CreateDomain(friendlyName));
    }
 
    public void Invoke<T>(Action<T> action) where T : MarshalByRefObject
    {
        T instance = (T)this.AppDomain.CreateInstanceAndUnwrap(typeof(T).Assembly.FullName, typeof(T).FullName);
        action.Invoke(instance);
    }
}

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

public class ObjectLock : MarshalByRefObject
{
    public void Lock(object objectToLock, int millisecondsTimeout)
    {
        lock (objectToLock)
        {
            Console.WriteLine("[{0}] Successfully lock the object.", AppDomain.CurrentDomain.FriendlyName);
            Thread.Sleep(millisecondsTimeout);
        }
    }
    public void CheckLock(object objectToLock)
    {
        if (Monitor.TryEnter(objectToLock, 10))
        {
            Console.WriteLine("[{0}] The object is not  locked.", AppDomain.CurrentDomain.FriendlyName);
        }
        else
        {
            Console.WriteLine("[{0}] The object is locked .", AppDomain.CurrentDomain.FriendlyName);
        }
    }
}

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

static void Main(string[] args)
{
    Action<ObjectLock> lockObj = objLock => objLock.Lock("Hello World!", 2000);
    Action<ObjectLock> checkLock = objLock => objLock.CheckLock("Hello World!");
 
    Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
    Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
 
    lockObjThread.Start();
    Thread.Sleep(500);
    checkLockThread.Start();           
}

输出结果:

1: [Foo] Successfully lock the object.
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类型。

namespace Artech.MemAllocation
{
    public class Foo
    {}
}

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

static void Main(string[] args)
{
    Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(Foo), 2000);
    Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(Foo));
 
    Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
    Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
 
    lockObjThread.Start();
    Thread.Sleep(500);
    checkLockThread.Start();
}

输出结果:

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

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

static void Main(string[] args)
{
    Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(int), 2000);
    Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(int));
 
    Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
    Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
 
    lockObjThread.Start();
    Thread.Sleep(500);
    checkLockThread.Start();
}

输出结果:

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

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

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

[LoaderOptimization(LoaderOptimization.MultiDomain)]
static void Main(string[] args)
{
    Action<ObjectLock> lockObj = objLock => objLock.Lock(typeof(Foo), 2000);
    Action<ObjectLock> checkLock = objLock => objLock.CheckLock(typeof(Foo));
 
    Thread lockObjThread = new Thread(() => AppDomainContext.NewContext("Foo").Invoke<ObjectLock>(lockObj));
    Thread checkLockThread = new Thread(() => AppDomainContext.NewContext("Bar").Invoke<ObjectLock>(checkLock));
 
    lockObjThread.Start();
    Thread.Sleep(500);
    checkLockThread.Start();
}

输出结果:

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

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

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

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

到此这篇关于C#内存管理CLR深入讲解的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持编程网。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

C#内存管理CLR深入讲解(上篇)

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

深入学习 C++,内存管理

在使用动态分配内存时,务必遵循内存分配与释放成对出现的原则,以防止内存泄漏和悬垂指针等问题。同时,合理使用智能指针和RAII等技术也能大大简化内存管理的工作。

C++中的内存管理:深入理解与应用

本文将深入探讨C++内存管理的各个方面,包括堆与栈的区别、动态内存分配、内存泄漏及其预防策略,旨在帮助读者更深入地理解这一关键主题。
C++内存编程2024-11-30

Python上下文管理器深入讲解

Python有三大神器,一个是装饰器,一个是迭代器、生成器,最后一个就是今天文章的主角--「上下文管理器」。上下文管理器在日常开发中的作用是非常大的,可能有些人用到了也没有意识到这一点
2022-12-21

深入理解 Linux 上的虚拟内存

Linux 发行版要求您在安装期间设置虚拟内存空间(交换分区),但大多数初学者并不知道这有多大用处。以下是您需要了解的有关 Linux 上的虚拟内存的所有信息。

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录