什么是CLR/JIT:在编译型语言中,所有的语言都必须经过编译器的编译,编译为中间语言,在经过虚拟机的解释,翻译成二进制语言,然后计算机才能运行,而CLR 就是虚拟机。翻译成中文解释就是CLR:公共语言运行时和 即时编译器。
以下是C#语言在运行中执行的过程:
CLR的用处:
公共语言运行时(CLR)是一套完整的、高级的虚拟机,它被设计为用来支持不同的编程语言,并支持它们之间的互操作。在上面也说过了,CLR就是一个解释器,将中间语言解释成计算机可以运行的二进制语言。
了解更多的CLR知识,可以看https://zhuanlan.zhihu.com/p/68158037博客。
主要来说:
1、CLR提供了一个完备的编程平台。
2、CLR是一个编译程序的基石。
3、CLR 的主要功能:
基础功能——那些对其他的特性有广泛影响的功能。包括:
- 垃圾回收
- 内存安全和类型安全
- 对编程语言的高级支持
次要功能——那些由基础功能发展而来的、但不是必须的功能:
- AppDomains 程序隔离
- 程序安全与沙盒
其他功能——那些运行时环境需要的、但并不依赖基础功能的特性。这些功能帮助我们建立了一个完整的编程环境。比如:
- 版本管理
- 调试、性能分析
- 互操作
- 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。
在栈上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不支持以下两种创建方式:
string str01 = "heng";
//错误创建方法1
string str02 = new string("heng");
//错误创建方法2
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
这篇博客还是挺概括的。
托管资源与非托管资源的区别,回收对应的对象应该怎样对待。
托管资源是由CLR全权负责的资源,CLR不负责的资源位非托管资源。
对于托管资源通过GC自动回收。
对于非托管资源GC管理,通过代码调用手动进行清除。
析构函数与Idispose接口的作用
托管的内存资源,这是不需要我们操心的,系统已经为我们进行管理了。
对于非托管的资源,这里再重申一下,就是Stream,数据库的连接,GDI+的相关对象,还有Com对象等等这些操作系统资源,需要我们手动去释放。
如何去释放,应该把这些操作放到哪里比较好呢。.Net提供了三种方法,也是最常见的三种,大致如下:
1. 析构函数;
2. 继承IDisposable接口,实现Dispose方法;
3. 提供Close方法。
析构函数 | Dispose方法 | Close方法 | |
---|---|---|---|
意义 | 销毁对象 | 销毁对象 | 关闭对象资源 |
调用方式 | 不能被显示调用,会被GC调用 | 需要显示调用或者通过using语句 | 需要显示调用 |
调用时机 | 不确定 | 确定,在显示调用或者离开using程序块 | 确定,在显示调用时 |