CLR和内存分配

一、CLR

CLR:即公共语言运行时(Common Language Runtime),是中间语言(IL)的运行时环境,负责将编译生成的MSIL编译成计算机可以识别的机器码,负责资源管理(内存分配和垃圾回收等)。

可能有人会提问:为什么不直接编译成机器码,而要先编译成IL,然后在编译成机器码呢?

原因是:计算机的操作系统不同(分为32位和64位),接受的计算机指令也是不同的,在不同的操作系统中就要进行不同的编译,写出的代码在不同的操作系统中要进行不同的修改。中间增加了IL层,不管是什么操作系统,编译生成的IL都是相同的,IL被不同操作系统的CLR编译成机器码,最终被计算机执行。

JIT:即时编译器,负责编译成机器码。

二、内存分配

内存分配:指程序运行时,进程占用的内存,由CLR负责分配。

值类型:值类型是struct的,例如:int、datetime等。

引用类型:即class,例如:类、接口,string等。

1、栈

栈:即线程栈,先进后出的一种数据结构,随着线程而分配,其顺序如下:

看下面的例子:

定义一个结构类型

1 public struct ValuePoint
2 {
3     public int x;
4     public ValuePoint(int x)
5     {
6          this.x = x;
7     }
8 }

在方法里面调用:

1 //先声明变量,没有初始化  但是我可以正常赋值  跟类不同
2 ValuePoint valuePoint;
3 valuePoint.x = 123;
4
5 ValuePoint point = new ValuePoint();
6 Console.WriteLine(valuePoint.x);

内存分配情况如下图所示:

注意:

(1)、值类型分配在线程栈上面,变量和值都是在线程栈上面。

(2)、值类型可以先声明变量而不用初始化。

2、堆

堆:对象堆,是进程中独立划出来的一块内存,有时一些对象需要长期使用不释放、对象的重用,这些对象就需要放到堆上。

来看下面的例子:

定义一个类

1 public class ReferencePoint
2 {
3      public int x;
4      public ReferencePoint(int x)
5      {
6            this.x = x;
7      }
8 }

在代码中调用:

1 ReferencePoint referencePoint = new ReferencePoint(123);
2 Console.WriteLine(referencePoint.x);

其内存分配如下:

注意:

(1)、引用类型分配在堆上面,变量在栈上面,值在堆上面。

(2)、引用类型分配内存的步骤:

a、new的时候去对象堆里面开辟一块内存,分配一个内存地址。

b、调用构造函数(因为在构造函数里面可以使用this),这时才执行构造函数。

c、把地址引用传给栈上面的变量。

3、复杂类型

a、引用类型里面嵌套值类型

先看下面引用类型的定义:

 1 public class ReferenceTypeClass
 2 {
 3         private int _valueTypeField;
 4         public ReferenceTypeClass()
 5         {
 6             _valueTypeField = 0;
 7         }
 8         public void Method()
 9         {
10             int valueTypeLocalVariable = 0;
11         }
12 }

在一个引用类型里面定义了一个值类型的属性:_valueTypeField和一个值类型的局部变量:valueTypeLocalVariable,那么这两个值类型是如何进行内存分配的呢?其内存分配如下:

内存分配为什么是这种情况呢?值类型不应该是都分配在栈上面吗?为什么一个是分配在堆上面,一个是分配在栈上面呢?

_valueTypeField分配在堆上面比较好理解,因为引用类型是在堆上面分配了一整块内存,引用类型里面的属性也是在堆上面分配内存。

valueTypeLocalVariable分配在栈上面是因为valueTypeLocalVariable是一个全新的局部变量,调用方法的时候,会启用一个线程去调用,线程栈来调用方法,然后把局部变量分配到栈上面。

b、值类型里面嵌套引用类型

先来看看值类型的定义:

 1 public struct ValueTypeStruct
 2 {
 3         private object _referenceTypeField;
 4         public ValueTypeStruct(int x)
 5         {
 6             _referenceTypeField = new object();
 7         }
 8         public void Method()
 9         {
10             object referenceTypeLocalVariable = new object();
11         }
12 }

在值类型里面定义了引用类型,其内存是如何分配的呢?其内存分配如下:

从上面的截图中可以看出:值类型里面的引用类型的变量分配在栈上,值分配在堆上。

总结:

1、方法的局部变量

