.NET 的内存管理机制(四)

简介: 结合实体案例分析对象初始化过程
代码案例
public class UserInfo
{
private Int32 age = -1;
private char level = 'A';
}
public class User
{
private Int32 id;
private UserInfo user;
}
public class VIPUser : User
{
public bool isVip;
public bool IsVipUser()
{
return isVip;
}
public static void Main()
{
VIPUser aUser;
aUser = new VIPUser();
aUser.isVip = true;
Console.WriteLine(aUser.IsVipUser());
}
}
将上述代码的执行过程,反编译为 IL 语言可知:new 关键字被编译为 newobj 指令来完成对象创建工作,进而调用类型的构造器来完成其初始化操作,然后我们描述执行的具体过程:

首先,将声明一个引用类型变量 aUser:

VIPUser aUser;

它仅是一个引用(指针),保存在线程的堆栈上,占用 4Byte 的内存空间,将用于保存 VIPUser 对象的有效地址,其执行过程正是上文描述的在线程栈上的分配过程。此时 aUser 未指向任何有效的实例,因此被自行初始化为 null,试图对 aUser 的任何操作将抛出 NullReferenceException 异常。

接着,通过 new 操作执行对象创建:
aUser = new VIPUser();

如上文所言,该操作对应于执行 newobj 指令,其执行过程又可细分为以下几步:

1、CLR 按照其继承层次进行搜索,计算类型及其所有父类的字段,该搜索将一直递归到System.Object 类型,并返回字节总数,以本例而言类型 VIPUser 需要的字节总数为 15Byte,具体计算为:VIPUser 类型本身字段 isVip(bool 型)为 1Byte;父类 User 类型的字段id(Int32 型)为 4Byte,字段 user 保存了指向 UserInfo 型的引用,因此占 4Byte,而同时还要为 UserInfo 分配 6Byte 字节的内存。

实例对象所占的字节总数还要加上对象附加成员所需的字节总数,其中附加成员包括 TypeHandle 和 SyncBlockIndex,共计 8 字节(在 32 位 CPU 平台下)。因此,需要在托管堆上分配的字节总数为 23 字节,而堆上的内存块总是按照 4Byte 的倍数进行分配,因此本例中将分配 24字节的地址空间。

2、CLR 在当前 AppDomain 对应的托管堆上搜索,找到一个未使用的 20 字节的连续空间,并为其分配该内存地址。事实上,GC 使用了非常高效的算法来满足该请求,NextObjPtr 指针只需要向前推进 20 个字节,并清零原 NextObjPtr 指针和当前 NextObjPtr 指针之间的字节,然后返回原 NextObjPtr 指针地址即可,该地址正是新创建对象的托管堆地址,也就是 aUser 引用指向的实例地址。而此时的 NextObjPtr 仍指向下一个新建对象的位置。注意,栈的分配是向低地址扩展,而堆的分配是向高地址扩展。

注意:另外,实例字段的存储是有顺序的,由上到下依次排列,父类在前子类在后。在上述操作时,如果试图分配所需空间而发现内存不足时,GC 将启动垃圾收集操作来回收垃圾对象所占的内存。

最后,调用对象构造器,进行对象初始化操作,完成创建过程。该构造过程,又可细分为以下几个环节:

(a)构造 VIPUser 类型的 Type 对象,主要包括静态字段、方法表、实现的接口等,并将其分配在上文提到托管堆的 Loader Heap 上。

(b)初始化 aUser 的两个附加成员:TypeHandle 和 SyncBlockIndex。将 TypeHandle 指针指向 Loader Heap 上的 MethodTable,CLR 将根据 TypeHandle 来定位具体的 Type;将SyncBlockIndex 指针指向 Synchronization Block 的内存块,用于在多线程环境下对实例对象的同步操作。

(c)调用 VIPUser 的构造器,进行实例字段的初始化。实例初始化时,会首先向上递归执行父类初始化,直到完成 System.Object 类型的初始化,然后再返回执行子类的初始化,直到执行 VIPUser 类为止。以本例而言,初始化过程为首先执行 System.Object 类,再执行 User类,最后才是 VIPUser 类。最终,newobj 分配的托管堆的内存地址,被传递给 VIPUser 的 this 参数,并将其引用传给栈上声明的 aUser。
上述过程,基本完成了一个引用类型创建、内存分配和初始化的整个流程,然而该过程只能看作是一个简化的描述,实际的执行过程更加复杂,涉及到一系列细化的过程和操作。

