现在,我们可以通过ILDASM工具(一款查看程序集IL代码的软件,在Microsoft SDKs目录中的子目录中)来查看该程序集的元数据表和Main方法中间码。

📅 2026/7/2 5:00:19
现在,我们可以通过ILDASM工具(一款查看程序集IL代码的软件,在Microsoft SDKs目录中的子目录中)来查看该程序集的元数据表和Main方法中间码。
c#源码第一行代码string rootDirectory Environment.CurrentDirectory;被翻译成IL代码 call string [mscorlib/*23000001*/]System.Environment/*01000004*/::get_CurrentDirectory() /* 0A000003 */这句话意思是调用 System.Environment类的get_CurrentDirectory()方法(属性会被编译为一个私有字段对应get/set方法)。点击视图元信息显示即可查看该程序集的元数据。我们可以看到System.Environment标记值为01000004在TypeRef类型引用表中找到该项:注意图TypeRefName下面有该类型中被引用的成员其标记值为0A000003也就是get_CurrentDirectory了。而从其ResolutionScope指向位于0x23000001而得之该类型存在于mscorlib程序集。于是我们打开mscorlib.dll的元数据清单可以在类型定义表(TypeDef)找到System.Environment,可以从元数据得知该类型的一些标志(Flags,常见的public、sealed、class、abstract)也得知继承(Extends)于System.Object。在该类型定义下还有类型的相关信息我们可以在其中找到get_CurrentDirectory方法。 我们可以得到该方法的相关信息这其中表明了该方法位于0x0002b784这个相对虚地址(RVA)接着JIT在新地址处理CIL周而复始。元数据在运行时的作用 https://docs.microsoft.com/zh-cn/dotnet/standard/metadata-and-self-describing-components#run-time-use-of-metadata程序集的规则上文我通过ILDASM来描述CLR执行代码的方式但还不够具体还需要补充的是对于程序集的搜索方式。对于System.Environment类型它存在于mscorlib.dll程序集中demo.exe是个独立的个体它通过csc编译的时候只是注册了引用mscorlib.dll中的类型的引用信息并没有记录mscorlib.dll在磁盘上的位置那么CLR怎么知道get_CurrentDirectory的代码它是从何处读取mscorlib.dll的对于这个问题.NET有个专门的概念定义我们称为 程序集的加载方式。程序集的加载方式对于自身程序集内定义的类型我们可以直接从自身程序集中的元数据中获取对于在其它程序集中定义的类型CLR会通过一组规则来在磁盘中找到该程序集并加载在内存。CLR在查找引用的程序集的位置时候第一个判断条件是 判断该程序集是否被签名。什么是签名强名称程序集就比如大家都叫张三姓名都一样喊一声张三不知道到底在叫谁。这时候我们就必须扩展一下这个名字以让它具有唯一性。我们可以通过sn.exe或VS对项目右键属性在签名选项卡中采取RSA算法对程序集进行数字签名加密公钥加密私钥解密。签名私钥签名公钥验证签名会将构成程序集的所有文件通过哈希算法生成哈希值然后通过非对称加密算法用私钥签名最后公布公钥生成一串token最终将生成一个由程序集名称、版本号、语言文化、公钥组成的唯一标识它相当于一个强化的名称即强名称程序集。mscorlib, Version4.0.0.0, Cultureneutral, PublicKeyTokenb77a5c561934e089我们日常在VS中的项目默认都没有被签名所以就是弱名称程序集。强名称程序集是具有唯一标识性的程序集并且可以通过对比哈希值来比较程序集是否被篡改不过仍然有很多手段和软件可以去掉程序集的签名。需要值得注意的一点是当你试图在已生成好的强名称程序集中引用弱名称程序集那么你必须对弱名称程序集进行签名并在强名称程序集中重新注册。之所以这样是因为一个程序集是否被篡改还要考虑到该程序集所引用的那些程序集根据CLR搜索程序集的规则(下文会介绍)没有被签名的程序集可以被随意替换所以考虑到安全性强名称程序集必须引用强名称程序集否则就会报错需要强名称程序集。.NET Framework 4.5中对强签名的更改https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/enhanced-strong-naming程序集搜索规则事实上按照存储位置来说程序集分为共享(全局)程序集和私有程序集。CLR查找程序集的时候会先判断该程序集是否被强签名如果强签名了那么就会去共享程序集的存储位置(后文的GAC)去找如果没找到或者该程序集没有被强签名那么就从该程序集的同一目录下去寻找。强名称程序集是先找到与程序集名称(VS中对项目右键属性应用程序-程序集名称)相等的文件名称然后 按照唯一标识再来确认确认后CLR加载程序集同时会通过公钥效验该签名来验证程序集是否被篡改(如果想跳过验证可查阅https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/how-to-disable-the-strong-name-bypass-feature)如果强名称程序集被篡改则报错。而弱名称程序集则直接按照与程序集名称相等的文件名称来找如果还是没有找到就以该程序集名称为目录的文件夹下去找。总之如果最终结果就是没找到那就会报System.IO.FileNotFoundException异常即尝试访问磁盘上不存在的文件失败时引发的异常。注意此处文件名称和程序集名称是两个概念不要模棱两可文件CLR头内嵌程序集名称。举个例子我有一个控制台程序其路径为D:\Demo\Debug\demo.exe通过该程序的元数据得知其引用了一个程序集名称为aa的普通程序集引用了一个名为bb的强名称程序集该bb.dll的强名称标识为xx001。现在CLR开始搜索程序集aa首先它会从demo.exe控制台的同一目录(也就是D:\Demo\Debug\)中查找程序集aa搜索文件名为aa.dll的文件如果没找到就在该目录下以程序集名称为目录的目录中查找也就是会查 D:\Demo\Debug\aa\aa.dll这也找不到那就报错。然后CLR开始搜索程序集bbCLR从demo.exe的元数据中发现bb是强名称程序集其标识为:xx001。于是CLR会先从一个被定义为GAC的目录中去通过标识找没找到的话剩下的寻找步骤就和寻找aa一样完全一致了。当然你也可以通过配置文件config中(配置文件存在于应用程序的同一目录中)人为增加程序集搜索规则1.在运行时runtime节点中添加privatePath属性来添加搜索目录不过只能填写相对路径runtime assemblyBinding xmlnsurn:schemas-microsoft-com:asm.v1 probing privatePathrelative1;relative2; / //程序集当前目录下的相对路径目录用;号分割 /assemblyBinding /runtime2.如果程序集是强签名后的那么可以通过codeBase来指定网络路径或本地绝对路径。runtime assemblyBinding xmlnsurn:schemas-microsoft-com:asm.v1 dependentAssembly assemblyIdentity namemyAssembly publicKeyToken32ab4ba45e0a69a1 cultureneutral / codeBase version2.0.0.0 hrefhttp://www.litwareinc.com/myAssembly.dll / /dependentAssembly /assemblyBinding /runtime当然我们还可以在代码中通过AppDomain类中的几个成员来改变搜索规则如AssemblyResolve事件、AppDomainSetup类等。有关运行时节点的描述:runtime 元素 - .NET Framework | Microsoft Learn项目的依赖顺序如果没有通过config或者在代码中来设定CLR搜索程序集的规则那么CLR就按照默认的也就是我上述所说的模式来寻找。所以如果我们通过csc.exe来编译项目引用了其它程序集的话通常需要将那些程序集复制到同一目录下。故而每当我们通过VS编译器对项目右键重新生成项目(重新编译)时VS都会将引用的程序集给复制一份到项目bin\输出目录Debug文件夹下我们可以通过VS中对引用的程序集右键属性-复制本地 True/Flase 来决定这一默认行为。值得一提的是项目间的生成是有序生成的它取决于项目间的依赖顺序。比如Web项目引用BLL项目BLL项目引用了DAL项目。那么当我生成Web项目的时候因为我要注册Bll程序集所以我要先生成Bll程序集而BLL程序集又引用了Dal所以又要先生成Dal程序集所以程序集生成顺序就是DalBLLWeb项目越多编译的时间就越久。程序集之间的依赖顺序决定了编译顺序所以在设计项目间的分层划分时不仅要体现出层级职责还要考虑到依赖顺序。代码存放在哪个项目要有讲究不允许出现互相引用的情况比如A项目中的代码引用BB项目中的代码又引用A。为什么Newtonsoft.Json版本不一致而除了注意编译顺序外我们还要注意程序集间的版本问题版本间的错乱会导致程序的异常。举个经典的例子Newtonsoft.Json的版本警告大多数人都知道通过版本重定向来解决这个问题但很少有人会琢磨为什么会出现这个问题找了一圈文章没找到一个解释的。比如A程序集引用了 C盘:\Newtonsoft.Json 6.0程序集B程序集引用了 从Nuget下载下来的Newtonsoft.Json 10.0程序集此时A引用B就会报发现同一依赖程序集的不同版本间存在无法解决的冲突 这一警告。A引用Newtonsoft.Json 6.0 Func() { var obj Newtonsoft.Json.Obj; B.JsonObj(); } B: 引用Newtonsoft.Json 10.0 JsonObj() { return Newtonsoft.Json.Obj; }A程序集中的Func方法调用了B程序集中的JsonObj方法JsonObj方法又调用了Newtonsoft.Json 10.0程序集中的对象那么当执行Func方法时程序就会异常报System.IO.FileNotFoundException: 未能加载文件或程序集Newtonsoft.Json 10.0的错误。这是为什么1.这是因为依赖顺序引起的。A引用了B首先会先生成B而B引用了 Newtonsoft.Json 10.0那么VS就会将源引用文件(Newtonsoft.Json 10.0)复制到B程序集同一目录(bin/Debug)下名为Newtonsoft.Json.dll文件其内嵌程序集版本为10.0。2.然后A引用了B所以会将B程序集和B程序集的依赖项(Newtonsoft.Json.dll)给复制到A的程序集目录下而A又引用了C盘的Newtonsoft.Json 6.0程序集文件所以又将C:\Newtonsoft.Json.dll文件给复制到自己程序集目录下。因为两个Newtonsoft.Json.dll重名所以直接覆盖了前者那么只保留了Newtonsoft.Json 6.0。3.当我们调用Func方法中的B.Convert()时候CLR会搜索B程序集找到后再调用 return Newtonsoft.Json.Obj 这行代码而这行代码又用到了Newtonsoft.Json程序集接下来CLR搜索Newtonsoft.Json.dll文件名称满足接下来CLR判断其标识发现版本号是6.0与B程序集清单里注册的10.0版本不符故而才会报出异常未能加载文件或程序集Newtonsoft.Json 10.0。以上就是为何Newtonsoft.Json版本不一致会导致错误的原因其也诠释了CLR搜索程序集的一个过程。那么如果我执意如此有什么好的解决方法能让程序顺利执行呢有有2个方法。第一种通过bindingRedirect节点重定向即当找到10.0的版本时给定向到6.0版本View Code如何在编译时加载两个相同的程序集注意我看过有的文章里写的一个AppDomain只能加载一个相同的程序集很多人都以为不能同时加载2个不同版本的程序集实际上CLR是可以同时加载Newtonsoft.Json 6.0和Newtonsoft.Json 10.0的。第二种对每个版本指定codeBase路径然后分别放上不同版本的程序集这样就可以加载两个相同的程序集。View Code如何同时调用两个两个相同命名空间和类型的程序集除了程序集版本不同外还有一种情况就是我一个项目同时引用了程序集A和程序集B但程序集A和程序集B中的命名空间和类型名称完全一模一样这个时候我调用任意一个类型都无法区分它是来自于哪个程序集的那么这种情况我们可以使用extern alias外部别名。我们需要在所有代码前定义别名extern alias a;extern alias b;然后在VS中对引用的程序集右键属性-别名分别将其更改为a和b(或在csc中通过/r:{别名}{程序集}.dll)。在代码中通过 {别名}::{命名空间}.{类型}的方式来使用。extern-alias介绍 https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/extern-alias共享程序集GAC我上面说了这么多有关CLR加载程序集的细节和规则事实上类似于mscorlib.dll、System.dll这样的FCL类库被引用的如此频繁它已经是我们.NET编程中必不可少的一部分几尽每个项目都会引用为了不再每次使用的时候都复制一份所以计算机上有一个位置专门存储这些我们都会用到的程序集叫做全局程序集缓存(Global Assembly Cache,GAC)这个位置一般位于C:\Windows\Microsoft.NET\assembly和3.5之前版本的C:\Windows\assembly。既然是共享存放的位置那不可避免的会遇到文件名重复的情况那么为了杜绝该类情况规定在GAC中只能存在强名称程序集每当CLR要加载强名称程序集时会先通过标识去GAC中查找而考虑到程序集文件名称一致但版本文化等复杂的情况所以GAC有自己的一套目录结构。我们如果想将自己的程序集放入GAC中那么就必须先签名然后通过如gacutil.exe工具(其存在于命令行工具中 https://docs.microsoft.com/zh-cn/dotnet/framework/tools/developer-command-prompt-for-vs中)来注册至GAC中值得一提的是在将强名称程序集安装在GAC中会效验签名。GAC工具 Gacutil.exe (Global Assembly Cache Tool) - .NET Framework | Microsoft Learn延伸CLR是按需加载程序集的没有执行代码也就没有调用相应的指令没有相应的指令CLR也不会对其进行相应的操作。 当我们执行Environment.CurrentDirectory这段代码的时候CLR首先要获取Environment类型信息通过自身元数据得知其存在mscorlib.dll程序集中所以CLR要加载该程序集而mscorlib.dll又由于其地位特殊早在CLR初始化的时候就已经被类型加载器自动加载至内存中所以这行代码可以直接在内存中读取到类型的方法信息。在这个章节我虽然描述了CLR搜索程序集的规则但事实上加载程序集读取类型信息远远没有这么简单这涉及到了属于.NET Framework独有的应用程序域概念和内存信息的查找。简单延伸两个问题mscorlib.dll被加载在哪里内存堆中又是什么样的一个情况应用程序域传统非托管程序是直接承载在Windows进程中托管程序是承载在.NET虚拟机CLR上的而在CLR中管控的这部分资源中被分成了一个个逻辑上的分区这个逻辑分区被称为应用程序域是.NET Framework中定义的一个概念。因为堆内存的构建和删除都通过GC去托管降低了人为出错的几率在此特性基础上.NET强调在一个进程中通过CLR强大的管理建立起对资源逻辑上的隔离区域每个区域的应用程序互不影响从而让托管代码程序的安全性和健壮性得到了提升。熟悉程序集加载规则和AppDomain是在.NET技术下进行插件编程的前提。AppDomain这部分概念并不复杂。当启动一个托管程序时最先启动的是CLR在这过程中会通过代码初始化三个逻辑区域最先是SystemDomain系统程序域然后是SharedDoamin共享域最后是{程序集名称}Domain默认域。系统程序域里维持着一些系统构建项我们可以通过这些项来监控并管理其它应用程序域等。共享域存放着其它域都会访问到的一些信息当共享域初始化完毕后会自动加载mscorlib.dll程序集至该共享域。而默认域则用储存自身程序集的信息我们的主程序集就会被加载至这个默认域中执行程序入口方法在没有特殊动作外所产生的一切耗费都发生在该域。我们可以在代码中创建和卸载应用程序域域与域之间有隔离性挂掉A域不会影响到B域并且对于每一个加载的程序集都要指定域的没有在代码中指定域的话默认都是加载至默认域中。AppDomain可以想象成组的概念AppDomain包含了我们加载的一组程序集。我们通过代码卸载AppDomain即同时卸载了该AppDomain中所加载的所有程序集在内存中的相关区域。AppDomain的初衷是边缘隔离它可以让程序不重新启动而长时间运行围绕着该概念建立的体系从而让我们能够使用.NET技术进行插件编程。当我们想让程序在不关闭不重新部署的情况下添加一个新的功能或者改变某一块功能我们可以这样做将程序的主模块仍默认加载至默认域再创建一个新的应用程序域然后将需要更改或替换的模块的程序集加载至该域每当更改和替换的时候直接卸载该域即可。 而因为域的隔离性我在A域和B域加载同一个程序集那么A域和B域就会各存在内存地址不同但数据相同的程序集数据。跨边界访问事实上在开发中我们还应该注意跨域访问对象的操作(即在A域中的程序集代码直接调用B域中的对象)是与平常编程中有所不同的一个域中的应用程序不能直接访问另一个域中的代码和数据对于这样的在进程内跨域访问操作分两类。一是按引用封送需要继承System.MarshalByRefObject传递的是该对象的代理引用与源域有相同的生命周期。二是按值封送需要被[Serializable]标记是通过序列化传递的副本副本与源域的对象无关。无论哪种方式都涉及到两个域直接的封送、解封所以跨域访问调用不适用于过高频率。(比如原来你是这样调用对象 var usernew User(); 现在你要这样var user(User){应用程序域对象实例}.CreateInstanceFromAndUnwrap(Model.dll,Model.User); )值得注意的是应用程序域是对程序集的组的划分它与进程中的线程是两个一横一竖方向不一样的概念不应该将这2个概念放在一起比较。我们可以通过Thread.GetDomain来查看执行线程所在的域。应用程序域在类库中是System.AppDomain类,部分重要的成员有获取当前 System.Threading.Thread 的当前应用程序域 public static AppDomain CurrentDomain { get; } 使用指定的名称新建应用程序域 public static AppDomain CreateDomain(string friendlyName); 卸载指定的应用程序域。 public static void Unload(AppDomain domain); 指示是否对当前进程启用应用程序域的 CPU 和内存监视开启后可以根据相关属性进行监控 public static bool MonitoringIsEnabled { get; set; } 当前域托管代码抛出异常时最先发生的一个事件框架设计中可以用到 public event EventHandlerFirstChanceExceptionEventArgs FirstChanceException; 当某个异常未被捕获时调用该事件如代码里只catch了a异常实际产生的是 b异常那么b异常就没有捕捉到。 public event UnhandledExceptionEventHandler UnhandledException; 为指定的应用程序域属性分配指定值。该应用程序域的局部存储值该存储不划分上下文和线程均可通过GetData获取。 public void SetData(string name, object data); 如果想使用托管代码来覆盖CLR的默认行为https://msdn.microsoft.com/zh-cn/library/system.appdomainmanager(vvs.85).aspx public AppDomainManager DomainManager { get; } 返回域的配置信息如在config中配置的节点信息 public AppDomainSetup SetupInformation { get; }应用程序域 应用程序域 - .NET Framework | Microsoft LearnAppDomain和AppPool注意此处的AppDomain应用程序域 和 IIS中的AppPool应用程序池 是2个概念AppPool是IIS独有的概念它也相当于一个组的概念对网站进行划组然后对组进行一些如进程模型、CPU、内存、请求队列的高级配置。内存应用程序域把资源给隔离开这个资源主要指内存。那么什么是内存呢要知道程序运行的过程就是电脑不断通过CPU进行计算的过程这个过程需要读取并产生运算的数据为此我们需要一个拥有足够容量能够快速与CPU交互的存储容器这就是内存了。对于内存大小32位处理器寻址空间最大为2的32次方byte也就是4G内存除去操作系统所占用的公有部分进程大概能占用2G内存而如果是64位处理器则是8T。而在.NET中内存区域分为堆栈和托管堆。堆栈和堆的区别堆和堆栈就内存而言只不过是地址范围的区别。不过堆栈的数据结构和其存储定义让其在时间和空间上都紧密的存储这样能带来更高的内存密度能在CPU缓存和分页系统表现的更好。故而访问堆栈的速度总体来说比访问堆要快点。线程堆栈操作系统会为每条线程分配一定的空间Windwos为1M这称之为线程堆栈。在CLR中的栈主要用来执行线程方法时保存临时的局部变量和函数所需的参数及返回的值等在栈上的成员不受GC管理器的控制它们由操作系统负责分配当线程走出方法后该栈上成员采用后进先出的顺序由操作系统负责释放执行效率高。而托管堆则没有固定容量限制它取决于操作系统允许进程分配的内存大小和程序本身对内存的使用情况托管堆主要用来存放对象实例不需要我们人工去分配和释放其由GC管理器托管。为什么值类型存储在栈上不同的类型拥有不同的编译时规则和运行时内存分配行为我们应知道C# 是一种强类型语言每个变量和常量都有一个类型在.NET中每种类型又被定义为值类型或引用类型。使用 struct、enum 关键字直接派生于System.ValueType定义的类型是值类型使用 class、interface、delagate 关键字派生于System.Object定义的类型是引用类型。对于在一个方法中产生的值类型成员将其值分配在栈中。这样做的原因是因为值类型的值其占用固定内存的大小。C#中int关键字对应BCL中的Int32short对应Int16。Int32为2的32位如果把32个二进制数排列开来我们要求既能表达正数也能表达负数所以得需要其中1位来表达正负首位是0则为首位是1则为-那么我们能表示数据的数就只有31位了而0是介于-1和1之间的整数所以对应的Int32能表现的就是2的31次方到2的31次方-1即2147483647和-2147483648这个整数段。1个字节8位32位就是4个字节像这种以Int32为代表的值类型本身就是固定的内存占用大小所以将值类型放在内存连续分配的栈中。托管堆模型而引用类型相比值类型就有点特殊newobj创建一个引用类型因其类型内的引用对象可以指向任何类型故而无法准确得知其固定大小所以像对于引用类型这种无法预知的容易产生内存碎片的动态内存我们把它放到托管堆中存储。托管堆由GC托管其分配的核心在于堆中维护着一个nextObjPtr指针我们每次实例(new)一个对象的时候CLR将对象存入堆中并在栈中存放该对象的起始地址然后该指针都会根据该对象的大小来计算下一个对象的起始地址。不同于值类型直接在栈中存放值引用类型则还需要在栈中存放一个代表(指向)堆中对象的值(地址)。而托管堆又可以因存储规则的不同将其分类托管堆可以被分为3类1.用于托管对象实例化的垃圾回收堆又以存储对象大小分为小对象(85000byte)的GC堆(SOHSmall Object Heap)和用于存储大对象实例的(85000byte)大对象堆(LOGLarage Object Heap)。2.用于存储CLR组件和类型系统的加载(Loader)堆其中又以使用频率分为经常访问的高频堆(里面包含有MethodTables方法表, MeghodDescs方法描述, FieldDescs方法描述和InterfaceMaps接口图)和较低的低频堆和Stub堆(辅助代码如JIT编译后修改机器代码指令地址环节)。3.用于存储JIT代码的堆及其它杂项的堆。加载程序集就是将程序集中的信息给映射在加载堆对产生的实例对象存放至垃圾回收堆。前文说过应用程序域是指通过CLR管理而建立起的逻辑上的内存边界那么每个域都有其自己的加载堆只有卸载应用程序域的时候才会回收该域对应的加载堆。而加载堆中的高频堆包含的有一个非常重要的数据结构表---方法表每个类型都仅有一份方法表(MethodTables)它是对象的第一个实例创建前的类加载活动的结果它主要包含了我们所关注的3部分信息1包含指向EEClass的一个指针。EEClass是一个非常重要的数据结构当类加载器加载到该类型时会从元数据中创建出EEClassEEClass里主要存放着与类型相关的表达信息。2包含指向各自方法的方法描述器(MethodDesc)的指针逻辑组成的线性表信息:继承的虚函数, 新虚函数, 实例方法, 静态方法。3包含指向静态字段的指针。那么实例一个对象CLR是如何将该对象所对应的类型行为及信息的内存位置(加载堆)关联起来的呢原来在托管堆上的每个对象都有2个额外的供于CLR使用的成员我们是访问不到的其中一个就是类型对象指针它指向位于加载堆中的方法表从而让类型的状态和行为关联了起来 类型指针的这部分概念我们可以想象成obj.GetType()方法获得的运行时对象类型的实例。而另一个成员就是同步块索引其主要用于2点1.关联内置SyncBlock数组的项从而完成互斥锁等目的。 2.是对象Hash值计算的输入参数之一。上述gif是我简单画的一个图可以看到对于方法中申明的值类型变量其在栈中作为一块值表示我们可以直接通过c#运算符sizeof来获得值类型所占byte大小。而方法中申明的引用类型变量其在托管堆中存放着对象实例(对象实例至少会包含上述两个固定成员以及实例数据可能)在栈中存放着指向该实例的地址。当我new一个引用对象的时候会先分配同步块索引(也叫对象头字节)然后是类型指针最后是类型实例数据(静态字段的指针存在于方法表中)。会先分配对象的字段成员然后分配对象父类的字段成员接着再执行父类的构造函数最后才是本对象的构造函数。这个多态的过程对于CLR来说就是一系列指令的集合所以不能纠结new一个子类对象是否会也会new一个父类对象这样的问题。而也正是因为引用类型的这样一个特征我们虽然可以估计一个实例大概占用多少内存但对于具体占用的大小我们需要专门的工具来测量。对于引用类型u2u1我们在赋值的时候实际上赋的是地址那么我改动数据实际上是改动该地址指向的数据这样一来因为u2和u1都指向同一块区域所以我对u1的改动会影响到u2对u2的改动会影响到u1。如果我想互不影响那么我可以继承IClone接口来实现内存克隆已有的CLR实现是浅克隆方法但也只能克隆值类型和String(string是个特殊的引用类型对于string的更改其会产生一个新实例对象)如果对包含其它引用类型的这部分我们可以自己通过其它手段实现深克隆如序列化、反射等方式来完成。而如果引用类型中包含有值类型字段那么该字段仍然分配在堆上。对于值类型ab我们在赋值的时候实际上是新建了个值那么我改动a的值那就只会改动a的值改动b的值就只会改动b的值。而如果值类型(如struct)中包含的有引用类型那么仍是同样的规则引用类型的那部分实例在托管堆中地址在栈上。我如果将值类型放到引用类型中(如object a3)会在栈中生成一个地址在堆中生成该值类型的值对象还会再生成这类型指针和同步块索引两个字段这也就是常说装箱反过来就是拆箱。每一次的这样的操作都会涉及到内存的分布、拷贝可见装箱和拆箱是有性能损耗因此应该减少值类型和引用类型之间转换的次数。但对于引用类型间的子类父类的转换仅是指令的执行消耗几尽没有开销。选class还是struct那么我到底是该new一个class呢还是选择struct呢通过上文知道对于class用完之后对象仍然存在托管堆占用内存。对于struct用完之后直接由操作系统销毁。那么在实际开发中定义类型时选择class还是struct就需要注意了要综合应用场景来辨别。struct存在于栈上栈和托管堆比较最大的优势就是即用即毁。所以如果我们单纯的传递一个类型那么选择struct比较合适。但须注意线程堆栈有容量限制不可多存放超大量的值类型对象并且因为是值类型直接传递副本所以struct作为方法参数是线程安全的但同样要避免装箱的操作。而相比较class如果类型中还需要多一些封装继承多态的行为那么class当然是更好的选择。GC管理器值得注意的是当我new完一个对象不再使用的时候这个对象在堆中所占用的内存如何处理在非托管世界中可以通过代码手动进行释放但在.NET中堆完全由CLR托管也就是说GC堆是如何具体来释放的呢当GC堆需要进行清理的时候GC收集器就会通过一定的算法来清理堆中的对象并且版本不同算法也不同。最主要的则为Mark-Compact标记-压缩算法。这个算法的大概含义就是通过一个图的数据结构来收集对象的根这个根就是引用地址可以理解为指向托管堆的这根关系线。当触发这个算法时会检查图中的每个根是否可达如果可达就对其标记然后在堆上找到剩余没有标记(也就是不可达)的对象进行删除这样那些不在使用的堆中对象就删除了。前面说了因为nextObjPtr的缘故在堆中分配的对象都是连续分配的因为未被标记而被删除那么经过删除后的堆就会显得支零破碎那么为了避免空间碎片化所以需要一个操作来让堆中的对象再变得紧凑、连续而这样一个操作就叫做Compact压缩。而对堆中的分散的对象进行挪动后还会修改这些被挪动对象的指向地址从而得以正确的访问最后重新更新一下nextObjPtr指针周而复始。而为了优化内存结构减少在图中搜索的成本GC机制又为每个托管堆对象定义了一个属性将每个对象分成了3个等级这个属性就叫做代0代、1代、2代。每当new一个对象的时候该对象都会被定义为第0代当GC开始回收的时候先从0代回收在这一次回收动作之后0代中没有被回收的对象则会被定义成第1代。当回收第1代的时候第1代中没有被清理掉的对象就会被定义到第2代。CLR初始化时会为0/1/2这三代选择一个预算的容量。0代通常以256 KB-4 MB之间的预算开始1代的典型起始预算为512 KB-4 MB2代不受限制最大可扩展至操作系统进程的整个内存空间。比如第0代为256K第1代为2MB。我们不停的new对象直到这些对象达到256k的时候GC会进行一次垃圾回收假设这次回收中回收了156k的不可达对象剩余100k的对象没有被回收那么这100k的对象就被定义为第1代。现在就变成了第0代里面什么都没有第1代里放的有100k的对象。这样周而复始GC清除的永远都只有第0代对象除非当第一代中的对象累积达到了定义的2MB的时候才会连同清理第1代然后第1代中活着的部分再升级成第二代...第二代的容量是没有限制但是它有动态的阈值(因为等到整个内存空间已满以执行垃圾回收是没有意义的)当达到第二代的阈值后会触发一次0/1/2代完整的垃圾收集。也就是说代数越长说明这个对象经历了回收的次数也就越多那么也就意味着该对象是不容易被清除的。这种分代的思想来将对象分割成新老对象进而配对不同的清除条件这种巧妙的思想避免了直接清理整个堆的尴尬。弱引用、弱事件GC收集器会在第0代饱和时开始回收托管堆对象对于那些已经申明或绑定的不经访问的对象或事件因为不经常访问而且还占内存(有点懒加载的意思)所以即时对象可达但我想在GC回收的时候仍然对其回收当需要用到的时候再创建这种情况该怎么办那么这其中就引入了两个概念WeakReference弱引用、WeakEventManager弱事件对于这2两个不区分语言的共同概念大家可自行扩展百度此处就不再举例。GC堆回收那么除了通过new对象而达到代的阈(临界)值时还有什么能够导致垃圾堆进行垃圾回收呢 还可能windows报告内存不足、CLR卸载AppDomain、CLR关闭等其它特殊情况。或者我们还可以自己通过代码调用。.NET有GC来帮助开发人员管理内存并且版本也在不断迭代。GC帮我们托管内存但仍然提供了System.GC类让开发人员能够轻微的协助管理。 这其中有一个可以清理内存的方法(并没有提供清理某个对象的方法)GC.Collect方法可以对所有或指定代进行即时垃圾回收(如果想调试需在release模式下才有效果)。这个方法尽量别用因为它会扰乱代与代间的秩序从而让低代的垃圾对象跑到生命周期长的高代中。GC还提供了判断当前对象所处代数、判断指定代数经历了多少次垃圾回收、获取已在托管堆中分配的字节数这样的三个方法我们可以从这3个方法简单的了解托管堆的情况。