一、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、内存分配和垃圾回收的更多相关文章

  1. JVM内存分配与垃圾回收机制管理

    项目上线,性能优化有个重要组成就是jvm内存分配和垃圾回收机制的管理配置. 网上随便能搜到相关的具体步骤,以及内存中各种参数对应的意义,不再赘述. 干货就是直接抛出遇到的问题,以及如何解决的,再说说待 ...

  2. 浅谈JVM内存分配与垃圾回收

    大家好,我是微尘,最近又去翻了周志明老师的<深入理解Java虚拟机>这本书.已经看了很多遍了,每次都感觉似乎看懂了,但没过多久就忘了.这次翻了第三章的垃圾收集器与内存分配策略,感觉有了新的 ...

  3. JVM的内存分配与垃圾回收策略

    自动内存管理机制主要解决了两个问题:给对象分配内存以及回收分配给对象的内存. >>垃圾回收的区域 前面的笔记中整理过虚拟机运行数据区,再看一下这个区域: 注意在这个Runtime Data ...

  4. .net 内存分配及垃圾回收总结

        生存期垃圾回收器 目前有很多种类型的垃圾回收器.微软实现了一种生存期垃圾回收器(Generation Garbage Collector). 生存期垃圾回收器将内存分为很多托管堆,每一个托管堆 ...

  5. java内存分配与垃圾回收

    JVM的内存分配主要基于两种,堆和栈. 我们来看一下java程序运行时候的内存分配策略: 1):静态存储区(方法区): 2):栈区: 3):堆区: 1):主要存放静态数据,全局static数据和常量. ...

  6. Java 内存分配及垃圾回收机制初探

    一.运行时内存分配 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域. 这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则 ...

  7. JVM 内存分配和垃圾回收(GC)机制

    一  判断对象是否存活 垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“活着”,哪些已经"死去”,即不能再被任何途径使用的对象. 1.1 引用计数法 (Reference ...

  8. JVM内存分配和垃圾回收以及性能调优

    JVM内存分配策略 一:堆中优先分配Eden 大多数情况下,对象都在新生代的Eden区中分配内存.而新生代会频繁进行垃圾回收. 二:大对象直接进入老年代 需要大量连续空间的对象,如:长字符串.数组等, ...

  9. JVM性能调优(3) —— 内存分配和垃圾回收调优

    前序文章: JVM性能调优(1) -- JVM内存模型和类加载运行机制 JVM性能调优(2) -- 垃圾回收器和回收策略 一.内存调优的目标 新生代的垃圾回收是比较简单的,Eden区满了无法分配新对象 ...

随机推荐

  1. CommonView for wifi抓包破解WPA无线网络

    运行环境:win8 64位+intel 5100n网卡 步骤1:下载CommonView完全破解版,非破解版只有跑10分钟 http://www.nlver.cn/soft/7305.html 步骤2 ...

  2. 使用 Swagger 文档化和定义 RESTful API

    大部分 Web 应用程序都支持 RESTful API,但不同于 SOAP API——REST API 依赖于 HTTP 方法,缺少与 Web 服务描述语言(Web Services Descript ...

  3. elasticsearch 性能监控基础

    一.Elasticsearch 是什么 Elasticsearch是一款用Java编写的开源分布式文档存储和搜索引擎,可以用于near real-time存储和数据检索. 1.Elasticsearc ...

  4. GCD 实现生产-消费 模式

    #import "ViewController.h" @interface ViewController (){ } @property (nonatomic,strong) di ...

  5. 解决Unity协程无法同步返回的问题

    Unity的协程是轻量的异步解决方案,但是每调用一次yield就必须等下一帧才能继续,这一点带来了很多约束. 比如如下代码: void OnEnable() { StartCoroutine(_Do( ...

  6. [tools]hugo&github构建静态网站/百度统计

    hugo/github构建网站基本原理 1.hugo是一个静态化的工具,你写md,然后他把md转换成对应样式的html, 2.并给html嵌入百度统计的script.然后你将html放到github上 ...

  7. 《图说VR入门》——DK2入门及其资源汇总

    本文章由cartzhang编写,转载请注明出处. 全部权利保留. 文章链接:http://blog.csdn.net/cartzhang/article/details/53174895 作者:car ...

  8. angular控制器的执行顺序和服务的注入情况

    这篇文章到底要讲什么呢? 这必须要从栗子开始讲起... 看下面这两段代码: demo1: http://jsfiddle.net/ujzmvp3j/1/ demo2: http://jsfiddle. ...

  9. Ubuntu 系统下卸载 IntelliJ IDEA

    参考:http://blog.csdn.net/csdnones/article/details/50449947 卸载只需要删除解压出来的目录就行了,然后删除/home/你用登录名/IntelliJ ...

  10. s3c2440内存控制器与SDRAM基本测试

    前面我们实验的LED和串口程序,是cpu发送地址给特定的寄存器,在寄存器中写相应的位,达到控制要求. 第一类是GPIO门电路如LED,第二类是协议类如串口,这些都不是cpu直接给地址信息,而是通过寄存 ...