C#中new一个对象时,发生了什么事?

本文详细解析了CLR在实例化引用类型与值类型时的具体流程,包括内存分配、内存初始化等关键步骤,并讨论了内存对齐的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题看似简单,不过事实上,CLR做的比这要多。。。
要准确回答这个问题,还要分情况来说。

new一个引用类型
首先,要实例化一个引用类型,就一定需要在堆上分配内存。要分配内存,就需要先计算出这个引用类型占多大空间,需要给它分配多少内存。
那怎么计算呢?简单!只要计算该类型所有字段的长度总和就可以啦。我们知道,引用类型的字段,占一个指针的长度(32位机器上是4个字节,64位机器上是8个字节)。值类型的字段长度可以通过递归的方法计算得出(递归终点是遇到引用类型或基本类型)。根据这些信息,我们就可以轻松计算出所有字段长度的总和了。
但是实际上计算方法会比这个复杂一点点,因为还要考虑到内存对齐的情况,关于内存对齐的解释我附在了本文的最后,这里就不多说了。考虑了内存对齐之后,得到的结果可能会比之前的要稍大一些。
不过这个仍然不是最终的结果。要得到最终的结果,还需要加上两个指针的长度。原因是,每个分配在堆上的对象都会有两个指针的“额外开销”,这两个开销分别是同步块索引和类型指针。关于同步块索引,一两句话也说不清楚,不过可以把它简单地理解成一个指向“同步块”的指针,而这个“同步块”的作用则是为了让拥有该同步块的对象能够支持线程同步。所谓类型指针,你可以这样来理解:每个对象都是一个类型的实例,而每个类型本身都有一个Type类型的实例来表示,对象的类型指针就是指向该类型的Type实例的指针。举个例子就清楚多了,我们知道,typeof(String)的值是一个Type类型的实例,这个Type类型的实例也就是所有的String对象的类型指针所指向的东西。
好了,到此为止,就可以得出实例化一个引用类型需要为其分配的内存数了。不过,要注意的是,CLR并不是在运行时计算分配内存的大小的,而是早在编译的时候就已经计算好这个量了。
接下来要做的是初始化分配得到的内存块。这个很简单,只要把这段内存的所有二进制位都设为0就可以了。
然后就是初始化两个“额外开销”的值了。对于同步块索引,CLR把它初始化为一个负数,并不指向任何的同步块。这是因为对于绝大多数对象,我们不要求它支持线程同步,所以不必急着给他实例化一个同步块,等到真的需要的时候再实际进行分配。而对于类型指针,则将其指向一个实实在在的对象——即该类型的类型对象实例。
再然后,就是调用类型的构造函数了。
完成了上述步骤,一个引用类型的对象实例就做好了,new操作符只要返回这个实例的引用就算完成任务了。

new一个值类型
首先,也是要计算需要分配多少内存。因为值类型是没有所谓的“额外开销”的,所以值类型所需的内存长度就是其内部字段的大小总和(同样需要考虑内存对齐)。同样的,CLR在编译的时候就已经计算好这个量了,不需要在运行时计算。
然后,CLR分配所需的内存。在哪里分配呢?这可说不准,在堆上或在栈上都有可能。
再然后就是调用类型构造函数了。这里需要注意,CLR并没有初始化这段内存块,而是把初始化内存块的任务都交给构造函数了。这样做是为了保证值类型轻量性的特点。这也是为什么C#语言在值类型的构造函数中强制要求为所以字段赋值的原因。另外,所有值类型的默认构造函数都会把内部字段都初始化为0。
到此,一个值类型也做好了。一般来说,对于值类型,new操作符并不需要返回其地址。原因在于,值类型的位置相对固定,因此在编译时就可以基本确定它们的位置。比如说,函数栈上的值类型实例都有一个相对于栈的偏移量,这个偏移量在编译时就是确定的。再比如说,作为引用类型的字段的值类型,都有一个相对于该引用类型地址的偏移量,这个偏移量也是早在编译时就固定下来的。所以,new操作符无需返回值类型实例的地址。

现在我们知道每new一个对象时CLR所需要做的工作了。可以看出,CLR的任务并不轻松。若是考虑到new一个对象之后还要垃圾回收该对象,那CLR就更辛苦了。所以,每当我们想要实例化一个类型的时候,都需要三思而后行。。。

 

附:关于内存对齐(这个是我之前学习的笔记,记得不是很系统,有兴趣的同学凑合看一下吧。。。)

为什么要内存对齐?

为了提高程序的性能,内存中的数据结构应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问。(对字,双字,和四字来说,自然边界分别是偶数地址,可以被4整除的地址,和可以被8整除的地址。)

怎样才算内存对其?

一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。

C#中,你可以使用`Microsoft.Build`命名空间下的类来编译另一个.NET工程项目。以下是一个简单的示例代码,展示了如何使用C#代码来编译另一个.NET项目: ```csharp using System; using Microsoft.Build.Locator; using Microsoft.Build.Execution; using Microsoft.Build.Framework; class Program { static void Main(string[] args) { // 注册MSBuild实例 MSBuildLocator.RegisterDefaults(); // 创建项目实例 ProjectInstance project = new ProjectInstance(@"C:\Path\To\Your\Project.csproj"); // 创建项目字典 var globalProperties = new Dictionary<string, string> { { "Configuration", "Release" } }; // 创建项目集合 ProjectCollection projectCollection = new ProjectCollection(globalProperties); // 创建构建请求 BuildRequestData buildRequest = new BuildRequestData(project, new[] { "Build" }, projectCollection.HostServices); // 创建构建管理器 BuildManager buildManager = BuildManager.DefaultBuildManager; // 创建构建参数 BuildParameters buildParameters = new BuildParameters(projectCollection) { Loggers = new[] { new ConsoleLogger(LoggerVerbosity.Normal) } }; // 执行构建 BuildResult buildResult = buildManager.Build(buildParameters, buildRequest); // 检查构建结果 if (buildResult.OverallResult == BuildResultCode.Success) { Console.WriteLine("构建成功!"); } else { Console.WriteLine("构建失败!"); } } } ``` 这个示例代码做了以下几件事: 1. 注册MSBuild实例。 2. 创建一个`ProjectInstance`对象指向你要编译的项目文件(.csproj)。 3. 设置全局属性,例如配置(Debug/Release)。 4. 创建一个`ProjectCollection`对象来管理项目。 5. 创建一个`BuildRequestData`对象来描述构建请求。 6. 创建一个`BuildManager`对象来管理构建过程。 7. 设置构建参数,例如日志记录器。 8. 执行构建并检查结果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值