你的C#代码是怎么跑起来的(二)

接上篇:你的C#代码是怎么跑起来的(一)

通过上篇文章知道了EXE文件的结构,现在来看看双击后是怎样运行的:

双击文件后OS Loader加载PE文件并解析,在PE Optional Header里找到基地址和RVA,通过这两个确定了程序的入口地址,这个地址指向MsCorEE.dll的_CorExeMain(),执行它。_CorExeMain()开始执行,选择加载合适版本的CLR,CLR开始运行,CLR运行时会分配一个连续的地址空间用作托管堆,并用一个指针NextObjPtr指到开始位置,下次分配内存时就从指针指的位置开始。

CLR运行后从CLR头里找到应用程序入口标识,也就是Main()方法的MethodDefToken,通过这个标识在元数据表MethodDef里找到Main方法的偏移位置,这样就可以找到Main()的IL代码。

CLR检查Main方法里面是否有没加载的类型,没有的话就加载进来并在托管堆上建一个类型对象,类型对象包含静态字段,方法,基类的引用。然后给类型的方法表里每个方法一个存根,存根是用于标识是否被JIT编译过。

JIT: just-in-time Compiler,即时编译器。

JIT编译之前CLR会对Main方法的代码进行验证,确保类型安全且元数据正确,一切没问题后先检查类型方法表里这个方法的存根,不为空的话表示已经编译过就不需要再次编译,没有的话JIT把这段IL代码编译成本地代码保存到内存中并方法表的存根做上标记,然后JIT返回编译前的位置并把原来CLR指向JIT的地址修改为指向本地代码的地址,这样函数的本地代码开始执行。程序执行到哪里就编译到哪里,没有执行到的就不会加载和编译,同样的代码再次执行的话就直接在内存里拿了,这也是为什么第一次运行C#时比较慢而后面就快的原因。这样就开始陆续执行所有的代码,程序也就跑起来了。

在内存上,运行线程会把函数的参数和局部变量压入线程栈上,栈上的空间默认是1M,方法的参数和局部变量都会压到函数的栈帧上,方法里的对象在托管堆NextObjPtr指向的位置分配内存并把内存地址存到栈上的局部变量里。CLR会给托管堆上的每个对象包括对象类型都添加两个字段,一个对象类型指针,一个同步块索引。

说起栈帧,大家在调试代码时应该都喜欢用CallStack吧,这可以通过看调用栈很方便来定位出问题的具体原因,这个CallStack也就是方法的栈帧的具体显示,一级一级的。

对象类型指针从字面上就很容易知道跟类型有关。CLR刚开始运行时就分配了一个Type的对象类型,他的对象类型指针指向自己,后面创建的对象类型的对象类型指针指针就指向这个Type,而new出来的对象的对象类型指针就指向它的类型,这样所有对象都能找到自己的类型使CLR在运行时能确保类型安全。

同步块索引的格式是前6个标志位加后面26位内容(32位系统),作用则有好几个。

1. 调用对象的gethashcode()后标志位改变一位,后26位会存储对象的hashcode,保证对象生命周期内hashcode的唯一;

2. lock时用到,CLR会维护一个同步块数组,每项由一个指向同步块的指针和对象指针组成,lock时同样改变标识位,然后去同步块数组找一个闲置项,后26则变成这项在数组中的索引,有人要问了,刚才hashcode不是用了这26位吗,现在变了,hashcode岂不是丢了。确实,hashcode在lock之后不能直接存到索引了,不过同步块中专门准备了一个字段用来存hashcode,所以可以转移到同步块中,这样设计是为了节省内存,因为大部分情况下是不用lock的,也就不需要增加多余的同步块。

另外为什么是索引而不是地址呢,因为同步块数组的大小不是固定的,随着对象的增多而变大,在内存上的位置可能会发生变化,所以用索引就不用管数组在哪个位置了。

当线程进入lock后检查同步块的m_motion,发现没有标识则进入lock区域并把标识改变,如果已经有同一个线程进去则把计数器加1,如果已经有其他线程则等待。

3. 垃圾回收时的标识,GC触发时首先认为所有的对象都是垃圾,由局部变量,寄存器,静态变量这些根向上找,凡是包含的对象都认为还有引用,在同步块索引上修改一位标识,当所有对象都遍历过后没有标识的对象就会被清掉,然后再是整理内存、修改引用地址等。