对象创建并初始化之后,内存的布局,可以表示为:

20211220114019.png

总结

综上分析可知,在托管堆中增加新的实例对象,只是将 NextObjPtr 指针增加一定的数值,再次新增的对象将分配在当前 NextObjPtr 指向的内存空间,因此在托管堆栈中,连续分配的对象在内存中一定是连续的,这种分配机制非常高效。

相关文章
|
29天前
|
存储 监控 算法
Java中的内存管理:理解Garbage Collection机制
本文将深入探讨Java编程语言中的内存管理,着重介绍垃圾回收(Garbage Collection, GC)机制。通过阐述GC的工作原理、常见算法及其在Java中的应用,帮助读者提高程序的性能和稳定性。我们将从基本原理出发,逐步深入到调优实践,为开发者提供一套系统的理解和优化Java应用中内存管理的方法。
|
2月前
|
监控 算法 Java
Java中的内存管理:理解Garbage Collection机制
本文将深入探讨Java编程语言中的内存管理,特别是垃圾回收(Garbage Collection, GC)机制。我们将从基础概念开始,逐步解析垃圾回收的工作原理、不同类型的垃圾回收器以及它们在实际项目中的应用。通过实际案例,读者将能更好地理解Java应用的性能调优技巧及最佳实践。
86 0
|
3月前
|
存储 开发框架 .NET
"揭秘.NET内存奥秘:从CIL深处窥探值类型与引用类型的生死较量,一场关于速度与空间的激情大戏!"
【8月更文挑战第16天】在.NET框架中,通过CIL(公共中间语言)可以深入了解值类型与引用类型的内存分配机制。值类型如`int`和`double`直接在方法调用堆栈上分配,访问迅速,生命周期随栈帧销毁而结束。引用类型如`string`在托管堆上分配,堆栈上仅存储引用,CLR负责垃圾回收,确保高效且自动化的内存管理。
55 6
|
10天前
|
存储 算法 Java
Go语言的内存管理机制
【10月更文挑战第25天】Go语言的内存管理机制
17 2
|
12天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
23 1
|
29天前
|
数据库连接 开发者
.NET 内存管理两种有效的资源释放方式
【10月更文挑战第15天】在.NET中,有两种有效的资源释放方式:一是使用`using`语句,适用于实现`IDisposable`接口的对象,如文件流、数据库连接等,能确保资源及时释放,避免泄漏;二是手动调用`Dispose`方法并处理异常,提供更灵活的资源管理方式,适用于复杂场景。这两种方式都能有效管理资源,提高应用性能和稳定性。
|
30天前
|
算法 Java 数据库连接
.NET 内存管理两种有效的资源释放方式
【10月更文挑战第14天】在 .NET 中,`IDisposable` 接口提供了一种标准机制来释放非托管资源,如文件句柄、数据库连接等。此类资源需手动释放以避免泄漏。实现 `IDisposable` 的类可通过 `Dispose` 方法释放资源。使用 `using` 语句可确保资源自动释放。此外,.NET 的垃圾回收器会自动回收托管对象所占内存,提高程序效率。示例代码展示了如何使用 `MyFileHandler` 类处理文件操作并释放 `FileStream` 资源。
|
1月前
|
存储 安全 NoSQL
driftingblues9 - 溢出ASLR(内存地址随机化机制)
driftingblues9 - 溢出ASLR(内存地址随机化机制)
35 1
|
2月前
|
消息中间件
共享内存和信号量的配合机制
【9月更文挑战第16天】本文介绍了进程间通过共享内存通信的机制及其同步保护方法。共享内存可让多个进程像访问本地内存一样进行数据交换,但需解决并发读写问题,通常借助信号量实现同步。文章详细描述了共享内存的创建、映射、解除映射等操作,并展示了如何利用信号量保护共享数据,确保其正确访问。此外,还提供了具体代码示例与步骤说明。
|
1月前
|
程序员 编译器 数据处理
【C语言】深度解析:动态内存管理的机制与实践
【C语言】深度解析:动态内存管理的机制与实践