托管对象本质-第一部分-布局



托管对象本质-第一部分-布局

原文地址:https://devblogs.microsoft.com/premier-developer/managed-object-internals-part-1-layout/
原文作者:Sergey
译文作者:杰哥很忙

目录

托管对象本质1-布局
托管对象本质2-对象头布局和锁成本
托管对象本质3-托管数组结构
托管对象本质4-字段布局

托管对象的布局非常简单:托管对象包含实例数据、指向元数据的指针(也称为方法表指针)和内部信息包(也称为对象头)。

译者补充: 方法表指针在某些文章也被称之为类型句柄,英文是TypeHandle。

当我第一次看到对象的布局时,我产生了一些疑问:

  1. 为什么对象的布局如此怪异?
  2. 为什么托管引用指向对象的中间,而对象头的偏移量为负?
  3. 对象头中存储了哪些信息?

译者补充:作者对于布局怪异实际指的就是对象头的偏移量为负数。由于托管对象以引用地址的偏移量记为0,对象头大小为4或8字节(取决于是32位还是64位,实际64位对象头也仅使用4字节,前面4个字节填充0)。因此由于对象头在对象指针之前,因此它的偏移量位-4或-8。

当我开始思考布局并做了一个快速研究时,我只有几个选择:

  1. JVM 从一开始就对托管对象使用了类似的布局。
    今天听起来有点疯狂,但请记住,由于Java早就有了一个特性(又名数组协方差),C#在借鉴Java语言时也引用了这一有史以来最糟糕的特性。与这个决定相比,重用一些关于对象结构的想法听起来并不合理。

    译者补充:数组协方差可能存在无法保证类型安全,从而产生一个运行时异常。详情可以看与C#数组的协方差和逆差

  2. 对象头的大小可以增大,而在 CLR 中没有横切更改。
    对象头包含 CLR 使用的一些辅助信息,CLR 可能需要比指针大小字段更多的信息。事实上,移动电话中使用的 .Net Compact Framework 对于大小对象具有不同的头(有关详细信息,请参阅 WP7:CLR 托管对象开销)。桌面 CLR 从未使用过此功能,但这并不意味着将来不可能实现此功能。

    译者补充:这里说的CLR没有根据切面大小改变对象头,指的是桌面CLR,因为移动设备的CLR会根据对象大小改变对象头的布局。

  3. 缓存行和其他性能相关特征。

Chris Brumme – CLR 架构师之一,在他的发表的Value Types的评论中提到,缓存友好性正是托管对象布局的原因。从理论上讲,由于缓存行大小(64 字节),访问彼此较近的字段的效率可能更高。这意味着根据字段在对象中的位置不同,访问间接引用字段会有不同的性能差异。我花了一些时间试图证明对于现代处理器该理论依据仍然是成立的,但无法获得任何能够显示存在的差异基准测试数据。

花了一些时间试图验证我的理论后,我联系了Vance Morrison问这个问题,并得到了以下的答案:目前的设计没有特别考虑。

因此,对于"为什么托管对象的布局如此怪异?"的一个简单回答是由于历史原因造成的。老实说,我可以看到在负索引移动对象头的逻辑,以强调此数据块是 CLR 的实现细节,它的大小可以随时间而变化,并且不应由用户检查。

译者补充:原作者说的是由于历史原因造成的也没有毛病,因为非托管代码就包含了对象头和对象引用地址,因此托管代码延续了这一风格。

现在是时候审视布局的更多细节了。再次之前,我们思考一下,CLR可以与托管对象实例关联哪些额外信息?以下是一些想法:

  • GC可以用来标记可从应用程序根访问对象的特殊标志。
  • 一种特殊的标志用于通知GC某个对象已固定,在垃圾收集期间不应移动。
  • 托管对象的哈希代码(当未重写GetHashCode方法时)。
  • 锁语句使用的关键节和其他信息:获取锁的线程等。

除了实例状态之外,CLR还存储了许多与类型相关的信息,如方法表、接口映射、实例大小等等,但这与我们当前的讨论无关。

