共计 12351 个字符,预计需要花费 31 分钟才能阅读完成。
什么是 AppDomain
AppDomain 是一组程序集的逻辑容器,CLR 初始化时创立的第一个 AppDomain 称为默认 AppDomain,默认的 AppDomain 只有在 Windows 过程终止时才会被销毁。
AppDomain 作用
- 一个 AppDomain 中的代码创立的对象不能由另一个 AppDomain 中的代码间接拜访。一个 AppDomain 中的代码创立一个对象后,该对象被该 AppDomain 领有。这个对象的生存期不可能比创立该对象的代码所在的 AppDomain 生存期长。一个 AppDomain 中的代码要拜访另一个 AppDomain 中的对象,必须应用“按援用封送”或者“按值封送”的语义。从而增强 AppDomain 之间的边界,使得两个不同的 AppDomain 之间不存在对象之间的间接援用。所以能够很容易的从一个过程中卸载 AppDomain 而不会影响到其它应用程序中正在运行的代码
- AppDomain 能够卸载,CLR 不反对卸载 AppDomain 中的单个程序集,能够卸载 AppDomain 从而卸载蕴含在该 AppDomain 中的所有程序集
- Appdomain 能够独自爱护,AppDomain 在创立之后会利用一个权限集,权限集决定了向 AppDomain 中运行的程序集授予的最大权限。保障当宿主加载一些代码后,这些代码不会毁坏宿主自身应用的一些重要数据结构
- AppDomain 能够独自施行配置,AppDomain 在创立之后会关联一组配置设置,这些设置次要影响 CLR 在 AppDomain 中加载程序集的形式。如:搜寻门路、版本绑定重定向、卷影复制以及加载器优化
Windows 过程图
上图所示有两个 APPdomain,别离为 AppDomain#1(默认 AppDomain)和 AppDomain#2。其中 AppDomain#1 蕴含 3 个程序集:MyApp.exe,TypeLib.dll,System.dll。AppDomain#2 蕴含 2 个程序集:Wintellect.dll 和 System.dll。
System.dll 被加载到两个程序集中,假如两个 AppDomain 都应用了来自 System.dll 中的同一个类型,那么在两个 AppDomain 的 Loader 堆中都会为同一个类型调配一个类型对象,类型对象的内存不会由两个 AppDomain 共享。另外,一个 AppDomain 中的代码调用一个类型调用的办法时,办法的 IL 代码会进行 JIT 编译,生成的本地代码将与每一个 AppDomain 关联,办法的代码不禁调用它的所有 AppDomain 共享。
尽管不共享类型对象的内存或者本地代码是一种节约,然而 AppDomain 的全副目标是提供隔离性。CLR 要求在卸载某个 AppDomain 并开释它的所有资源的同时,不会对其它 AppDomain 产生负面影响。
有些程序集原本就会被多个 AppDomain 应用,如 MSCorLib.dll,该程序集蕴含了 System.Object,System.Int32 以及其它所有与.NET Framework 密不可分的类型。CLR 初始化时,该程序集会主动加载,而且所有 AppDomain 都共享该程序集中的类型。为了缩小资源耗费,该程序集通过“AppDomain 中立”的形式加载,CLR 会为它们保护一个非凡的 Loader 堆,该 Loader 堆中的所有类型对象以及为这些类型定义的办法 JIT 编译生成的所有本地代码,都会被过程中的所有 AppDomain 共享。
* 共享资源的代价:“AppDomain 中立”的形式加载的所有程序集永远不能被卸载,为了回收它们占用的资源惟一的办法便是终止 Windows 过程,让 Windows 回收资源。
跨 AppDomain 拜访对象
- 按援用封送
// 按援用封送
public class MarshalByRefType : MarshalByRefObject
{public MarshalByRefType()
{Console.WriteLine("{0} 在 {1} 中执行", this.GetType().ToString(), Thread.GetDomain().FriendlyName);
}
public void SomeMethod()
{Console.WriteLine("SomeMethod 在 {0} 中执行", Thread.GetDomain().FriendlyName);
}
public MarshalByValType MethodWithReturn()
{Console.WriteLine("MethodWithReturn 在 {0} 中执行", Thread.GetDomain().FriendlyName);
MarshalByValType t = new MarshalByValType();
return t;
}
public NonMarshalableType MethodArgAndReturn(string callDomainName)
{Console.WriteLine("AppDomain {0} 调用 AppDomain {1}", callDomainName, Thread.GetDomain().FriendlyName);
NonMarshalableType t = new NonMarshalableType();
return t;
}
}
- 按值封送
// 按值封送
[Serializable]
public class MarshalByValType : Object
{
private DateTime m_CreateTime = DateTime.Now;
public MarshalByValType()
{Console.WriteLine("{0} 在 {1} 中执行,创立于 {2}", this.GetType().ToString(), Thread.GetDomain().FriendlyName, this.m_CreateTime);
}
public override string ToString()
{return this.m_CreateTime.ToLongDateString();
}
}
- 齐全不能封送类型
// 该实例无奈跨 AppDomain 传送
public class NonMarshalableType : Object
{public NonMarshalableType()
{Console.WriteLine("创立 NonMarshalableType 在 {0} 中执行", Thread.GetDomain().FriendlyName);
}
}
调用代码
class Program
{static void Main(string[] args)
{
// 获取 AppDomain 援用
AppDomain appDomain = Thread.GetDomain();
// 获取 AppDomain 名称
string appDomainName = appDomain.FriendlyName;
Console.WriteLine("默认 AppDomain FriendlyName = {0}", appDomainName);
// 获取 AppDomain 中蕴含 Main 办法的程序集
string exeAssembly = Assembly.GetEntryAssembly().FullName;
Console.WriteLine("Main assembly = {0}", exeAssembly);
// 定义局部变量援用 AppDomain
AppDomain appDomain1 = null;
// 按援用封送
Console.WriteLine("{0} Demo 1", Environment.NewLine);
// 新建一个 AppDomain
appDomain1 = AppDomain.CreateDomain("MyAppDomain1", null, null);
MarshalByRefType mbrt = null;
// 将程序集加载到新的 AppDomain 中,结构对象把它封送到新建的 AppDomain(理论失去一个代理援用)
mbrt = (MarshalByRefType)appDomain1.CreateInstanceAndUnwrap(exeAssembly, "MyAppDomain.MarshalByRefType");
Console.WriteLine("Type = {0}", mbrt.GetType());
// 证实失去的是代理对象的援用
Console.WriteLine("是代理对象 = {0}", RemotingServices.IsTransparentProxy(mbrt));
// 看起来像是在 MarshalByRefType 上调用一个办法,其实是在代理类型上调用办法
// 代理使线程转至领有对象的那个 AppDomain,并在实在的对象上调用这个办法
mbrt.SomeMethod();
// 卸载创立的 AppDomain
AppDomain.Unload(appDomain1);
//mbrt 援用一个无效的代理对象,代理对象援用一个有效的 AppDomain
try
{
// 在代理对象上调用一个办法,AppDomain 有效抛出异样
mbrt.SomeMethod();
Console.WriteLine("调用胜利");
}
catch (AppDomainUnloadedException)
{Console.WriteLine("调用失败");
}
// 按值封送
Console.WriteLine("{0} Demo 2", Environment.NewLine);
// 新建一个 AppDomain
appDomain1 = AppDomain.CreateDomain("MyAppDomain2", null, null);
// 将程序集加载到新的 AppDomain 中,结构对象把它封送到新建的 AppDomain(理论失去一个代理援用)
mbrt = (MarshalByRefType)appDomain1.CreateInstanceAndUnwrap(exeAssembly, "MyAppDomain.MarshalByRefType");
// 对象的办法返回对象的一个正本,对象按值传送
MarshalByValType mbvt = mbrt.MethodWithReturn();
// 证实失去的不是代理对象的援用
Console.WriteLine("是代理对象 = {0}", RemotingServices.IsTransparentProxy(mbvt));
// 看起来像是在 MarshalByRefType 上调用一个办法,理论也是如此
Console.WriteLine(mbvt.ToString());
// 卸载创立的 AppDomain
AppDomain.Unload(appDomain1);
//mbvt 援用无效的对象,卸载 AppDomain 没有影响
try
{
// 不会抛出异样
Console.WriteLine(mbvt.ToString());
Console.WriteLine("调用胜利");
}
catch (AppDomainUnloadedException)
{Console.WriteLine("调用失败");
}
// 新建一个 AppDomain
appDomain1 = AppDomain.CreateDomain("MyAppDomain3", null, null);
// 将程序集加载到新的 AppDomain 中,结构对象把它封送到新建的 AppDomain(理论失去一个代理援用)
mbrt = (MarshalByRefType)appDomain1.CreateInstanceAndUnwrap(exeAssembly, "MyAppDomain.MarshalByRefType");
Console.WriteLine("{0} Demo 3", Environment.NewLine);
// 对象的办法返回一个不可封送的对象,抛出异样
try
{NonMarshalableType nmt = mbrt.MethodArgAndReturn(appDomainName);
}
catch (Exception e)
{Console.WriteLine(e.Message);
}
Console.ReadKey();}
}
运行后果
代码剖析:
首先取得一个 AppDomain 对象的援用,以后调用线程正在这个 AppDomain 中执行。因为多个 AppDomain 能够在一个 Windows 过程中,所以线程能执行一个 AppDomain 中的代码再执行另一个 AppDomain 中的代码。从 CLR 的角度看线程一次只能执 AppDomain 中的代码。
AppDomain 创立之后能够赋予它一个敌对名称用来标识 AppDomain,CLR 应用可执行文件的文件名来作为默认的 AppDomain 的敌对名称。
按援用封送
调用 CreateDomain 通知 CLR 在同一个过程中创立一个新的 AppDomain,新的 AppDomain 有本人的 Loader 堆(目前是空的),因而还没有程序集被加载到 Loader 中。创立 AppDomain 时 CLR 不在这个 AppDomain 中创立任何线程,AppDomain 中也没有代码运行,除非显示的让一个线程调用 AppDomain 中的代码。
为了在新的 AppDomain 中创立一个新类型的实例,首先必须将一个程序集加载到 AppDomain 中,而后构建该程序集中定义的一个类型的实例。CreateInstanceAndUnwrap 做的便是这个事件,该办法接管两个参数,第一个参数示意 AppDomain 要加载的程序集,第二个参数示意想要构建实例对象的类型名称。在外部该办法将导致调用线程从以后 AppDomain 转至新的 AppDomain,当初线程将指定的程序集加载到新的 AppDomain 中,并扫描程序集的类型定义元数据表,查找指定的类型(MyAppDomain.MarshalByRefType”),找到类型后创立该类型实例,线程返回默认的 AppDomain,使得 CreateInstanceAndUnwrap 能返回对新的 MarshalByRefType 对象的援用。
因为 CLR 并不容许一个 AppDomain 中的变量援用另一个 AppDomain 中创立的对象,因而在 CreateInstanceAndUnwrap 办法返回对象的援用之前还须要执行一些额定的逻辑。
援用返回之前的额定工作
MarshalByRefType 类型是从 System.MarshalByRefObject 派生的,这个类是一个十分非凡的基类,当 CreateInstanceAndUnwrap 发现自己封送的一个对象的类型派生自 MarshalByRefObject 时,CLR 就会跨 AppDomain 边界按援用封送对象。
源 AppDomain 想向指标 AppDomain 发送或返回一个对象援用时,CLR 会在指标 AppDomain 的 Loader 堆中定义一个代理类型,这个代理类型是用原始类型的元数据定义的。因而它看起来和原始类型齐全一样:有齐全一样的实例成员(属性、事件和办法)。然而实例字段不会成为代理类型的一部分。
在指标 AppDomain 中定义好代理类型后,CreateInstanceAndUnwrap 办法会创立这个代理类型的一个实例,初始化它的字段来标识 AppDomain 和实在对象,而后将这个代理对象的援用返回指标 AppDomain。调用 RemotingServices.IsTransparentProxy 证实返回的的确是一个代理对象。
接着援用程序应用代理来调用 SomeMethod 办法,因为 mbrt 援用一个代理对象,所以会调用由代理实现的 SomeMethod 办法。在代理的调用中,利用了代理对象中的信息字段,将调用线程从默认 AppDomain 切换至新的 AppDomain。当初该线程的任何行为都在新 AppDomain 的安全策略和配置下执行。而后线程应用代理对象的 GCHandle 字段查找新 AppDomain 中的真是对象,并用真是对象调用真是的 SomeMethod 办法。
* 一个 AppDomain 中的线程调用另一个 AppDomain 中的办法时,线程会在两个 AppDomain 中进行切换,这也意味着跨 AppDomain 边界的办法调用是同步的。任意时刻一个线程只能在一个 AppDomain 中
紧接着调用 Unload 办法强制 CLR 卸载指定的 AppDomain,并强制执行一次垃圾回收,开释由卸载的 AppDomain 中的代码创立的所有对象。此时默认的 AppDomain 还援用着一个无效的代理对象,然而代理对象不再援用一个无效的 AppDomain。此时再试图调用 SomeMothed 时,调用的是该办法在代理中的实现,代理发现实在对象的 AppDomain 曾经卸载,所以抛出异样。
按值封送
代码与按援用封送相似,不同的是 MarshalByValType 不是从 MarshalByRefObject 派生的,所以 CLR 不能定义一个代理类型,并创立一个代理类型的实例。对象不能按援用封送,然而因为 MarshalByValType 标记了[Serializable],所以 CreateInstanceAndUnwrap 可能按值封送对象。
源 AppDomain 想向指标 AppDomain 发送或返回一个对象援用时,CLR 将对象的实例字段序列化成一个字节数组。这个字节数组从源 AppDomain 复制到指标 AppDomain,而后 CLR 在指标 AppDomain 中反序列化字节数组,这个操作会强制 CLR 将定义了被反序列化的类型的程序集加载到指标 AppDomain 中。接着 CLR 创立类型的一个实例,并利用字节数组中的值初始化对象的字段,使之与源对象中的值雷同。而后 CreateInstanceAndUnwrap 返回对这个正本的援用。如此便实现了对象的跨 AppDomain 边界按值封送。
到此源 AppDomain 中的对象和指标 AppDomain 中的对象就有了独立生存期,它们的状态能够独立地更改。如果源 AppDomain 中没有根放弃源对象地存活,源对象的内存会在下一次垃圾回收时被回收。
接下来程序应用实在对象调用 ToString 办法,因为 mbrt 援用一个实在的对象,所以会调用这个办法的实在实现,线程不会在 AppDomain 之间切换。
为了进一步证实不是代理对象,当初将 AppDomain 卸载,持续调用 ToString 办法,调用依然胜利。
不可封送类型
因为 NonMarshalableType 类型既没有派生自 MarshalByRefObject 也没有 [Serializable] 标记,所以不能按援用封送也不能按值封送,对象齐全不能跨 AppDomain 边界进行封送。同时抛出一个 SerializationException 异样。
监督 AppDomain
能够将 AppDomain 的动态属性 MonitoringIsEnabled 设置为 true,从而监督 AppDomain 的资源耗费状况。
示例代码
class AppDomainMonitorDelta : IDisposable
{
private AppDomain m_AppDomain;
private TimeSpan m_ThisADCpu;
private long m_ThisADMemoryInUse;
private long m_ThisAdMemoryAllocated;
static AppDomainMonitorDelta()
{
// 关上 AppDomain 监督
AppDomain.MonitoringIsEnabled = true;
}
public AppDomainMonitorDelta(AppDomain appDomain)
{
this.m_AppDomain = appDomain ?? AppDomain.CurrentDomain;
this.m_ThisADCpu = this.m_AppDomain.MonitoringTotalProcessorTime;
this.m_ThisADMemoryInUse = this.m_AppDomain.MonitoringSurvivedMemorySize;
this.m_ThisAdMemoryAllocated = this.m_AppDomain.MonitoringTotalAllocatedMemorySize;
}
public void Dispose()
{GC.Collect();
Console.WriteLine("FriendlyName={0}, CPU={1}ms",
this.m_AppDomain.FriendlyName, (this.m_AppDomain.MonitoringTotalProcessorTime - this.m_ThisADCpu).TotalMilliseconds
);
Console.WriteLine("Allocated {0:N0} bytes of which {1:N0} survived GCs",
this.m_AppDomain.MonitoringTotalAllocatedMemorySize - this.m_ThisAdMemoryAllocated,
this.m_AppDomain.MonitoringSurvivedMemorySize - this.m_ThisADMemoryInUse
);
}
}
class Program
{static void Main(string[] args)
{using (new AppDomainMonitorDelta(null))
{
// 调配回收时会存活的约 10M 字节
var list = new List<Object>();
for (int i = 0; i < 1000; i++)
list.Add(new Byte[10000]);
// 调配回收时不会存活的约 20M 字节
for (int i = 0; i < 2000; i++)
new Byte[10000].GetType();
// 放弃 CPU 工作约 5 秒
long stop = Environment.TickCount + 5000;
while (Environment.TickCount < stop) ;
}
Console.ReadKey();}
}
输入后果
AppDomain 类的 4 个只读属性
- MonitoringSurvivedProcessMemorySize:Int64 属性,返回由以后 CLR 理论管制的所有 AppDomain 正在应用的字节数,只保障在上一次垃圾回收时是精确的
- MonitoringTotalAllocatedMemorySize:Int64 属性,返回一个特定的 AppDomain 已调配的字节数,只保障在上一次垃圾回收时是精确的
- MonitoringSurvivedMemorySize:Int64 属性,返回一个特定的 AppDomain 以后正在应用的字节数,只保障在上一次垃圾回收时是精确的
- MonitoringTotalProcessorTime:TimeSpan 属性,返回一个特定的 AppDomain 的 CPU 占用率
AppDomain 卸载
卸载 AppDomain 会导致 CLR 卸载 AppDomain 中的所有程序集,还会开释 AppDomain 的 Loader 堆。能够调用 AppDomain 的静态方法 Unload 卸载 AppDomain。
卸载 AppDomain 时 CLR 执行的一系列操作
- CLR 挂起过程中执行过托管代码的所有线程
- CLR 查看所有线程栈,查看有哪些线程正在执行要卸载的那个 AppDomain 中的代码,或者哪些线程会在某个时刻返回至要卸载的那个 AppDomain。在任何一个栈上,如果有筹备卸载的 AppDomain,CLR 都会强制对应的线程抛出一个 ThreadAbortException 异样并同时复原线程的执行。这将导致线程开展,在开展的过程中执行遇到的所有 finally 块中的代码,以进行资源清理。如果没有代码捕获 ThreadAbortException 异样,它会成为一个未解决的异样,并且 CLR 会吞噬该异样。线程会终止,但过程会持续运行(这一点十分非凡,因为对于其它所有未解决的异样 CLR 都会终止过程)
- 当上一步发现的所有线程都来到 AppDomain 后,CLR 遍历堆,为援用了“由已卸载的 AppDomain 创立的对象”的每一个代理对象都设置一个标记。这些代理对象当初晓得它们援用的实在对象曾经不存在了,如果任何代码试图调用一个有效的代理对象上的办法,该办法会抛出 AppDomainUnloadException
- CLR 强制垃圾回收,对现已卸载 AppDomain 创立的任何对象占用的内存进行回收。并调用这些对象的 Finalize 办法,彻底清理对象所占用的资源
- CLR 复原所有残余线程的执行,调用 AppDomain.Unload 办法的线程持续运行(AppDomain.Unload 的调用是同步进行的)
* 如果调用 AppDomain.Unload 办法的线程正好在要卸载的 AppDomain 中,CLR 会创立一个新的线程来尝试卸载 AppDomain。第一个线程被强制抛出 ThreadAbortException 并开展,新建的线程将期待 AppDomain 卸载,而后新线程终止。
FirstChance 异样告诉
给 AppDomain 的实例事件 FirstChanceException 增加委托能够在捕捉到异样的时候取得回调。
CLR 异样解决:异样首次抛出时,CLR 会调用已向抛出异样的那个 AppDomain 注销的 FirstChanceException 回调办法。而后 CLR 查找栈上在同一个 AppDomain 中的任何 catch 块,如果有一个 catch 块能解决异样,则异样解决实现,程序持续失常执行。如果 AppDomain 中没有一个 catch 块能解决异样,则 CLR 沿着栈向上调用 AppDomain,再次抛出同一个异样对象。CLR 会持续调用已向以后 AppDomain 注销的 FirstChanceException 回调办法,该过程会始终继续,晓得到达线程栈的顶部。如果异样还未被任何代码解决,CLR 将终止整个过程。
*FirstChanceException 只负责监督 AppDomain 抛出异样时获取一个告诉,回调办法并不能解决异样
可执行应用程序执行过程
Windows 通过一个托管 EXE 文件初始化一个过程时,会加载垫片。垫片会查看蕴含在 EXE 文件中的 CLR 头信息。头信息指明生成和测试应用程序时应用的 CLR 版本 (垫片依据这个信息决定哪个版本的 CLR 加载到过程中),CLR 加载并初始化好之后,它会检查程序集的 CLR 头,判断应用程序的入口是哪个(Main 办法),CLR 调用这个办法使应用程序真正启动并运行。
代码运行时会拜访其它类型,援用另一个程序集的类型时 CLR 会定位所需的程序集,并把它加载到同一个 AppDomain 中。当应用程序的 Main 办法返回后,Windows 过程终止并销毁默认的 AppDomain 和其它所有 AppDomain。
* 可调用 System.Environment.Exit 办法敞开 Windows 过程,该办法能保障所有对象的 Finalize 办法被执行
当托管代码呈现谬误时,CLR 能够做什么?
- 如果一个线程的执行工夫过长,CLR 能够终止线程并返回一个响应
- CLR 能够卸载 AppDomain,从而卸载有问题的代码
- CLR 能够被禁用,阻止更多的托管代码在程序中运行
- CLR 能够退出 Windows 过程(先终止所有线程后卸载所有 AppDomain)
宿主如何拿回它的线程?
宿主应用程序个别都要放弃对本人线程的管制,以数据库服务为例:新申请到达数据库,线程 A 取得该申请,后把该申请派发给线程 B 执行理论工作。假如线程 B 要执行的代码进入有限循环,这将导致数据库服务器派发的线程 B 一去不复返了,如此服务器是不是应该创立更多的线程,而这些线程自身也可能进入有限循环。
宿主可利用线程终止性能解决上述问题,线程终止工作形式如图:
- 1、客户端向服务器发送一个申请
- 2、服务器接到该申请并把它一个线程池来执行理论工作
- 3、线程池线程取得该申请,开始执行可信代码
- 4、可信代码进入 try 块,逾越 AppDomain 边界调用代码(蕴含不可信代码)
- 5、宿主在接到客户端的申请时会记录一个工夫,如果不可信代码在设定的工夫期限内没有做出响应,宿主就会调用 Thread 的 Abort 办法终止线程,强制抛出 ThreadAbortException
- 6、线程开始开展,调用 finally 块进行清理工作。最终线程池线程穿梭 AppDomain 返回。因为宿主代码是从一个 try 块中调用不可信代码的,所以宿主有一个 catch 块捕获 ThreadAbortException 异样
- 7、为了响应捕捉到的 ThreadAbortException 异样,宿主调用 Thread 的 ResetAbort 办法
- 8、因为宿主的代码曾经捕捉到了 ThreadAbortException 异样,因而宿主能够向客户端返回某种模式的谬误,容许线程池线程返回线程池,供将来的新申请应用
ThreadAbortException 是一个比拟非凡的异样,即便代码捕获了该异样,CLR 也不容许将该异样吞噬,即在 catch 块的尾部 CLR 会从新抛出该异样。同时反对调用 Thread 的 ResetAbort 办法通知 CLR 不须要在 catch 的尾部从新抛出 ThreadAbortException 异样。而调用 ResetAbort 办法时要求调用者被授予 SecurityPermission 权限并且 ControlThread 标记被设置为 true,然而宿主在为不可信代码创立 AppDomain 时不会向其授予该权限,这样便能保障不可信代码不可能自行处理并吞噬该异样,从而保障宿主能失常的捕捉到 ThreadAbortException 异样,从新获取该线程的控制权,并把它从新放回到线程池中。
* 当线程从它的 ThreadAbortException 开展时,不可信代码能够执行 catch 块的 finally 块,在这些块中代码可能进入有限循环从而组织宿主从新获取线程的控制权。这时候宿主应用程序能够用过卸载 AppDomain、禁用 CLR、终止过程的形式来修复这个问题。如果不可信代码捕获了 ThreadAbortException 异样并且从新抛出一个新的异样类型,如果这个新的异样被捕捉到 CLR 会在 catch 的尾部主动从新抛出 ThreadAbortException 异样。