什么是CLR/JIT:在编译型语言中,所有的语言都必须经过编译器的编译,编译为中间语言,在经过虚拟机的解释,翻译成二进制语言,然后计算机才能运行,而CLR 就是虚拟机。翻译成中文解释就是CLR:公共语言运行时和 即时编译器。

以下是C#语言在运行中执行的过程:

18_C#之CLRJIT - 图1

CLR的用处:

公共语言运行时(CLR)是一套完整的、高级的虚拟机,它被设计为用来支持不同的编程语言,并支持它们之间的互操作。在上面也说过了,CLR就是一个解释器,将中间语言解释成计算机可以运行的二进制语言。

了解更多的CLR知识,可以看https://zhuanlan.zhihu.com/p/68158037博客。

主要来说:

1、CLR提供了一个完备的编程平台。

2、CLR是一个编译程序的基石。

3、CLR 的主要功能:

  1. 基础功能——那些对其他的特性有广泛影响的功能。包括:

    1. 垃圾回收
    2. 内存安全和类型安全
    3. 对编程语言的高级支持
  2. 次要功能——那些由基础功能发展而来的、但不是必须的功能:

    1. AppDomains 程序隔离
    2. 程序安全与沙盒
  3. 其他功能——那些运行时环境需要的、但并不依赖基础功能的特性。这些功能帮助我们建立了一个完整的编程环境。比如:

    1. 版本管理
    2. 调试、性能分析
    3. 互操作
  4. 4、CLR中最重要的就是包含了GC,GC是面向对象语言很重要的角色。

面向对象语言的编译环节

自己理解:以C#为例,C#语言首先要通过编译器的编译生成中间语言IL,然后中间语言会通过CLR编译成二进制语言,在交到操作系统运行。

堆栈与内存的关系,堆栈的划分

在我们日常编写的程序中,程序运行一定要占用内存的,具体占用内存的什么地方,这是需要理解的,我们日常编写程序时,都知道声明一个变量会占用内存,new一个对象会占用内存,这里的内存,只是内存的总体说明,以为在日常生活中,我们使用的电子产品都是用内存来衡量大小的,实际上的内存在开发者中是有具体的分区的,下面介绍内存才C#中的使用:

内存的分区:

在C#中我理解与查阅资料,将内存分为四个区:

1、栈区:定义在栈中的数据,如值类型数据,在所在方法中执行完毕就会被弹栈,如果有些地址也在这个方法中,那么也会弹栈,这就是为什么在其他方法中不能再访问其他方法中定义的对象。而这些对象岂不是“失联”了,不就内存泄漏了﹖其实不会,这就是.net平台的垃圾回收机制(分代进行回收),保证内存充足。

2、堆区:引用类型:Person p=new Person(); p存放在代码区,new Person()产生对象存放在堆区,由.net平台的垃圾回收机制(GC)管理,将堆区对象的地址存放在p在栈区开辟的内存里;

3、全局区:全局变量和静态类、常量、静态成员,都是在全局区,但是它们的地址仍放在栈区,为什么会保存住呢?因为在.net程序编译时这些静态和全局都是最先编译的,所以最先压栈,那么也就只能等程序结束时才会弹栈,所以全程可用。缺点就是启动慢、编译时间长;当然优点也有,如常说的,静态类常用于窗体传值和实现单例模式,这就得益于它的一次编译全程可用。

4、代码区:存放函数体内的二进制代码。

内存的分配

1、值类型

Int a = 10;

Int 表示变量为值类型;

变量a放在代码区;

10赋值给a,10存放在a在栈区开辟的内存区内;

a内赋的值为值类型对象本身,即为值10(在栈区内存区内a代表的就是值10本身)。

2、值类型扩展

Int a =10;

Int b =a;

b = 20;

10存放在a在栈区开辟的内存区内。a 把值10赋给b,b会在重新在栈区开辟的新的内存区来储存值,两个内存区保持独立互不影响。b重新赋值,b会把内存区内(栈区)的值10替换成值20重新存储,因为两个内存区互相独立,因此a的内存区内的值不会受影响而改变,仍然为值10。

18_C#之CLRJIT - 图2

在栈上a和b分别占用一块内存区,互不干扰。

3、引用类型

Person Tom = new Person();

Tom存放在代码区;

new Person()产生的对象存放在堆区开辟的内存区内;

将堆区对象的引用地址赋值给Tom,引用地址(指针)存放在Tom在栈区开辟的内存区内;(在栈区内存区内Tom保存的是一份引用地址(指针),通过引用地址去堆区内寻找对象本身的值)

4、引用类型扩展

5、静态类型

值类型存储在栈中, 引用类型存储在堆中. 这与静态与否没关系, 静态与否只影响对象的初始化()

所谓静态,就是一定会存在的而且会永恒存在、不会消失,这样的数据包括常量、常变量(const 变量)、静态变量、全局变量等。这些变量的指针存放在栈区,值本身存放在静态存储区,他们在程序编译完成后就已经分配好了,生命周期持续至程序结束。

内存管理

1)栈区管理:

