你的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触发时首先认为所有的对象都是垃圾,由局部变量,寄存器,静态变量这些根向上找,凡是包含的对象都认为还有引用,在同步块索引上修改一位标识,当所有对象都遍历过后没有标识的对象就会被清掉,然后再是整理内存、修改引用地址等。
看个简单的例子,只用于演示,不考虑合理性:
using System; namespace Test
{
class Program
{
static void Main(string[] args)
{
int height = ;
int weight = ;
People.Find();
People developer = new Developer()(height, weight);
bool isHealthyWeight = developer.IsHealthyWeight();
bool isRich = developer.IsRich();
}
} class People
{
int _height;
int _weight; public People(int height, int weight)
{
_height = height;
_weight = weight;
} public virtual bool IsRich(); public bool IsHealthyWeight()
{
var healthyWeight = (Height - ) * 0.7;
return Weight <= healthyWeight * 1.1 && Weight >= healthyWeight * 0.9;
} public static string Find(string id) { return ""; }
} class Developer : People
{
public Developer(int height, int weight) : base(height, weight)
{ } public override bool IsRich()
{
return false;
}
} }
*图片不清楚可以放大看
首先判断类型是否都加载,用到了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#这本书,小弟算是总结一下,谢谢观看。
你的C#代码是怎么跑起来的(二)的更多相关文章
- 转【面向代码】学习 Deep Learning(二)Deep Belief Nets(DBNs)
[面向代码]学习 Deep Learning(二)Deep Belief Nets(DBNs) http://blog.csdn.net/dark_scope/article/details/9447 ...
- 一只代码小白git托管路上的二三事
[经验]一只代码小白git托管路上的二三事 写在前面的话 寒假的时候,娄老师给我们布置了代码托管的作业,并要求把托管地址发给学委.因假期的时候没有带电脑回家,所以只是在手机上草草注册了,也稀里糊涂就将 ...
- Java代码加密与反编译(二):用加密算法DES修改classLoader实现对.class文件加密
Java代码加密与反编译(二):用加密算法DES修改classLoader实现对.class文件加密 二.利用加密算法DES实现java代码加密 传统的C/C++自动带有保护机制,但java不同,只要 ...
- CLR via C#(01)-.NET平台下代码是怎么跑起来的
1. 源代码编译为托管模块 程序在.NET框架下运行,首先要将源代码编译为托管模块.CLR是一个可以被多种语言所使用的运行时,它的很多特性可以用于所有面向它的开发语言.微软开发了多种语言的编译器,编译 ...
- 你的C#代码是怎么跑起来的(一)
写了那么多C#代码,大家有没有想过自己写的代码编译后的可执行文件内部是什么样子,是怎样在系统上运行的? 编译成exe,然后双击exe文件运行,这中间到底发生了些什么呢,这篇先来剖析下exe内部的样子: ...
- 你编写的Java代码是咋跑起来的?
如果你是一名 Java 开发人员,你肯定指定 Java 代码有很多种不同的运行方式.比如说可以在开发工具(IDEA.Eclipse等)中运行,可以双击执行 jar 文件运行,也可以在命令行中运行,甚至 ...
- PyODPS DataFrame 的代码在哪里跑
在使用 PyODPS DataFrame 编写数据应用时,尽管编写的是同一个脚本文件,但其中的代码会在不同位置执行,这可能导致一些无法预期的问题,本文介绍当出现相关问题时,如何确定代码在何处执行,以及 ...
- VSCode代码修改后跑起来没反应,打开本地文件,代码没变化
两种解决办法: 首先:修改VSCode默认配置文件,点击左下角设置标志图 -> 设置,出来了设置相关的东西,搜索 files.autoSave 第一种:把"files.autoSave ...
- JavaScript代码段整理笔记系列(二)
上篇介绍了15个常用代码段,本篇将把剩余的15个补齐,希望对大家有所帮助!!! 16.检测Shift.Alt.Ctrl键: event.shiftKey; //检测Shift event.altKey ...
随机推荐
- 十五天精通WCF——第八天 对“绑定”的最后一点理解
转眼已经中断10几天没有写博客了,也不是工作太忙,正好碰到了端午节,然后最近看天津台的爱情保卫战入迷了...太好看了,一直都是回味无穷...而且 涂磊老师话说的真是tmd的经典,然后就这样耽搁了,好了 ...
- SQL Server字符串左匹配
在SQL Server中经常会用到模糊匹配字符串的情况,最简单的办法就是使用like关键字(like语法http://msdn.microsoft.com/en-us/library/ms179859 ...
- hibernate和mybatis的区别
本文出自 alicejiyayun原创 第一章 Hibernate与MyBatis Hibernate 是当前最流行的O/R mapping框架,它出身于sf.NET,现在已经成为Jboss的 ...
- DataTable去除重复行,根据某一字段进行distinct
网上有很多方法,比如利用视图处理: //去掉重复行 DataView dv = table.DefaultView; table = dv.ToTable(true, new string[] { & ...
- node js学习(二)——REPL(交互式解释器)
1.简介 Node.js REPL(Read Eval Print Loop:交互式解释器) 表示一个电脑的环境,类似 Window 系统的终端或 Unix/Linux shell,我们可以在终端中输 ...
- 使用elasticsearch的关键技术点
前言 最近有一个项目用到了搜索引擎,这里记录下使用过程中遇到的一些问题和解决方案. 0.准备工作 1)安装elasticsearch 2)安装Marvel 3)安装head tips:在es的配置文件 ...
- Linux压缩指令
1.windows系统和Linux系统的压缩文件只有zip格式的是通用的 2.windows系统可以解压几乎所有的Linux压缩格式 3.Linux压缩指令 gzip 指令 格式: gzip 压缩的文 ...
- 【redis使用全解析】常见运维操作
作者:gnuhpc 出处:http://www.cnblogs.com/gnuhpc/ 1.1 启动 1.1.1 启动redis $ redis-server redis.conf 常见选项: ./r ...
- 编译Ansj之Solr插件
Ansj是一个比较优秀的中文分词组件,具体情况就不在本文介绍了.ansj作者在其官方代码中,提供了对lucene接口的支持.如果用在Solr下,还需要简单的扩展一下. 1.基于maven管理 ansj ...
- MMORPG大型游戏设计与开发(part2 of net)
网络第二部分的将要给大家描述的是网络代码方面的设计,从基础的代码讲起,了解详细的网络模块构架. 没有放出整个源代码,是因为其中还有许多不足的地方,不过想必大家应该也能猜想出这个项目源码的地址了.不过对 ...