根据变量自身的类型决定,与所在的环境没关系。变量如果是值类型,就分配在栈上。变量如果是引用类型,内存地址的引用存放在栈上,值存放在堆上。

2、对象是引用类型

其属性/字段,都是在堆上分配内存。

3、对象是值类型

其属性/字段由自身的类型决定。属性/字段是值类型就分配在栈上;属性/字段是引用类型就分配在堆上。

上面的三种情况可以概括成下面一句话:

引用类型在任何时候都是分配在堆上;值类型任何时候都是分配在栈上,除非值类型是在引用类型里面。

4、String字符串的内存分配

首先要明确一点:string是引用类型。

先看看下面的例子:

string student = "大山";//在堆上面开辟一块儿内存  存放“大山”  返还一个引用(student变量)存放在栈上

其内存分配如下图所示:

这时,在声明一个变量student2,然后用student给student2赋值:

1 string student2 = student;

这时内存是如何分配的呢?其内存分配如下:

从上面的截图中可以看出:student2被student赋值的时候,是在栈上面复制一份student的引用给student2,然后student和student2都是指向堆上面的同一块内存。

输出student和student2的值:

1 Console.WriteLine("student的值是:" + student);
2 Console.WriteLine("student2的值是:"+student2);

结果:

从结果可以看出:student和student2的值是一样的,这也能说明student和student2指向的是同一块内存。

这时修改student2的值:

1 student2 = "App";

这时在输出student和student2的值,其结果如下图所示:

从结果中可以看出:student的值保持不变,student2的值变为App,为什么是这样呢?这是因为string字符串的不可变性造成的。一个string变量一旦声明并初始化以后,其在堆上面分配的值就不会改变了。这时修改student2的值,并不会去修改堆上面分配的值,而是重新在堆上面开辟一块内存来存放student2修改后的值。修改后的内存分配如下:

在看下面一个例子:

1 string student = "大山";
2 string student2 = "App";
3 student2 = "大山";
4 Console.WriteLine(object.ReferenceEquals(student,student2));

结果:

可能有人会想:按照上面讲解的,student和student2应该指向的是不同的内存地址,结果应该是false啊,为什么会是true呢?这是因为CLR在分配内存的时候,会查找是否有相同的值,如果有相同的值,就重用;如果没有,这时在重新开辟一块内存。所以修改student2以后,student和student2都是指向同一块内存,结果输出是true。

三、内存回收

值类型存放在线程栈上,线程栈是每次调用都会产生,用完自己就会释放。

引用类型存放在堆上面,全局共享一个堆,空间有限,所以才需要垃圾回收。

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回收,不需要考虑释放,非托管资源需要自行进行释放。

非托管资源的释放需要实现IDisposable接口里面的Dispose方法。例如:

1 public class People : IDisposable
2 {
3         public void Dispose()
4         {
5             this.Dispose();
6         }
7 }

4、析构函数和Dispose()的区别

a、析构函数

析构函数  主要是用来释放非托管资源,等着GC的时候去把非托管资源释放掉  系统自动执行。GC回收的时候,CLR一定调用的,但是可能有延迟(释放对象不知道要多久呢)。

b、Dispose()

Dispose() 也是释放非托管资源的,主动释放,方法本身是没有意义的,我们需要在方法里面实现对资源的释放。GC的时候不会调用Dispose()方法,而是使用对象时,使用者主动调用这个方法,去释放非托管资源。

原文地址:https://www.cnblogs.com/dotnet261010/p/9248555.html

时间: 2024-10-26 20:08:43

CLR和内存分配的相关文章

CLR 内存分配和垃圾收集

目录 内存分配 垃圾收集 如何分析内存问题 非托管资源 参考文献 注释 NET提供了一个运行时环境 CLR, 负责资源管理(内存分配和垃圾收集),通过垃圾回收器(Garbage Collector)—GC,对内存自动回收. 每当您创建新对象时,CLR都会从托管堆为该对象分配内存. 只要托管堆中有地址空间可用,运行时就会继续为新对象分配空间.但是,内存不是无限大的. 最终,垃圾回收器必须执行回收以释放一些内存. 垃圾回收器优化引擎根据正在进行的分配情况确定执行回收的最佳时间. 当垃圾回收器执行回收

SQL Server ->> Memory Allocation Mechanism and Performance Analysis(内存分配机制与性能分析)之 -- Minimum server memory与Maximum server memory

