What is "Type" in managed heap?

我们知道,在程序运行过程中,每个对象(object)都是对应了一块内存,这里的对象不仅仅指的是某个具体类型的实例(instance),也包括类型(type)本身。我想大家也很清楚CLR如何为我们创建一个类型的实例(instance)的:CLR计算即将被创建的Instance的size(所有的字段加上额外的成员所占的空间:TypeHandle和SyncBlockIndex);在当前AppDomain对应的managed heap中为之开辟一块连续的内存空间;初始化Instance的这两个额外的成员TypeHandle和SyncBlockIndex(TypeHandle是一个指针,指向Type的method table,SyncBlockIndex用于在多线程的条件下确保对该Instance操作的同步,它指向一块被称为Synchronization Block的内存块,我们对该Instance加锁, CLR会使instance的SyncBlockIndex指向某一个Synchronization Block,反之解锁会重置SyncBlockIndex);最后调用对应的constructor。

在面向对象的原则下,Instance的Field代表的是对象的状态(state), 而方法则体现的是对象的行为(behavior)。状态只能和具体的Instance绑定在一起,而属于同一类型的不同的Instance则具有一样的行为,所以行为是和Type绑定在一起的。同时Type定义了很多原数据的信息。这些基于Type的信息是如何保存的,今天我们就来简单地讨论这个问题。

一、 Sample

在开始介绍之前我们给出一个有趣例子。在讨论String interning的时候,我通过对具有相同字符序列的string进行加锁,证明了基于进程的string interning。今天我仍然沿用这种机制,不过进行加锁的对象不是string,而是Type对象。

上面是整个Solution的结构,为了把CustomType类型定义在一个和主程序不同的Assembly中,我添加了Artech.TypeInManagedHeap.ClassLibrary Project。CustomType是一个空的Class,没有定义任何的成员,因为我们需要的仅仅是CustomType这个Type本身:

using System;
using System.Collections.Generic;
using System.Text;

namespace Artech.TypeInManagedHeap.ClassLibrary
{
    public class CustomType
    {
    }
}

在Main所在的Artech.TypeInManagedHeap.ConsoleApp Project,我定义了一个MarshalByRefType,他继承自MarshalByRefObject,因为我需要让它在不同的AppDomain中以By Reference进行传递。唯一的方法ExecuteWithTypeLocked中,先对传入的Type对象加锁,获得锁后做一些输出,随后进行10s的时间延迟。

class MarshalByRefType : MarshalByRefObject
    {
        public void ExecuteWithTypeLocked(Type type)
        {
            lock (type)
            {
                Console.WriteLine("The operation with a Type locked is executed\n\tAppDomain:\t{0}\n\tTime:\t\t{1}\n\tType:{2}\n",
                   AppDomain.CurrentDomain.FriendlyName, DateTime.Now, type);
                Thread.Sleep(10000);
            }
        }
}
class Program
    {
        static void Main(string[] args)
        {
            try
            {
                AppDomain appDomain1 = AppDomain.CreateDomain("Artech.AppDomain1");
                AppDomain appDomain2 = AppDomain.CreateDomain("Artech.AppDomain2");

                MarshalByRefType marshalByRefObj1 = appDomain1.CreateInstanceAndUnwrap("Artech.TypeInManagedHeap.ConsoleApp", "Artech.TypeInManagedHeap.ConsoleApp.MarshalByRefType") as MarshalByRefType;
                MarshalByRefType marshalByRefObj2 = appDomain2.CreateInstanceAndUnwrap("Artech.TypeInManagedHeap.ConsoleApp", "Artech.TypeInManagedHeap.ConsoleApp.MarshalByRefType") as MarshalByRefType;

                Thread thread1 = new Thread(new ParameterizedThreadStart(Execute));
                Thread thread2 = new Thread(new ParameterizedThreadStart(Execute));

                thread1.Start(marshalByRefObj1);
                thread2.Start(marshalByRefObj2);

                Console.Read();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.Read();
            }
        }

        static void Execute(object obj)
        {
            try
            {
                MarshalByRefType marshalByRefObj = obj as MarshalByRefType;
                marshalByRefObj.ExecuteWithTypeLocked(typeof(int));
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.Read();
            }
        }
}  

在主程序中,我在两个新创建的AppDomain中创建了MarshalByRefType 实例,并在各自的线程中调用ExecuteWithTypeLocked方法。该程序的目的是证明在不同线程中被加锁的Type对象是否是同一个对象,如果是同一个对象,果是两个线程中的操作的执行间隔应该是10s,否则他们几乎在同一个时刻执行。

