一、CLR
CLR即公共语言运行时(Common Language Runtime),是中间语言(IL)的运行时环境,负责将编译后的代码编译成计算机可以识别的机器码,负责资源管理(内存分配和垃圾回收等)。
二、内存分配
我们知道C#对象分为值类型和引用类型两种
值类型:char、int 、float、int、datetime、枚举和结构struct等,值存在于栈中。
引用类型:类 (Class) 、String、接口、委托和数组等,声明一个引用类型时,先在栈上分配一个空间等待存储堆中的地址,new出这个对象时才分配堆中的空间,并把堆中的地址赋值给栈。
引用类型分配内存的步骤:
1、new的时候去对象堆里面开辟一块内存,分配一个内存地址。
2、调用构造函数。
3、把堆地址传递给栈。
需要注意的是,引用类型中的变量(比如int)也是跟随引用类型储存于堆中。
对象初始化顺序
在学习内存分配之前,我们先了解声明一个类时,类中的对象初始化顺序。这对于我们后面理解声明对象时入栈、入堆有帮助。
本类中:
静态变量>静态代码块>成员变量>非静态代码块>构造函数
继承父类:
父类静态变量>父类静态代码块>子类静态变量>子类静态代码块>父类成员变量>父类非静态代码块>父类构造函数>子类成员变量>子类非静态代码块>子类构造函数
1. 栈 先进后出
举个列子来看一下实际内存分配
我们先定义一个 值类型 struct
public struct ValuePoint
{
public int x;
public ValuePoint(int x)
{
this.x = x;
}
}
然后在方法里面调用
//先声明变量,没有初始化 但是可以正常赋值 跟类不同
ValuePoint valuePoint;
valuePoint.x = 123;
ValuePoint point = new ValuePoint();
Console.WriteLine(valuePoint.x);
内存分配情况如下
注意:
(1)、值类型分配在栈上面,变量和值都是在栈上面。
(2)、值类型可以先声明变量而不用初始化
2.堆
定义一个引用类型Class
public class ReferencePoint
{
public int x;
public ReferencePoint(int x)
{
this.x = x;
}
}
调用此类
ReferencePoint referencePoint = new ReferencePoint(123);
Console.WriteLine(referencePoint.x);
其内存分配如下:
注意:
(1)、引用类型分配在堆上面,变量在栈上面,值在堆上面。
(2)、引用类型分配内存的步骤:
a、new的时候去对象堆里面开辟一块内存,分配一个内存地址。
b、调用构造函数。
c、把堆地址传给栈保存。
3、复杂类型
1.引用类型嵌套值类型
定义一个类,包括一个全局变量和一个局部变量
public class ReferenceTypeClass
{
private int _valueTypeField;
public ReferenceTypeClass()
{
_valueTypeField = 0;
}
public void Method()
{
int valueTypeLocalVariable = 0;
}
}
包括全局变量_valueTypeField和一个值类型的局部变量valueTypeLocalVariabl,那这种情况下内存是如何分配的呢?
值类型不应该是都分配在栈上面吗?为什么一个是分配在堆上面,一个是分配在栈上面呢?
_valueTypeField分配在堆上面比较好理解,因为引用类型是在堆上面分配了一整块内存,引用类型里面的属性也是在堆上面分配内存。
valueTypeLocalVariable分配在栈上面是因为valueTypeLocalVariable是一个全新的局部变量,调用方法的时候,会启用一个线程去调用,线程栈来调用方法,然后把局部变量分配到栈上面。
2.值类型嵌套引用类型
public struct ValueTypeStruct
{
private object _referenceTypeField;
public ValueTypeStruct(int x)
{
_referenceTypeField = new object();
}
public void Method()
{
object referenceTypeLocalVariable = new object();
}
}
从上面的截图中可以看出:值类型里面的引用类型的变量分配在栈上,值分配在堆上。
我们看一个比较特殊的引用类型:String
string student = "大山";
内存分配如下
然后在声明一个变量student2,然后用student给student2赋值:
string student2 = student;
这个时候内存分配情况如下:
student2被student赋值的时候,是在栈上面复制一份student的引用给student2,然后student和student2都是指向堆上面的同一块内存。
然后我们修改一下student2的值
student2 = "App";
输出student和student2的值:
1 Console.WriteLine("student的值是:" + student); 2 Console.WriteLine("student2的值是:"+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在分配内存的时候,会查找是否有相同的值,如果有相同的值,就重用;如果没有,这时在重新开辟一块内存。
但是其他引用类型和string正好相反。
定义一个类
1 public class Refence
2 {
3 public int Value { get; set; }
4 }
调用这个类
1 Refence r1 = new Refence();
2 r1.Value = 30;
3 Refence r2 = r1;
4 Console.WriteLine($"r2.Value的值:{r2.Value}");
5 r2.Value = 50;
6 Console.WriteLine($"r1.Value的值:{r1.Value}");
7 Console.ReadKey();
从运行结果可以看出,如果是普通的引用类型,如果修改其他一个实例的值,那么另一个实例的值也会改变。正好与string类型相反。
学习好内存分配原则更有利于我们写出高质量的代码,持续学习,持续仅不~~