Minimum server memory与Maximum server memory是SQL Server下配置实例级别最大和最小可用内存(注意不等于物理内存)的服务器配置选项.它们是管理SQL Server内存的途径之一. Minimum server memory与Maximum server memory Minimum server memory(MB): 最小服务器内存.一旦超过这个线就不会再把内存换回去.但是也不是说SQL Server一启动马上就申请这么多的内存. Maximum

C#基础之内存分配

C#基础之内存分配 1.创建一个对象 一个对象的创建过程主要分为内存分配和初始化两个环节.在.NET中CLR管理的内存区域主要有三部分:栈.GC堆.LOH堆,栈主要用来分配值类型数据.它的管理是有系统控制的,而不是像GC堆那样是由GC控制的.当线程执行完值类型实例所在方法后,这块空间将会被自动释放,一般栈的执行效率高不过容量有限.GC堆用来分配小对象实例,它是由GC完全控制内存的分配和回收.LOH堆则是为大对象实例准备的,它不会被压缩且只在GC完全回收时才会回收.在IL中可以看到newobj.l

.NET的堆和栈04,对托管和非托管资源的垃圾回收以及内存分配

在" .NET的堆和栈01,基本概念.值类型内存分配"中,了解了"堆"和"栈"的基本概念,以及值类型的内存分配.我们知道:当执行一个方法的时候,值类型实例会在"栈"上分配内存,而引用类型实例会在"堆"上分配内存,当方法执行完毕,"栈"上的实例由操作系统自动释放,"堆"上的实例由.NET Framework的GC进行回收. 在" .NET的堆和栈02,值类型和

垃圾回收GC:.Net自动内存管理 上(一)内存分配

垃圾回收GC:.Net自动内存管理 上(一)内存分配 前言 .Net下的GC完全解决了开发者跟踪内存使用以及控制释放内存的窘态.然而,你或许想要理解GC是怎么工作的.此系列文章中将会解释内存资源是怎么被合理分配及管理的,并包含非常详细的内在算法描述.同时,还将讨论GC的内存清理流程及什么时清理,怎么样强制清理. 引子 为你的应用程序实现合理的资源管理是一件困难的,乏味的工作.这可能会把你的注意力从你当前正在解决的实际问题中转移到它身上.那么,如果有一个现有的机制为开发者管理令人厌恶的内存管理,会

C#中字符串的内存分配与驻留池

刚开始学习C#的时候,就听说CLR对于String类有一种特别的内存管理机制:有时候,明明声明了两个String类的对象,但是他们偏偏却指向同一个实例.如下: String s1 = "Hello";String s2 = "Hello"; //s2和s1的实际值都是"Hello"bool same = (object) s1 == (object) s2; //这里比较s1.s2是否引用了同一个对象实例 //所以不能写作bool same =

托管堆内存分配优化

内存问题概述 和CPU一样,内存也是一个直接影响服务端性能的重要的硬件资源. 一般来说,如果服务端内存不足,从导致以下两个问题产生: 1. 导致服务端把一些原本要写到内存中的数据,写到硬盘上面.这样不仅仅加大了CPU和磁盘的I/O操作,同时也延长了读取这些数据的时间. 2. 阻止了一些缓存策略的使用. 对于内存不足,一直最快最直接的方式就是去买内存条加在服务器上面.但是这样存在一个隐患的问题就是:如果加了新的内存之后,服务端又面临内存不足的问题,我们不可能无止境的加内存条,那么我们就必须从站点本

垃圾回收GC:.Net自己主动内存管理 上(一)内存分配

垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己主动内存管理 上(三)终结器 前言 .Net下的GC全然攻克了开发人员跟踪内存使用以及控制释放内存的窘态.然而,你也许想要理解GC是怎么工作的.此系列文章中将会解释内存资源是怎么被合理分配及管理的,并包括很具体的内在算法描写叙述. 同一时候,还将讨论GC的内存清理流程及什么时清理.怎么样强制清理.

【c/c++】内存分配大小

测试平台:linux 32位系统 用sizeof()运算符计算分配空间大小.单位:字节 1. 数组名与变量名的区别 int main() { char q[] = "hello"; cout << "q:" << sizeof(q) << endl; char *mq = q; cout << "mq:" << sizeof(mq) << endl; const char *