CLR寄宿和AppDomain
一、CLR寄宿
.net framework在windows平台的顶部允许。者意味着.net framework必须用windows能理解的技术来构建。所有托管模块和程序集文件必须使用windows PE文件格式,而且要么是windows exe文件,要么是DLL文件
1,MSCorEE.dll(垫片)
①CLRCreateInstance函数在MSCorEE.dll文件中实现。“垫片”的工作是决定创建哪个版本的CLR(1.0、2.0、3.0的CLR代码在MSCorWks.dll文件中;版本4则在Clr.dll文件中)
②CLRCreateInstance函数可返回一个ICLRMetaHost接口。宿主应用程序可调用这个接口的GetRuntime函数,指定宿主要创建的CLR版本。然后,垫片将所需版本的CLR加载到宿主的进程中
2,ICLRRuntimeHost可以做以下事情
①设置宿主管理器。该诉CLR宿主想参与与涉及以下操作的决策:内存分配、线程调度/同步以及程序集加载等。宿主还可声明它想获得有关垃圾回收启动和停止以及特定操作超时的通知
②获取CLR管理器。告诉CLR阻止使用某些类/成员。另外,宿主能分辨哪些代码可以调试,哪些不可以,以及当特定事件(例如AppDomain卸载、CLR停止或者堆栈移除异常)发生时宿主应调用哪个方法
③初始化并启动CLR
④加载程序集并执行其中的代码
⑤停止CLR,阻止任何更多的托管代码在Windows进程中运行
二、AppDomain
CLR COM服务器初始化时会创建一个AppDomain。AppDomain是一组程序集的逻辑容器。CLR初始化时创建的第一个AppDomain称为“默认AppDomain”,这个默认的AppDomain只有在Windows进程终止时才会被销毁
1,AppDomain的具体功能
①一个AppDomain的代码不能直接访问另一个AppDomain的代码创建的对象。
一个AppDomain中的代码创建了一个对象后,该对象便被该AppDomain“拥有”。换言之,它的生存期不能超过创建它的代码所有的AppDomain。一个AppDomain中的代码要访问另一个AppDomain的对象,只能使用“按引用封送”或者“按值封送”的予以。这就强制建立了清晰的分割和边界,因为一个AppDomain中的代码不能直接引用另一个AppDomain中的代码创建的对象。这种隔离使AppDomain能很容易地从进程中卸载,不会影响其他AppDomain正在运行的代码
②AppDomain可以卸载
CLR不支持从AppDomain中卸载特定的程序集,但可以告诉CLR卸载一个AppDomain,从而卸载该AppDomain当前包含的所有程序集
③AppDomain可以单独保护
AppDomain创建后会应用一个权限集,它决定了想这个AppDomain中运行的程序集授予最大权限,正是由于存在这样的权限,所以当宿主加载一些代码后,可以保证这些代码不会破坏(或读取)宿主本身使用的一些重要数据结构
④AppDomain可以单独配置
AppDomain创建后会关联一组配置设置。这些设置主要影响CLR在AppDomain中加载程序集的方式,设计搜索路径、版本绑定重定向、卷影复制以及加载器优化
2,Windows进程
中立的AppDomain:
MSCorLib.dll包含了System.Object,System.Int32以及其他所有与.NET Framework密不可分的类型
该Loader堆中的所有类型对象,以及为这些类型定义的JIT编译生成的所有本机代码,都会由进程中的所有AppDomain共享
该中立的AppDomain不能被卸载,除非终止windows进程
三、跨越AppDomain边界访问对象
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Remoting; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication7 { class Program { static void Main(string[] args) { Marshalling(); } private static void Marshalling() { //获取AppDomain引用(“调用线程”当前正在该AppDomain中执行) AppDomain adCallingThreadDomain = Thread.GetDomain(); //每个AppDomain都分配了友好字符串名称(以便调试) //获取这个AppDomain的友好名称并显示它 String CallingDomainName = adCallingThreadDomain.FriendlyName; Console.WriteLine("默认AppDomain友好的名称={0}",adCallingThreadDomain); //获取并显示我们的AppDomain中包含了“Main”方法的程序集 String exeAssembly = Assembly.GetEntryAssembly().FullName; Console.WriteLine("包含“Main”方法的程序集={0}", exeAssembly); //定义局部变量来引用一个AppDomain AppDomain ad2 = null; //************************************************************************************************************ //************************************************************ DEMO 1:使用“按引用封送”进行跨AppDomain通信 *** //************************************************************************************************************ Console.WriteLine("{0} Demo1 按引用封送", Environment.NewLine); //新建一个AppDomain(从当前AppDomain继承安全性和配置) ad2 = AppDomain.CreateDomain("AD #2", null, null); MarshalByRefType mbrt = null; //将我们的程序集加载到新AppDomain,构造一个对象,把它封送回我们的AppDomain(实际得到对一个代理的引用) mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); Console.WriteLine("Type={0}", mbrt.GetType());//CLR在类型上撒谎了 //证明得到的是对一个代理对象的引用 Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbrt)); //看起来像是在MarshalByRefType上调用了一个方法,实则不然。 //我们是在代理类型上调用了一个方法,代理是线程切换到拥有对象的那个 //AppDomain,并在真实的对象上调用这个方法 mbrt.SomeMethod(); //卸载新的AppDomain AppDomain.Unload(ad2); //此时,mbrt引用了一个有效的代理对象;代理对象引用一个无效的AppDomain try { mbrt.SomeMethod(); Console.WriteLine("调用成功"); } catch (AppDomainUnloadedException) { Console.WriteLine("调用失败,AppDomain被卸载了"); } //************************************************************************************************************ //************************************************************ DEMO 2:使用“按值封送”进行跨AppDomain通信 *** //************************************************************************************************************ Console.WriteLine("{0} Demo2 按值封送", Environment.NewLine); ad2 = AppDomain.CreateDomain("AD #2", null, null); mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); //对象的方法返回所返回对象的副本 //对象按值(而非按引用)封送 MarshalByValType mbvt= mbrt.MethodWithReturn(); //证明得到的是对一个代理对象的引用 Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbvt)); //看起来在MarshalByValType上调用一个方法,实际也是如此 Console.WriteLine("Return object created " + mbvt.ToString()); //卸载新的AppDomain AppDomain.Unload(ad2); //此时,mbrt引用了一个有效的x代理对象;代理对象引用一个无效的AppDomain try { //卸载AppDomain之后调用mbvt方法不会抛出异常 Console.WriteLine("Return object created " + mbvt.ToString()); Console.WriteLine("调用成功"); } catch (AppDomainUnloadedException) { Console.WriteLine("调用失败,AppDomain被卸载了"); } //************************************************************************************************************ //************************************************************ DEMO 3:使用不可封送的类型进行跨AppDomain通信 *** //************************************************************************************************************ ad2 = AppDomain.CreateDomain("AD #2", null, null); mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); try { NonMarshalableType nmt = mbrt.MethodArgAndReturn(CallingDomainName);//抛出异常:未标记为可序列化 } catch (SerializationException) { Console.WriteLine("抛出异常:未标记为可序列化"); } Console.ReadKey(); } } //该类型的实例可跨越AppDomain的边界“按引用封送” public sealed class MarshalByRefType : MarshalByRefObject { public MarshalByRefType() { Console.WriteLine("{0} ctor running in {1}", GetType(), Thread.GetDomain().FriendlyName); } public void SomeMethod() { Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName); } public MarshalByValType MethodWithReturn() { Console.WriteLine("Execute in " + Thread.GetDomain().FriendlyName); MarshalByValType t = new MarshalByValType(); return t; } public NonMarshalableType MethodArgAndReturn(string callingDomainName) { //注意:callingDomainName是可序列化的 Console.WriteLine("Calling from '{0}' to '{1}'.", callingDomainName, Thread.GetDomain().FriendlyName); NonMarshalableType t=new NonMarshalableType(); return t; } } //该类的实例可跨越AppDomain的边界“按值封送” [Serializable] public sealed class MarshalByValType : Object { private DateTime m_creationTime = DateTime.Now;//注意:DateTime是可序列化的 public MarshalByValType() { Console.WriteLine("{0} ctor running in {1}, Created no {2:D}", GetType(), Thread.GetDomain().FriendlyName, m_creationTime); } public override string ToString() { return m_creationTime.ToLongDateString(); } } //该类的实例不能跨AppDomain边界进行封送 //[Serializable] public sealed class NonMarshalableType : Object { public NonMarshalableType() { Console.WriteLine("Execute in " + Thread.GetDomain().FriendlyName); } } }
一个线程能执行一个AppDomain中的代码,再执行另一个AppDomain的代码。Thread.GetDomain()方法向CLR询问它正在执行哪个AppDomain。AppDomain的FriendlyName属性获取AppDomain的友好名称(默认AppDomain使用可执行文件的名称作为友好名称)
演示1:使用“按引用封送(Marshal-by-Refernce)”AppDomain通信
1,AppDomain.CreateDomain三个参数:
friendlyName:代表新AppDomain的友好名称的一个String,
securityInfo:一个System.Security.Polict.Evidence,是CLR用于计算AppDomain权限集的证据。本例为null,造成新的AppDomain从创建它的AppDomain继承权限集。通常,如果希望围绕AppDomain中的代码创建安全边界,可构造一个System.Security.PermssionSet对象,在其中添加希望的权限对象(实现了IPermission接口的类型实例),将得到的PermssionSet对象引用传给接受一个PermssionSet的CreateDomain方法
info:一个System.AppDomainSetup,代表CLR为新AppDomain使用的配置设置。同样,本例为该参数传递为null,是新的AppDomain从创建它的AppDomain继承配置设置。如果希望对新AppDomain进行特殊配置,可构造一个AppDomainSetup对象,将它的各种属性(例如置文件的名称)设为你希望的值,然后将得到的AppDomainSetup对象引用传给CreateDomain方法
2,AppDomain的CreateInstanceAndUnwrap内部实现
①CreateInstanceAndUnwrap方法导致调用线程从当前AppDomain切换到新的AppDomain
②线程将指定的程序集加载到新AppDomain中,并扫描程序集的类型定义元数据表,查找指定类型
③找到类型后,线程调用该类型的无参构造器(CreateInstanceAndUnwrap方法一些重载方法允许在调用类型的构造器时传递实参)
④现在线程又切换回默认的AppDomain,时CreateInstanceAndUnwrap能返回对新类型对象的引用
3,“按引用封送”的具体含义
当CreateInstanceAndUnwrapA线它封送的一个对象的类型派生自MarshalByRefObject时,CLR就会跨AppDomain边界按引用封送
①源AppDomain想向目标AppDomain发送或返回对象引用时,CLR会在目标AppDomain的Loader堆中定义一个代理类型,代理类型是用原始类型的元数据定义的,所以它看起来和原始类型完全一样,有完全一样的实例成员(属性、事件和方法)。实例字段不会成为(代理)类型的一部分。代理类型确定定义了几个(自己的)实例字段,但这些字段和原始类型的不一致。相反,这些字段只是指出哪个AppDomain“拥有”真实的对象,以及如何在拥有(对象的)AppDomain中找到真实的对象
②在目标AppDomain中定义好这个代理类型之后,CreateInstanceAndUnwrapA方法就会创建代理类型的实例,初始化它的字段来标识源AppDomain和真实对象,然后将这个代理对象的引用返回给目标AppDomain(调用该对象的GetType方法,他会向你撒谎)
③应用程序使用代理调用SomeMethod方法。由于mbrt变量用代理对象,所以会调用由代理实现的SomeMethod。代理的实现利用代理对象中的信息字段,将调用线程从默认AppDomain切换至新AppDomain。现在,该线程的任何行动都在新AppDomain的安全策略和配置设置下运行。线程接着使用代理对象的GCHandle字段查找新AppDomain中的真实对象,并用真实对象调用真实的SomeMethod方法
④AppDomain类的公共静态方法Unload方法会强制CLR卸载指定的AppDomain(包括加载到其中的所有程序集),并强制执行一次垃圾回收,已释放有卸载的AppDomain中的代码创建的所有对象,这时,默认AppDomain的mbrt变量仍然引用一个有效的代理对象,但代理对象已经不再引用一个有效的AppDomain
⑤使用“按引用封送”的语义进行跨AppDomain边界的对象访问,会产生一些性能上的开销。所以一般应该尽量少用这个功能
4,MarshalByRefObject派生
从MarshalByRefObject派生的类型可定义实例字段。但这些实例字段不会成为代理类型的一部分,也不会包含在代理对象中
对派生自的MarshalByRefObject类型的实例字段进行读写时,JIT编译器会自动生成代码,分别调用system.object的FieldGetter或FieldSetter来使用代理对象。这些方法利用反射机制获取或设置字段值,性能很差
派生自MarshalByRefObject的类型真的应该避免定义任何静态成员
演示2:使用“按值封送(Marshal-by-Value)”AppDomain通信
CLR在目标AppDomain中精确的赋值了源对象。然后MethodWithReturn方法返回对这个副本的引用。源AppDomain中的对象和目标AppDomain中的对象有了独立的生存期,它们的状态也可以独立地更改
演示3:使用不可封送的类型跨AppDomain通信
由于NonMarshalableType不是从System. MarshalByRefObject中派生的,也没有用[Serializable]定制特性进行标记,所以不允许MethodArgAndReturn按引用或按值封送对象--对象完全不能跨越AppDomain边界进行封送
四、卸载AppDomain
①CLR挂起进程中执行过托管代码的所有线程
②CLR检查所有线程栈,查看哪些线程正在执行要卸载的AppDomain中的代码,或者哪些线性会在某个时候返回至要卸载的AppDomain。任何栈上有要卸载的AppDomain,CLR都会强制对应的线程抛出一个ThreadAbortException(同时恢复线程的执行)。这将导致线程展开,并执行遇到的所有finally块以清理资源。如果没有代码捕捉ThreadAbortException,它最终会成为未处理的异常,CLR会“吞噬”这个异常,线程会终止,但进程可继续执行
③当第2步发现的所有线程都离开AppDomain后,CLR遍历堆,为引用了“由于卸载的AppDomain创建的对象”的每个代理对象都设置一个标志(flag)、这些代理对象现在知道他们引用的真实对象已经不存在了。现在,任何代码在无效的代理对象上调用方法都会抛出AppDomainUnloadedException异常
④CLR强制垃圾回收,回收由已卸载的AppDomain创建的任何对象的内存。这些对象的Finalze方法被调用,是对象由机会正确清理他们占用的资源
⑤CLR恢复剩余所有线程的执行。调用AppDomain.Unload方法的线程将继续运行;对于AppDomain.Unload的调用是同步进行的
五、监视AppDomain
1,AppDomain的几条MonitoringEnabled属性设置为true显式打开监控。打开监控后,代码可查询AppDomain类提供的以下4个属性
①MonitoringSurvivedProcessMemorySize:这个Int64静态属性返回由当前CLR实例控制的所有AppDomain使用的字节数。这个数字值保证在上一次垃圾回收时时准确的
②MonitoringTotalAllocatedMemorySize:这个Int64实例属性返回特定AppDomain已分配的字节数。这个数字只保证在上一次垃圾回收时是准确的
③MonitoringSuvivedMemorySize:这个Int64实例属性返回特定AppDomain当前正在使用的字节数。这个数字只保证在上一次垃圾回收时是准确的
④MonitoringTotalProcessorTime:这个TimeSpan实例返回特定AppDomain的CPU占用率
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApplication8 { class Program { static void Main(string[] args) { AppDomainResourceMonitoring(); Console.WriteLine(Environment.TickCount); Console.ReadLine(); } public static void AppDomainResourceMonitoring() { using (new AppDomainMonitorDalte(null)) { //分配在回收时能存活的约10MB var list = new List<object>(); for (int x = 0; x < 1000; x++) { list.Add(new byte[1000]); } //分配在回收时存活不了的约20MB for (int x = 0; x < 2000; x++) { new byte[10000].GetType(); } //保持CPU工作约5秒 var stop = Environment.TickCount + 5000; while (Environment.TickCount < stop) ; } } } public class AppDomainMonitorDalte : IDisposable { private AppDomain m_appdomain; private TimeSpan m_thisADCpu; private Int64 m_thisAdMemoryInUse; private Int64 m_thisADMemoryAllocated; static AppDomainMonitorDalte() { //确定已打开AppDomain监视 AppDomain.MonitoringIsEnabled = true; } public AppDomainMonitorDalte(AppDomain ad) { m_appdomain = ad ?? AppDomain.CurrentDomain; m_thisADCpu = m_appdomain.MonitoringTotalProcessorTime; m_thisAdMemoryInUse = m_appdomain.MonitoringSurvivedMemorySize; m_thisADMemoryAllocated = m_appdomain.MonitoringTotalAllocatedMemorySize; } public void Dispose() { GC.Collect(); Console.WriteLine("AppDomain友好名称={0},CPU={1}ms", m_appdomain.FriendlyName, (m_appdomain.MonitoringTotalProcessorTime - m_thisADCpu).TotalMilliseconds); Console.WriteLine("Allocated {0:N0} bytes of which {1:N0} survied GCs", m_appdomain.MonitoringTotalAllocatedMemorySize - m_thisADMemoryAllocated, m_appdomain.MonitoringSurvivedMemorySize - m_thisAdMemoryInUse); } } }
六、AppDomain FirstCance异常通知
为AppDomain的实例事件FirstChanceException添加一个委托就可以了
1,CLR如何处理异常:
①异常首次抛出时,CLR调用向抛出异常的AppDomain登记的所有FirstChanceException回调方法。
②然后。CLR查找栈上同一个AppDomain中的任何catch块,有一个catch块能处理异常,则异常处理完成,将继续执行
③如果AppDomain中没有一个catch块能处理异常,则CLR沿着栈向上来到调用AppDomain,再次抛出同一异常对象(序列化和反序列化之后)
④这个感觉抛出一个新的异常,CLR调用当前AppDomain登记的所有FirstChanceException回调方法
⑤这个过程会一直执行,知道抵达线程栈顶部。如果异常还未处理,则进程终止