看个简单的例子,只用于演示,不考虑合理性:

 1 using System;
 2 
 3 namespace Test
 4 {
 5     class Program
 6     {
 7         static void Main(string[] args)
 8         {
 9             int height = 170;
10             int weight = 60;
11             People.Find();
12             People developer = new Developer()(height, weight);
13             bool isHealthyWeight = developer.IsHealthyWeight();
14             bool isRich = developer.IsRich();
15         }
16     }
17 
18     class People
19     {
20         int _height;
21         int _weight;
22 
23         public People(int height, int weight)
24         {
25             _height = height;
26             _weight = weight;
27         }
28 
29         public virtual bool IsRich();
30 
31         public bool IsHealthyWeight()
32         {
33             var healthyWeight = (Height - 80) * 0.7;
34             return Weight <= healthyWeight * 1.1 && Weight >= healthyWeight * 0.9;
35         }
36 
37         public static string Find(string id) { return ""; }
38     }
39 
40     class Developer : People
41     {
42         public Developer(int height, int weight) : base(height, weight)
43         { }
44 
45         public override bool IsRich()
46         {
47             return false;
48         }
49     }
50     
51 }

*图片不清楚可以放大看

首先判断类型是否都加载,用到了int,bool,string,这些是在mscorlib.dll程序集的system命名空间下,所以先加载mscorlib.dll程序集,再把int,bool,string加到类型对象里。另外还有我们自己定义的Developer和People,也把类型对象创建好,另外也别忘了基类object,也要加载进来。(实际上还有double啊,这里就没画了)另外继承类的类型对象里面都有个字段指向基类,所以才能往上执行到基类方法表里的方法。

局部变量都在线程栈上,Find()方法是静态方法,直接去People类型对象的方法表里去找,找到后看是否有存根标识,没有的话做JIT编译,有的话直接运行。

developer的实例化虽然是用People定义的,但实例还是Developer,所以developer的类型对象指针指向Developer,对象里除了类型对象指针还有实例字段,包括基类的。内存分配在托管堆上,并把地址给到线程栈上的变量中。

虚函数也一样,在运行时已经确定是Developer,所以会调用Developer方法表里的IsRich方法,一样先JIT,再运行。

以上就是一个简单的C#程序的运行过程和在内存上的表现,本篇主要内容来自CLR via C#这本书,小弟算是总结一下,谢谢观看。

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏决胜机器学习

《Redis设计与实现》读书笔记(十) ——Redis对象相关其他设计与实现

《Redis设计与实现》读书笔记(十) ——Redis对象相关其他设计与实现 (原创内容,转载请注明来源,谢谢) 一、类型检查与命令多态 redis对键操作的命...

2776
来自专栏深度学习之tensorflow实战篇

python 序列化数据:pickle与json ,dumps与loads

python 序列化数据:pickle与json pickle 只能在python中用python文件间序列化,实现了两个python 内存数据的交互(可序列...

3266
来自专栏python3

python Json与pickle数据序列化

在程序运行的过程中,所有的变量都是在内存中。一旦程序结束,变量所占用的内存就被操作系统全部回收。

741
来自专栏大数据架构师专家

python异常处理

异常处理是工作中编写代码必须要完成的内容,对于不符合预期的用户操作或数据输入,程序总会出现异常情况,而对异常情况能够妥善处理,是保证程序稳定性的关键工作...

702
来自专栏架构之路

Spring AOP中 args和arg-names的区别

这两天在看aop aspectj的各种语法,发现里面有两个概念 args和arg-names很容易混淆,网上也基本没说清楚,所以就动手试了一下,发现还是自己试试...

3086
来自专栏技术博文

ls按时间排序输出文件列表

ls按时间排序输出文件列表 首先,ls --help查看ls相关的与时间排序相关的参数: > ls --help|grep -E "time|sort" 如果不...

3226
来自专栏康怀帅的专栏

PHP 面向对象 接口

使用接口 interface,可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容。 要实现一个接口,使用 implements 操作符。 接口中定义...

2836
来自专栏游戏开发那些事

【小白学C#】谈谈C#多播委托因异常而终止的解决方案

  前几天,马三在与朋友闲聊技术的时候,朋友忽然抛出一个问题,把马三难倒了,本着求知的精神,回来以后马三就查阅了相关资料并做了一些实验,终于把问题搞明白了,因此...

1223
来自专栏xcywt

关于delete 和 new

关于new和delete,有如下代码 // new_test.cpp #include<iostream> using namespace std; cl...

1906
来自专栏向治洪

Android热补丁技术—dexposed原理简析(手机淘宝采用方案)

上篇文章《Android无线开发的几种常用技术》我们介绍了几种android移动应用开发中的常用技术,其中的热补丁正在被越来越多的开发团队所使用,它涉及到da...

2186

扫码关注云+社区