IsMarked 标记

托管对象头可用于多种不同的用途。你可能认为垃圾收集器(GC)使用对象头中的一个位来标记该对象是由根引用的,并且应该保持活动状态。这是一种常见的误解,只有少量的名著提及。

比如Jeffrey Richter写的《CLR via C#》, 《Pro .NET Performance》作者是Sasha Goldstein,当然还有一些其他人.

CLR 作者决定不使用对象头,而是使用一个巧妙的技巧:方法表指针的最低位用于存储在垃圾回收期间存储对象可访问且不应被回收的标志。

下面是来自Coreclr的一个mark标记的实现,在文件gc.cpp的8974行:

#define marked(i) header(i) -> IsMmarked();
#define set_marked(i) header(i)->SetMarked()
#define clear_marked(i) header(i)->ClearMarked()
 
// class CObjectHeader
BOOL IsMarked() const
{
    return !!(((size_t)RawGetMethodTable()) & GC_MARKED);
}
void ClearMarked()
{
    RawSetMethodTable(GetMethodTable());
}
void SetMarked()
{
    RawSetMethodTable((MethodTable*)(((size_t)RawGetMethodTable()) | GC_MARKED));
}
MethodTable* GetMethodTable() const
{
    return((MethodTable*)(((size_t)RawGetMethodTable()) & (~(GC_MARKED))));
}

20200123114440.png

由于gc.cpp文件太大导致GitHub不分析它。 这意味着我不能将超链接添加到特定代码行。

CLR 堆中的托管指针以4个或8个字节地址长度进行对齐,取决于32位还是64位平台。这意味着每个指针的 2 或 3 位始终为 0,可用于其他目的。JVM 也使用同样的技巧,称为"压缩 Oops",该功能允许 JVM 具有 32 GB 堆大小,并且仍使用 4 个字节作为托管指针。

译者补充:当我们在对象上标注StructLayout以控制对象的分布甚至偏移值。若对象没有填满4字节或8字节时,CLR会进行自动填充。
“这意味着每个指针的 2 或 3 位始终为 0,可用于其他目的。”对于这句话的解释,个人理解如下:由于64位指针最多支持264内存,即16TiB的内存大小,而对于windows系统则有软件上的内存大小限制,windows7旗舰版支持192GB的内存,而windows server 2008 R2支持2TiB内存大小,Windows Server 2012提高到4TiB的最大内存限制。因此可以如作者所说,windows 64位操作系统预留了2到3位指针用于其他目的,因此最大内存支持4TiB。

从技术上讲,即使在 32 位平台上,也有 2 位可用于标志。基于 object.h 文件的注释,我们可以认为确实如此,并且方法表指针的第二个最低位用于固定(以标记在垃圾回收的压缩阶段不应移动对象)。不幸的是,并不能判断该说法是否正确,因为来自 gc.cpp(行 3850-3859)的 SetPinned/IsPinned 方法基于对象头中的保留位实现,并且我无法在 coreclr 代码版本库中找到实际设置方法表指针位的任何代码。

下次我们会讨论所得实现以及锁的性能消耗大小。

相关文献

  1. 数组协方差
  2. CPU高速缓存行对齐(cache line)
  3. 类型实例的创建位置、托管对象在托管堆上的结构
  4. .net托管环境下struct实例字段的内存布局(Layout)和大小(Size)
  5. 托管堆上对象的大小(Size)和Layout
  6. .NET对象的内存布局
  7. .NET Framework Internals: How the CLR Creates Runtime Objects
  8. What limits Windows 7 x64 machines to <=192GB RAM?

20191127212134.png
微信扫一扫二维码关注订阅号杰哥技术分享
出处:https://www.cnblogs.com/Jack-Blog/p/12230616.html
作者:杰哥很忙
本文使用「CC BY 4.0」创作共享协议。欢迎转载,请在明显位置给出出处及链接。

posted @ 2020-01-23 14:52  杰哥很忙  阅读(764)  评论(0编辑  收藏  举报