CLR、内存分配和垃圾回收
一、CLR
CLR:即公共语言运行时(Common Language Runtime),是中间语言(IL)的运行时环境,负责将编译生成的MSIL编译成计算机可以识别的机器码,负责资源管理(内存分配和垃圾回收等)。
可能有人会提问:为什么不直接编译成机器码,而要先编译成IL,然后在编译成机器码呢?
原因是:计算机的操作系统不同(分为32位和64位),接受的计算机指令也是不同的,在不同的操作系统中就要进行不同的编译,写出的代码在不同的操作系统中要进行不同的修改。中间增加了IL层,不管是什么操作系统,编译生成的IL都是相同的,IL被不同操作系统的CLR编译成机器码,最终被计算机执行。
JIT:即时编译器,负责编译成机器码。
二、内存分配
内存分配:指程序运行时,进程占用的内存,由CLR负责分配。
值类型:值类型是struct的,例如:int、datetime等。
引用类型:即class,例如:类、接口,string等。
1、栈
栈:即线程栈,先进后出的一种数据结构,随着线程而分配,其顺序如下:
看下面的例子:
定义一个结构类型
public struct ValuePoint
{
public int x;
public ValuePoint(int x)
{
this.x = x;
}
}
在方法里面调用:
//先声明变量,没有初始化 但是我可以正常赋值 跟类不同
ValuePoint valuePoint;
valuePoint.x = ; ValuePoint point = new ValuePoint();
Console.WriteLine(valuePoint.x);
内存分配情况如下图所示:
注意:
(1)、值类型分配在线程栈上面,变量和值都是在线程栈上面。
(2)、值类型可以先声明变量而不用初始化。
2、堆
堆:对象堆,是进程中独立划出来的一块内存,有时一些对象需要长期使用不释放、对象的重用,这些对象就需要放到堆上。
来看下面的例子:
定义一个类
public class ReferencePoint
{
public int x;
public ReferencePoint(int x)
{
this.x = x;
}
}
在代码中调用:
ReferencePoint referencePoint = new ReferencePoint();
Console.WriteLine(referencePoint.x);
其内存分配如下:
注意:
(1)、引用类型分配在堆上面,变量在栈上面,值在堆上面。
(2)、引用类型分配内存的步骤:
a、new的时候去对象堆里面开辟一块内存,分配一个内存地址。
b、调用构造函数(因为在构造函数里面可以使用this),这时才执行构造函数。
c、把地址引用传给栈上面的变量。
3、复杂类型
a、引用类型里面嵌套值类型
先看下面引用类型的定义:
public class ReferenceTypeClass
{
private int _valueTypeField;
public ReferenceTypeClass()
{
_valueTypeField = ;
}
public void Method()
{
int valueTypeLocalVariable = ;
}
}
在一个引用类型里面定义了一个值类型的属性:_valueTypeField和一个值类型的局部变量:valueTypeLocalVariable,那么这两个值类型是如何进行内存分配的呢?其内存分配如下:
内存分配为什么是这种情况呢?值类型不应该是都分配在栈上面吗?为什么一个是分配在堆上面,一个是分配在栈上面呢?
_valueTypeField分配在堆上面比较好理解,因为引用类型是在堆上面分配了一整块内存,引用类型里面的属性也是在堆上面分配内存。
valueTypeLocalVariable分配在栈上面是因为valueTypeLocalVariable是一个全新的局部变量,调用方法的时候,会启用一个线程去调用,线程栈来调用方法,然后把局部变量分配到栈上面。
b、值类型里面嵌套引用类型
先来看看值类型的定义:
public struct ValueTypeStruct
{
private object _referenceTypeField;
public ValueTypeStruct(int x)
{
_referenceTypeField = new object();
}
public void Method()
{
object referenceTypeLocalVariable = new object();
}
}
在值类型里面定义了引用类型,其内存是如何分配的呢?其内存分配如下:
从上面的截图中可以看出:值类型里面的引用类型的变量分配在栈上,值分配在堆上。
总结:
1、方法的局部变量
根据变量自身的类型决定,与所在的环境没关系。变量如果是值类型,就分配在栈上。变量如果是引用类型,内存地址的引用存放在栈上,值存放在堆上。
2、对象是引用类型
其属性/字段,都是在堆上分配内存。
3、对象是值类型
其属性/字段由自身的类型决定。属性/字段是值类型就分配在栈上;属性/字段是引用类型就分配在堆上。
上面的三种情况可以概括成下面一句话:
引用类型在任何时候都是分配在堆上;值类型任何时候都是分配在栈上,除非值类型是在引用类型里面。
4、String字符串的内存分配
首先要明确一点:string是引用类型。
先看看下面的例子:
string student = "大山";//在堆上面开辟一块儿内存 存放“大山” 返还一个引用(student变量)存放在栈上
其内存分配如下图所示:
这时,在声明一个变量student2,然后用student给student2赋值:
string student2 = student;
这时内存是如何分配的呢?其内存分配如下:
从上面的截图中可以看出:student2被student赋值的时候,是在栈上面复制一份student的引用给student2,然后student和student2都是指向堆上面的同一块内存。
输出student和student2的值:
Console.WriteLine("student的值是:" + student);
Console.WriteLine("student2的值是:"+student2);
结果:
从结果可以看出:student和student2的值是一样的,这也能说明student和student2指向的是同一块内存。
这时修改student2的值:
student2 = "App";
这时在输出student和student2的值,其结果如下图所示:
从结果中可以看出:student的值保持不变,student2的值变为App,为什么是这样呢?这是因为string字符串的不可变性造成的。一个string变量一旦声明并初始化以后,其在堆上面分配的值就不会改变了。这时修改student2的值,并不会去修改堆上面分配的值,而是重新在堆上面开辟一块内存来存放student2修改后的值。修改后的内存分配如下:
在看下面一个例子:
string student = "大山";
string student2 = "App";
student2 = "大山";
Console.WriteLine(object.ReferenceEquals(student,student2));
结果:
可能有人会想:按照上面讲解的,student和student2应该指向的是不同的内存地址,结果应该是false啊,为什么会是true呢?这是因为CLR在分配内存的时候,会查找是否有相同的值,如果有相同的值,就重用;如果没有,这时在重新开辟一块内存。所以修改student2以后,student和student2都是指向同一块内存,结果输出是true。
注意:
这里需要区分string和其他引用类型的内存分配。其他引用类型的情况和string正好相反。看下面的例子
先定义一个Refence类,里面有一个int类型的属性,类定义如下:
public class Refence
{
public int Value { get; set; }
}
在Main()方法里面调用:
Refence r1 = new Refence();
r1.Value = ;
Refence r2 = r1;
Console.WriteLine($"r2.Value的值:{r2.Value}");
r2.Value = ;
Console.WriteLine($"r1.Value的值:{r1.Value}");
Console.ReadKey();
结果:
从运行结果可以看出,如果是普通的引用类型,如果修改其他一个实例的值,那么另一个实例的值也会改变。正好与string类型相反。
三、内存回收
值类型存放在线程栈上,线程栈是每次调用都会产生,用完自己就会释放。
引用类型存放在堆上面,全局共享一个堆,空间有限,所以才需要垃圾回收。
CLR在堆上面是连续分配内存的。
1、C#中的资源分为两类:
a、托管资源
由CLR管理的存在于托管堆上的称为托管资源,注意这里有2个关键点,第一是由CLR管理,第二存在于托管堆上。托管资源的回收工作是不需要人工干预的,CLR会在合适的时候调用GC(垃圾回收器)进行回收。
b、非托管资源
非托管资源是不由CLR管理,例如:Image Socket, StreamWriter, Timer, Tooltip, 文件句柄, GDI资源, 数据库连接等等资源(这里仅仅列举出几个常用的)。这些资源GC是不会自动回收的,需要手动释放。
2、托管资源
a、垃圾回收期(GC)
定期或在内存不够时,通过销毁不再需要或不再被引用的对象,来释放内存,是CLR的一个重要组件。
b、垃圾回收器销毁对象的两个条件
1)对象不再被引用----设置对象=null。
2)对象在销毁器列表中没有被标记。
c、垃圾回收发生时机
1)垃圾回收发生在new的时候,new一个对象时,会在堆中开辟一块内存,这时会查看内存空间是否充足,如果内存空间不够,则进行垃圾回收。
2)程序退出的时候也会进行垃圾回收。
d、垃圾回收期工作原理
GC定期检查对象是否未被引用,如果对象没有被引用,则在检查销毁器列表。若在销毁器列表中没有标记,则立即回收。若在销毁器列表中有标记,则开启销毁器线程,由该线程调用析构函数,析构函数执行完,删除销毁器列表中的标记。
注意:
不建议写析构函数,原因如下:
1)对象即使不用,也会在内存中驻留很长一段时间。
2)销毁器线程为单独的线程,非常耗费资源。
e、优化策略
1)分级策略
a、首次GC前 全部对象都是0级。
b、第一次GC后,还保留的对象叫1级。这时新创建的对象就是0级。
c、垃圾回收时,先查找0级对象,如果空间还不够,再去找1级对象,这之后,还存在的一级对象就变成2级,0级对象就变成一级对象。
d、垃圾回收时如果0~2级都不够,那么就内存溢出了。
注意:
越是最近分配的,越是会被回收。因为最近分配的都是0级对象,每次垃圾回收时都是先查询0级对象。
3、非托管资源
上面讲的都是针对托管资源的,托管资源会被GC回收,不需要考虑释放。但是,垃圾回收器不知道如何释放非托管的资源(例如,文件句柄、网络连接和数据库连接)。托管类在封装对非托管资源的直接或间接引用时,需要制定专门的规则,确保非托管的资源在回收类的一个实例时会被释放。
在定义一个类时,可以使用两种机制来自动释放非托管的资源。这些机制常常放在一起实现,因为每种机制都为问题提供了略为不同的解决方法。这两种机制是:
a、声明一个析构函数(或终结器),作为类的一个成员。
b、在类中实现System.IDisposable接口。
1)、析构函数或终结器
析构函数看起来类似于一个方法:与包含的类同名,但有一个前缀波形符号(~)。它没有返回值,不带参数,也没有访问修饰符。看下面的一个例子:
public class MyClass
{
/// <summary>
/// 析构函数
/// </summary>
~MyClass()
{
// 要执行的代码
}
}
析构函数存在的问题:
a、由于使用C#时垃圾回收器的工作方式,无法确定C#对象的析构函数何时执行。所以,不能在析构函数中放置需要在某一时刻运行的代码,也不应该寄希望于析构函数会以特定顺序对不同类的实例调用。如果对象占用了宝贵而重要的资源,应尽快释放这些资源,此时就不能等待垃圾回收器来释放了。
b、C#析构函数的实现会延迟对象最终从内存中删除的时间。没有析构函数的对象会在垃圾回收器的一次处理中从内存中删除,但有析构函数的对象需要两次处理才能销毁:第一次调用析构函数时,没有删除对象,第二次调用才真正删除对象。
c、运行库使用一个线程来执行所有对象的Finalize()方法。如果频繁使用析构函数,而且使用它们执行长时间的清理任务,对性能的影响就会非常显著。
注意:
在讨论C#中的析构函数时,在低层的.NET体系结构中,这些函数称为终结器(finalizer)。在C#中定义析构函数时,编译器发送给程序集的实际上是Finalize()方法,它不会影响源代码。C#编译器在编译析构函数时,它会隐式地把析构函数的代码编译为等价于重写Finalize()方法的代码,从而确保执行父类的Finalize()方法。例如,下面的C#代码等价于编译器为~MyClass()析构函数生成的IL:
protected override void Finalize()
{
try
{
// 析构函数中要执行的代码
}
finally
{
// 调用父类的Finalize()方法
base.Finalize();
}
}
2)、IDisposable接口
在C#中,推荐使用System.IDisposable接口替代析构函数。IDisposable接口定义了一种模式,该模式为释放非托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾回收器相关的问题。IDisposable接口声明了一个Dispose()方法,它不带参数,返回void。例如:
public class People : IDisposable
{
public void Dispose()
{
this.Dispose();
}
}
Dispose()方法的实现代码显式地释放由对象直接使用的所有非托管资源,并在所有也实现了IDisposable接口的封装对象上调用Dispose()方法。这样,Dispose()方法为何时释放非托管资源提供了精确的控制。
3)、using语句
C#提供了一种语法,可以确保在实现了IDisposable接口的对象的引用超出作用域时,在该对象上自动调用Dispose()方法。该语法使用了using关键字来完成此工作。例如:
using (var people = new People())
{
// 要处理的代码
}
4)、析构函数和Dispose()的区别
a、析构函数
析构函数 主要是用来释放非托管资源,等着GC的时候去把非托管资源释放掉 系统自动执行。GC回收的时候,CLR一定调用的,但是可能有延迟(释放对象不知道要多久呢)。
b、Dispose()
Dispose() 也是释放非托管资源的,主动释放,方法本身是没有意义的,我们需要在方法里面实现对资源的释放。GC的时候不会调用Dispose()方法,而是使用对象时,使用者主动调用这个方法,去释放非托管资源。
5)、终结器和IDisposable接口的规则
a、如果类定义了实现IDisposable的成员(类里面的属性实现了IDisposable接口),那么该类也应该实现IDisposable接口。
b、实现IDisposable并不意味着也应该实现一个终结器。终结器会带来额外的开销,因为它需要创建一个对象,释放该对象的内存,需要GC的额外处理。只在需要时才应该实现终结器,例如。发布本机资源。要释放本机资源,就需要终结器。
c、如果实现了终结器,也应该实现IDisposable接口。这样,本机资源可以早些释放,而不仅是在GC找出被占用的资源时,才释放资源。
d、在终结器的实现代码中,不能访问已经终结的对象。终结器的执行顺序是没有保证的。
e、如果所使用的一个对象实现了IDisposable接口,就在不再需要对象时调用Dispose方法。如果在方法中使用这个对象,using语句比较方便。如果对象是类的一个成员,那么类也应该实现IDisposable接口。
CLR、内存分配和垃圾回收的更多相关文章
- JVM内存分配与垃圾回收机制管理
项目上线,性能优化有个重要组成就是jvm内存分配和垃圾回收机制的管理配置. 网上随便能搜到相关的具体步骤,以及内存中各种参数对应的意义,不再赘述. 干货就是直接抛出遇到的问题,以及如何解决的,再说说待 ...
- 浅谈JVM内存分配与垃圾回收
大家好,我是微尘,最近又去翻了周志明老师的<深入理解Java虚拟机>这本书.已经看了很多遍了,每次都感觉似乎看懂了,但没过多久就忘了.这次翻了第三章的垃圾收集器与内存分配策略,感觉有了新的 ...
- JVM的内存分配与垃圾回收策略
自动内存管理机制主要解决了两个问题:给对象分配内存以及回收分配给对象的内存. >>垃圾回收的区域 前面的笔记中整理过虚拟机运行数据区,再看一下这个区域: 注意在这个Runtime Data ...
- .net 内存分配及垃圾回收总结
生存期垃圾回收器 目前有很多种类型的垃圾回收器.微软实现了一种生存期垃圾回收器(Generation Garbage Collector). 生存期垃圾回收器将内存分为很多托管堆,每一个托管堆 ...
- java内存分配与垃圾回收
JVM的内存分配主要基于两种,堆和栈. 我们来看一下java程序运行时候的内存分配策略: 1):静态存储区(方法区): 2):栈区: 3):堆区: 1):主要存放静态数据,全局static数据和常量. ...
- Java 内存分配及垃圾回收机制初探
一.运行时内存分配 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域. 这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则 ...
- JVM 内存分配和垃圾回收(GC)机制
一 判断对象是否存活 垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“活着”,哪些已经"死去”,即不能再被任何途径使用的对象. 1.1 引用计数法 (Reference ...
- JVM内存分配和垃圾回收以及性能调优
JVM内存分配策略 一:堆中优先分配Eden 大多数情况下,对象都在新生代的Eden区中分配内存.而新生代会频繁进行垃圾回收. 二:大对象直接进入老年代 需要大量连续空间的对象,如:长字符串.数组等, ...
- JVM性能调优(3) —— 内存分配和垃圾回收调优
前序文章: JVM性能调优(1) -- JVM内存模型和类加载运行机制 JVM性能调优(2) -- 垃圾回收器和回收策略 一.内存调优的目标 新生代的垃圾回收是比较简单的,Eden区满了无法分配新对象 ...
随机推荐
- SQL SERVER 查看数据库信息
a.使用sys.database_files查看有关数据库文件的信息 b.使用sys.filegroups查看有关数据库组的信息. c.使用sys.master_files 查看数据库文件的基本信息和 ...
- 关于ios7的适配问题
过年回来一直搞ios7的项目适配,也算是有些眉目. 方法一:直接动用Window #if __IPHONE_OS_VERSION_MAX_ALLOWED>=__IPHONE_7_0 [appli ...
- 【Unity】6.6 Random类
分类:Unity.C#.VS2015 创建日期:2016-04-20 一.简介 Unity引擎提供的Random类可以用来生成随机数.随机点或旋转角度. 1.成员变量 seed:设置用于随机数生成器的 ...
- vue-cli 工程中引入jquery
在vue-cli 生成的工程中引入了jquery,记录一下.(模板用的webpack) 首先在package.json里的dependencies加入"jquery" : &quo ...
- 使用ScriptEngineManager解析json
使用ScriptEngineManager能够解析比較复杂的json串,应用比較方便,可是效率不敢说,由于这种方法相当于执行了一次js.会占资源, 代码例如以下: try { String mdski ...
- GitBash: 右键添加 Git Bash Here 菜单
步骤: 1.通过在“运行”中输入‘regedit’,打开注册表. 2.找到[HKEY_CLASSES_ROOT\Directory\Background]. 3.在[Background]下如果没有[ ...
- linux命令(36):vimdiff文件对比
启动方法 首先保证系统中的diff命令是可用的.Vim的diff模式是依赖于diff命令的.Vimdiff的基本用法就是: # vimdiff FILE_LEFT FILE_RIGHT 或者 # vi ...
- BleedTree动画混合树[Unity]
Unity在中土大陆的大肆推广能从广泛的中文翻译资料中看出.所以,手册才是王道. 游戏动画中的一个常见任务是在两个或更多相似运动之间混合.最佳的已知示例可能是根据角色速度混合行走和奔跑动画.另一个示例 ...
- Animation.setFillAfter and Animation.setFillBefore的作用
转:http://blog.csdn.net/yangweigbh/article/details/9788531 setFillAfter(boolean fillAfter) 在Android ...
- 关于RAID_1+0和RAID_0+1的比较
RAID的概念就不多说了,说说 RAID 0 和 RAID 1 . RAID 0 是条带存储,叠加所有硬盘容量,因此不具有容错性,原理如下图所示: RAID 1 使用非常原始的方式(复制一份.镜像)进 ...