首先进行加锁的对象是System.Int32 Type(typeof(int)))我们来运行程序,看看输出结果:

输出的两个时间刚好相差10s,这充分说明了在不同的AppDomain中进行加锁的System.Int32 Type是同一个对象。

我们现在来对我们自定义的CustomType Type进行加锁,我只需修改Execute方法:

static void Execute(object obj)
        {
            try
            {
                MarshalByRefType marshalByRefObj = obj as MarshalByRefType;
                marshalByRefObj.ExecuteWithTypeLocked(typeof(CustomType));                
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.Read();
            }
        }

现在看看输出的结果:

两个操作输出了相同的时间,这说明了在两个不同AppDomain中进行加锁的CustomType Type对象并非同一个对象。

我们进一步作一些修改,在Main方法上运用LoaderOptimizationAttribute

   [LoaderOptimizationAttribute(LoaderOptimization.MultiDomain)]
        static void Main(string[] args)
        {
            … …
        }

看看现在的输出又如何:

现在的时间间隔又变成了10s,也就是说,在这种情况下,CustomType Type虽然使用在不同的AppDomain中,但是它们实际是同一个对象。

二、Managed code的执行

我先不对上面出现的现象做出解释,我首先对在CLR下托管代码的执行过程做一个简单的介绍。我们就以上面的Sample为例,对于下面的4行code, 如果MarshalByRefType实现在另外一个Assembly中(假设叫做CustomAssembly.dll), 我们来看看CLR到底会为为我们做些什么。

AppDomain appDomain1 = AppDomain.CreateDomain("Artech.AppDomain1");AppDomain appDomain2 = AppDomain.CreateDomain("Artech.AppDomain2");MarshalByRefType marshalByRefObj1 = appDomain1.CreateInstanceAndUnwrap("Artech.TypeInManagedHeap.ConsoleApp", "Artech.TypeInManagedHeap.ConsoleApp.MarshalByRefType") as MarshalByRefType;MarshalByRefType marshalByRefObj2 = appDomain2.CreateInstanceAndUnwrap("Artech.TypeInManagedHeap.ConsoleApp", "Artech.TypeInManagedHeap.ConsoleApp.MarshalByRefType") as MarshalByRefType;

CLR先创建了两个AppDomain:Artech.AppDomain1和Artech.AppDomain2。接着调用CreateInstanceAndUnwrap方法。发现一个MarshalByRefType类型,并且该类型并没有在已经加载的Assembly中定义,于是CLR会在一些特殊的目录或者GAC中试着找到定义了MarshalByRefType的CustomAssembly.dll,找到后加载该Assembly。注意该Assembly的加载是基于AppDomain的,也就是说,两个CustomAssembly.dll被分别加载到Artech.AppDomain1和Artech.AppDomain2中

每个AppDomain都具有一段限于自己使用的、被隔离的托管堆。托管堆中又具有很多不同的划分,分别基于不同的目的用于存储不同的信息。其中最重要的是GC heap和Loader heap。GC heap用于Reference type实例的存储,每个实例的生命周期受GC的管理。GC以某种机制进行垃圾收集回收垃圾对象的内存。Loader heap在存储原数据相关的信息,也就是我们说的Type。每个Type在Loader heap中体现为一个MethodTable,MethodTable主要记录了这些metadata的信息,比如Type的base type,实现的interface, Type被定义的module, static field,以及所有的方法,而System.Type则可以看成是对MethodTable的封装,在程序执行过过程中一个Type对象对应着一个具体的MethodTable。

当基于Type的Meta data被成功加载在各自AppDomain的Loader heap中之后,CLR便按照开篇介绍的Instance创建的过程创建对象,Instance对应的managed heap就是GC heap。在初始化Instance额外成员TypeHandle过程中,就是把它指向在Loader heap 的MethodTable。通过TypeHandle这个指针就可以定位到具体的Type。

如果我们执行了Intance的某个方法,那么CLR根据TypeHandle找到对应的MethodTable,随后定位到具体的方法,通过JIT Compiler把IL指令变成基于处理器的machine instruction,并执行之,该machine instruction被保存,用于下一次执行。

通过上面的介绍,我们说Assembly的加载和Type在Loader heap的加载都是基于某个单独AppDomain,是被AppDomain隔离起来,不能被其他的AppDomain共享。这样充分的利用AppDomain提供的内存隔离机制,保证了托管程序的健壮性。但是,从另一方面讲,由于在不同的AppDomain使用的Type,都会在该AppDomain中加载对应的Assembly,并在AppDomain所在的Loader heap加载基于Type的metadata,这样对于一些不是很Common的Type来说没有问题,但是对一些我们经常使用的基本类型,比如int,Array,object等,则会带来Performance的损耗和内存的压力。于是出现了另一种加载机制:以中立域的方式加载

