图解C#值类型与引用类型的内存分配情况 |
您所在的位置:网站首页 › 储存内存大小的区别 › 图解C#值类型与引用类型的内存分配情况 |
文章目录
前言一、类型系统之值类型和引用类型1.值类型与引用类型的派生关系2.值类型与引用类型的主要区别3.值类型和引用类型的使用场合
二、内存的逻辑划分之栈和堆1.栈的特征2.栈的结构3.堆的特征4.堆的结构
三、代码运行时的内存分配情况1.变量和对象在内存中的分配2.方法参数在栈中的分配3.特殊的引用类型“System.String”
总结
前言
关于C#的值类型和引用类型,在面试中经常被问到的问题就是:值类型与引用类型的内存分配有哪些区别,大多数同学只能回答到值类型存储在栈上,引用类型存储在堆上。具体是如何分配的,栈和堆的结构特征是怎样的,了解的较少。今天我们通过这篇文章,来彻底搞懂C#值类型与引用类型的内存分配情况。 一、类型系统之值类型和引用类型C#的类型一共分为两类,一种是值类型(Value Type),一类是引用类型(Reference Type)。
值类型:在内存管理方面具有更好的效率,但不支持多态,不能派生新的类型,适合用做存储数据的载体; 引用类型:支持多态,可以派生新的类型,适合用于定义应用程序的行为。 二、内存的逻辑划分之栈和堆C#程序在CLR上运行时,内存从逻辑上划分两大块:栈、堆,这两个基本元素组成了C#程序的运行环境。 栈,在程序运行的时候,每个线程(Thread)都会维护一个自己的专属线程堆栈。把它想像成叠在一起的盒子(像搭积木一样)。每一次调用一个方法就会在最上面叠一个盒子,用来跟踪程序运行情况。我们只能使用栈中叠在最上面的盒子里的东西。当最上面的盒子里的代码执行完毕(如方法执行完成),就把它扔掉并继续去使用下一个盒子。 堆,是程序在运行的时候请求操作系统分配给自己的内存空间,可以想象成一个仓库,储存着我们使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉。 ![]() ![]() ![]() ![]() ![]() 参考链接:CLR如何创建运行时对象 三、代码运行时的内存分配情况下面我们以实际的代码运行过程为例,详细介绍C#代码在运行时的内存分配情况。 1.变量和对象在内存中的分配示例代码: class TestClass { public int x; public static string y; } void Test1() { var a=1; var b=new TestClass(); var c=a; var d=b; var e=d.x; var f=TestClass.y; }内存分配情况 Test1()方法被调用时,系统为该方法创建一个栈桢,用于存储该方法使用到的值类型的变量、指针、调用其他方法的返回地址等; 方法执行到 var a=1 时,首先入栈,变量a的值1存储在栈中,栈的起始地址为0x000000671b77e5a4; 方法执行到 var b=new TestClass() 时,会在堆中开辟一块儿内存用于存储TestClass实例对象,然后变量b入栈,变量b的值为TestClass实例对象的引用(实际上存储的是TestClass实例在堆上的内存地址,也就是指针); 方法执行到 var c=a 时,将变量c压入栈,因为a是值类型,所以对变量a的值进行拷贝赋值给c; 方法执行到 var d=b 时,将变量d压入栈,因为b是引用类型,所以将变量b引用的地址赋值给变量d,仍然指向堆内存中的TestClass实例对象; 方法执行到 var e=d.x 时,将变量e压入栈,因为x字段是值类型,所以将x的实际值0(int类型初始化的默认值为0)赋值给e; 方法执行到 var f=TestClass.y 时,将变量f压入栈,因为y字段是引用类型,所以f变量的值为y字段的引用。 2.方法参数在栈中的分配示例代码: class TestClass { public int x; public int sum(int i,int j){ return i+j; } } void Test1() { var a=new TestClass(); int b = 0; b=a.sum(1,2); }内存分配情况: 方法执行到 var a=new TestClass() ,会在堆中开辟一块儿内存用于存储TestClass实例对象,然后变量a入栈,变量a的值为TestClass实例对象的引用(实际上存储的是TestClass实例在堆上的内存地址,也就是指针); 方法执行到 int b = 0 ,将局部变量b压入栈,因为b是值类型,所以值0存储在栈中; 方法执行到 b=a.sum(1,2) ,首先两个int类型实参1,2分别入栈,并将sum方法的返回地址压入栈,sum方法执行结束之后应返回至该位置。 3.特殊的引用类型“System.String”特性一:字符串是不可变的,字符串一经创建便不能更改,不能变长、变短或修改其中的任何字符。 特性二:字符串驻留(字符串池化),CLR可通过一个String对象共享多个完全一致的String内容,这样能减少系统中字符串的数量,从而节省内存。String的驻留机制实际上是在SystemDomain中进行的。 当CLR被加载之后,会在SystemDomain对应的managed heap中创建一个Hashtable,Hashtable中记录了所有在代码中使用字面量声明的字符串实例的引用,Hashtable的Key为字符串本身,Value为字符串对象的地址。 示例代码: static void Main(string[] args) { //申请一块堆内存,把地址放在Hashtable的key为hello的元素中 string str1 = "hello"; //由于上一句已经创建了key为hello的元素,所以不需要申请新的堆内存 string str2 = "hello"; //编译成MSIL语言时 已经与string str3 = "hello"一样了 string str3 = "" + "e" + "l" + "l" + "o"; //自己显示进行new string str4 = new string(new char[] { 'h', 'e', 'l', 'l', 'o' }); //申请一块堆内存,把地址放在Hashtable的key为hello2的元素中 string str5 = "hello2"; //True 引用同一块堆内存 Console.WriteLine(object.ReferenceEquals(str1, str2).ToString()); //True 也是引用同一块堆内存 Console.WriteLine(object.ReferenceEquals(str1, str3).ToString()); //False 引用了不同的堆内存 Console.WriteLine(object.ReferenceEquals(str1, str4).ToString()); // 先从Hashtable中检索是否有重复的key ,检索到了hello2,所以不需要申请新的堆内存 str2 = "hello2"; //False str2与str1已经不引用同一个堆 Console.WriteLine(object.ReferenceEquals(str1, str2).ToString()); //True 变成与str5引用同一个堆内存 Console.WriteLine(object.ReferenceEquals(str2, str5).ToString()); // 控制台输入两个相同的字符串 str1 = Console.ReadLine(); str2 = Console.ReadLine(); //False 因为 str1 和 str2 两个变量并非字面量声明的字符串,所以不会触发字符串驻留机制 Console.WriteLine(object.ReferenceEquals(str1, str2).ToString()); Console.ReadLine(); } 总结不理解值类型和引用类型区别的同学,可能会给代码引入诡异的bug或性能问题,通过这篇文章讲解了值类型与引用类型的区别,栈与堆的结构特征,代码运行时的内存分配情况,并配以示例代码及相关图形说明,尽可能形象的讲解了值类型与引用类型,希望能给大家带来帮助。 |
今日新闻 |
点击排行 |
|
推荐新闻 |
图片新闻 |
|
专题文章 |
CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭 |