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
ExpandedBlockStart.gif {
    public class CustomType
ExpandedSubBlockStart.gif    {
    }

}

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

class MarshalByRefType : MarshalByRefObject
ExpandedBlockStart.gif     {
        public void ExecuteWithTypeLocked(Type type)
ExpandedSubBlockStart.gif        {
            lock (type)
ExpandedSubBlockStart.gif            {
                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
ExpandedBlockStart.gif     {
        static void Main(string[] args)
ExpandedSubBlockStart.gif        {
            try
ExpandedSubBlockStart.gif            {
                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)
ExpandedSubBlockStart.gif            {
                Console.WriteLine(ex.Message);
                Console.Read();
            }

        }


        static void Execute(object obj)
ExpandedSubBlockStart.gif        {
            try
ExpandedSubBlockStart.gif            {
                MarshalByRefType marshalByRefObj = obj as MarshalByRefType;
                marshalByRefObj.ExecuteWithTypeLocked(typeof(int));
            }

            catch (Exception ex)
ExpandedSubBlockStart.gif            {
                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)
ExpandedBlockStart.gif         {
            try
ExpandedSubBlockStart.gif            {
                MarshalByRefType marshalByRefObj = obj as MarshalByRefType;
                marshalByRefObj.ExecuteWithTypeLocked(typeof(CustomType));                
            }

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

        }

现在看看输出的结果:


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

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

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

看看现在的输出又如何:


现在的时间间隔又变成了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


作者:蒋金楠
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
9月前
|
Docker 容器
解决Native memory allocation (mmap) failed to map 2060255232 bytes for committing reserved memory.
解决Native memory allocation (mmap) failed to map 2060255232 bytes for committing reserved memory.
697 0
MGA (Managed Global Area) Reference Note (Doc ID 2638904.1)
MGA (Managed Global Area) Reference Note (Doc ID 2638904.1)
223 0
SAP WM Storage Type Capacity Check Method 5 (Usage check based on SUT)
SAP WM Storage Type Capacity Check Method 5 (Usage check based on SUT)
SAP WM Storage Type Capacity Check Method 5 (Usage check based on SUT)
why process type for MyOpportunity creation is empty
why process type for MyOpportunity creation is empty
79 0
why process type for MyOpportunity creation is empty
Identify the logic how BOL node name is categorized into different object type
Identify the logic how BOL node name is categorized into different object type
108 0
Identify the logic how BOL node name is categorized into different object type