在《再说string》中,我提到过,在CLR初始化过程中,会创建3个Domain:SystemDomainSharedDomainDefaultDomain。SharedDomain中加载的是一些AppDomain中性,能被各不同的AppDomain共享的信息。定义了一些基本类型,比如int,object,Array等的Assembly -MSCorLib.dll就是被加载到SharedDomain中供同一个进程的各个AppDomain共享。同理存储于SharedDomain的Loader heap的Type也是被各个AppDomain共享的。

MSCorLib.dll在初始化的时候被自动地加载到SharedDomain,而对于一般的Assembly,CLR为我们提供了一些方式实现这样的加载方式,比如在Main方法上运用LoaderOptimizationAttribute

三、回到Sample

有了上面的理论基础,我们再一次回到我们第一部分的Sample,对于运行的现象就很容易理解了:

首先对System.Int32 Type进行加锁,由于System.Int32 是定义在MSCorLib.dll中,并且该Assembly一中立的方式加载到SharedDomain中,同一个进程中的各个AddDomain用到的System.Int32 Type实际上是同一个对象。

后来我们又对我们自定义的CustomType进行加锁,这个Type对应的Assembly为Artech.TypeInManagedHeap.ClassLibrary.dll, 在某人的情况下两个Assembly被加载到我们新创建的两个AppDomain中,所以在AppDomain1和AppDomain2的CustomType Type是不同的对象。

最后我们在Main上运用了[LoaderOptimizationAttribute(LoaderOptimization.MultiDomain)],实际上就是指示CLR以中立的方式把Artech.TypeInManagedHeap.ClassLibrary.dll加载到SharedDomain中,所以这时候爱两个AppDomain中的CustomType Type是相同的对象。

四、 一点补充

由于Type对象是基于Loader heap的,而非GC heap,所以Type的生命周期会保持到AppDomain被卸载,对于以中立的方式加载到SharedDomain的情况,Type对象的生命周期会延续要进程的结束。

另外,切忌对Type进行加锁,如果你对Type加锁,你实际上相当于对所有的该Type对应的instance加锁,粒度太大,极易形成死锁。

关于CLR如何创建对象,请参考:

Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects》   By Hanu Kommalapati and Tom Christian

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术博客

C#简单的面试题目(三)

31.C#提供一个默认的无参构造函数,当我实现了另外一个有一个参数的构造函数时,还想保留这个无参数的构 造函数。这样我应该写几个构造函数?     两个,一...

31910
来自专栏xx_Cc的学习总结专栏

iOS底层原理总结 - 探寻block的本质(一)

28640
来自专栏犀利豆的技术空间

Redis 的基础数据结构(一) 可变字符串、链表、字典

这周开始学习 Redis,看看Redis是怎么实现的。所以会写一系列关于 Redis的文章。这篇文章关于 Redis 的基础数据。阅读这篇文章你可以了解:

10330
来自专栏大内老A

ASP.NET MVC中的ActionFilter是如何执行的?

在ASP.NET MVC中的四大筛选器(Filter),ActionFilter直接应用在某个Action方法上,它在目标Action方法执行前后对调用进行拦截...

25670
来自专栏蘑菇先生的技术笔记

探索C#之6.0语法糖剖析

26360
来自专栏xiaoxi666的专栏

【模板小程序】十进制大数相加(正整数版本+整数版本【正负0】),包含合法性检查

为适应于不同用途,将大数算法写成了两个版本,分别为只处理正整数的版本和包含负数处理的版本,可根据需要选用。

12230
来自专栏小樱的经验随笔

Codeforces 719B Anatoly and Cockroaches

B. Anatoly and Cockroaches time limit per test:1 second memory limit per test:25...

30060
来自专栏Java进阶之路

关于java子父类关系的小坑

27010
来自专栏恰童鞋骚年

C#委托与事件学习笔记

      今天跟随视频学习了一下C#中最重要的一些概念之委托与事件。老杨的视频讲的还是挺深入浅出,不过刚接触C#.NET的人还是朦朦胧胧,就像张子阳先生说的“...

10730
来自专栏NetCore

.NET反射、委托技术与设计模式

1 反射技术与设计模式   反射(Reflection)是。NET中的重要机制,通过放射,可以在运行时获得。NET中每一个类型(包括类、结构、委托、接口和枚举...

24490

扫码关注云+社区

领取腾讯云代金券