C#中栈是编译期间就分配好的内存空间,因此你的代码中必须就栈的大小有明确的定义;栈区内存无需我们管理,也不受GC管理,栈顶元素使用完毕弹出就会立即释放。

2)堆区管理:

堆区是程序运行期间动态分配的内存空间,你可以根据程序的运行情况确定要分配的堆内存的大小。在C#中堆区内存由GC(Garbage collection:垃圾收集器)负责清理,当对象超出作用域范围或者对象失去指向的引用地址,就会在一定时间内进行统一的处理,无需程序员手动处理。

当对象不再使用时,这个被存储在堆栈中的引用变量将被删除,但是从上述机制可以看出,在托管堆中这个引用指向的对象仍然存在,其空间何时被释放取决垃圾收集器而不是引用变量失去作用域时。

在使用电脑的过程中大家可能都有过这种经验:电脑用久了以后程序运行会变得越来越慢,其中一个重要原因就是系统中存在大量内存碎片,就是因为程序反复在堆栈中创建和释入变量,久而久之可用变量在内存中将不再是连续的内存空间,为了寻址这些变量也会增加系统开销。在.net中这种情形将得到很大改善,这是因为有了垃圾收集器的工作,垃圾收集器将会压缩托管堆的内存空间,保证可用变量在一个连续的内存空间内,同时将堆栈中引用变量中的地址改为新的地址,这将会带来额外的系统开销,但是,其带来的好处将会抵消这种影响,而另外一个好处是,程序员将不再花上大量的心思在内在泄露问题上。

String类型的特殊之处

string是引用类型的,在C#中引用类型的内存分配在托管堆(堆内存)上。string不支持以下两种创建方式:

  1. string str01 = "heng";
  2. //错误创建方法1
  3. string str02 = new string("heng");
  4. //错误创建方法2
  5. string str03 = new string(str01);

string是System.String的别名,绝大多数情况下使用string和System.String是等效的,两者细微的区别在这里不讨论,在此处我们可以理解为两者完全一样。

string是可读但是不可写的,在C#中,每一个字符串都有一个暂存池存储字符串,当赋值的时候,只是更改简单的字符串引用。处理字符串一定要有变量来接收返回值,因为所有对于字符串的操作都会返回一个新的字符串,而原字符串则不受影响。

string暂存池

string作为最常用的类型,在实际项目中会有大量的字符串操作,这样会带来大量的字符串创建,内存分配、回收,进而影响性能。因此CLR对于string进行了特殊的优化,CLR中存在“字符串暂存池”概念。那么CLR 如何做到的呢?在CLR初始化时创建一个内部的哈希表,这个表相当于一个字典表,键就是字符串,值是指向托管堆中该字符串对象的引用。

不是所有的字符串都放在暂存池中,以下三种情况会查询暂存池(若查询不到就将其存入暂存池):

  • 利用字面量值创建string对象
  • 利用string.Intern()创建string对象
  • 字面量值+字面量值拼接创建string对象

以下两种情况会不查询暂存池:

  • 利用ToString()创建 string对象(StringBuilder、char[])
  • 利用new string()创建 string对象

C#中有个(Intern Pool)暂存池,但当初居然认为所有的字符串都会存放在此。

1. 驻留池由CLR来维护,其中的所有字符串对象的值都不相同。

2. 只有编译阶段的文本字符常量会被自动添加到驻留池。

3.运行时期动态创建的字符串不会被加入到驻留池中。

4.string.Intern()可以把动态创建的字符串加入到驻留池中。

即使这个动态创建的字符串和驻留池中的某个字符串的值相等,引用也不会相等。

即使是动态创建的两个字符串的值相等,他们的引用依然不相等。(charArray.ToString()特例)

1)== 它是比较的栈里面的值是否相等(值比较)

2)Equals 它比较的是堆里面的值是否相等(引用地址值比较)

3)Object.ReferenceEquals(obj1,obj2) 它是比较的是内存地址是否相等

堆中队对象的GC回收机制

https://www.cnblogs.com/wangqiang3311/p/10280000.html

https://blog.csdn.net/tran119/article/details/81459640

这篇博客还是挺概括的。

18_C#之CLRJIT - 图3

托管资源与非托管资源的区别,回收对应的对象应该怎样对待。

托管资源是由CLR全权负责的资源,CLR不负责的资源位非托管资源。

对于托管资源通过GC自动回收。

对于非托管资源GC管理,通过代码调用手动进行清除。

析构函数与Idispose接口的作用

托管的内存资源,这是不需要我们操心的,系统已经为我们进行管理了。

对于非托管的资源,这里再重申一下,就是Stream,数据库的连接,GDI+的相关对象,还有Com对象等等这些操作系统资源,需要我们手动去释放。

如何去释放,应该把这些操作放到哪里比较好呢。.Net提供了三种方法,也是最常见的三种,大致如下:

1. 析构函数;

2. 继承IDisposable接口,实现Dispose方法;

3. 提供Close方法。

析构函数 Dispose方法 Close方法
意义 销毁对象 销毁对象 关闭对象资源
调用方式 不能被显示调用,会被GC调用 需要显示调用或者通过using语句 需要显示调用
调用时机 不确定 确定,在显示调用或者离开using程序块 确定,在显示调用时