int i = 4;2
object o = i;3
object o2 = o;4
Console.WriteLine(ReferenceEquals(o, o2)); //true5
o = 8;6
Console.WriteLine(ReferenceEquals(o, o2)); //false7 8 Console.WriteLine("i={0}, o={1}, o2={2}", i, o, o2); //
输出 4, 8, 4
IL过程
1 IL_0001: ldc.i4.4
2 IL_0002: stloc.0
3 IL_0003: ldloc.0
4 IL_0004: box [mscorlib]System.Int32
5 IL_0009: stloc.1
6 IL_000a: ldloc.1
7 IL_000b: stloc.2
8 IL_000c: ldc.i4.8
9 IL_000d: box [mscorlib]System.Int32
10 IL_0012: stloc.1
1.Ldc.I4.4
Pushes the integer value of 4 onto the evaluation stack as an int32.
将整数值 4 作为 int32 推送到计算堆栈上。(如果整数值是5则为Ldc.l4.5)
EvaluationStack为4(入栈)
2.Stloc.0
Pops the current value from the top of the evaluation stack and stores
it in the local variable list at index 0.
从计算堆栈的顶部弹出当前值并将其存储到索引 0处的局部变量列表中。
EvaluationStack为空(出栈)
此时List栈[0]为4
3.Ldloc.0
Loads the local variable at index 0 onto the evaluation stack.
将索引 0处的局部变量加载到计算堆栈上。
EvaluationStack为4(入栈)
4.Box
Converts a value type to an object reference (type O).
将值类转换为对象引用(O类型)。
将值类型转为为引用类型,装箱(object o = i)
装箱在值类型向引用类型转换时发生
拆箱在引用类型向值类型转换时发生
Box:执行IL box指令,在内存堆中申请System.Int32类型需要的堆空间
众所周知常量是值类型,值类型是要放在栈上的,而object是引用类型,它需要放在堆上;要把值类型放在堆上就需要执行一次装箱操作。
5.Stloc.1
Pops the current value from the top of the evaluation stack and stores
it in the local variable list at index 1.
从计算堆栈的顶部弹出当前值并将其存储到索引 1处的局部变量列表中。
EvaluationStack为空(出栈)
List[1]=EvaluationStack4,即栈List[1]=4
6.Ldloc.1
Loads the local variable at index 1 onto the evaluation stack.
将索引 1 处的局部变量加载到计算堆栈上。
低——高
EvaluationStack 4(List[1]=4)
7.Stloc.2
Pops the current value from the top of the evaluation stack and stores it in the local variable list at index 2.
从计算堆栈的顶部弹出当前值并将其存储到索引 2 处的局部变量列表中。
出栈
EvaluationStack为空
List[2]=4
8.Ldc.I4.8
Pushes the integer value of 8 onto the evaluation stack as an int32.
将整数值 8 作为 int32 推送到计算堆栈上。
EvaluationStack为8
9.Box
Converts a value type to an object reference (type O).
将值类转换为对象引用(O类型)。
将值类型引用为引用类型 (o = 8)
10.Stloc.1
Pops the current value from the top of the evaluation stack and stores
it in the local variable list at index 1.
从计算堆栈的顶部弹出当前值并将其存储到索引 1处的局部变量列表中。
EvaluationStack为空
List[1]=8 o在栈中存储的值变为了8
但是List[2]存的仍然是4
这样看来就比较清楚了,o2和o的地址明显不一样,因为o=8中有一个对8的装箱操作,之后仅修改了第二个内存变量中保存的地址,而第三个内存变量中保存的仍然是上次装箱4时的地址,所以o=8, o2=4
顺便说一下string吧
string是不可变的,也就是说,一个string对象创建以后,如果修改它的内容,就会创建一个新的string对象,换句话说,地址变了。《CLR
VIA C#》中的解释是string pooling,所有内容相同的字符串对象都会指向同一个metadata中的同一个string对象。所以下面的这段代码
String s1 = "Hello";
String s2 = "Hello";
Console.WriteLine(Object.ReferenceEquals(s1, s2));
结果是True
执行装箱操作时不可避免的要在堆上申请内存空间,并将堆栈上的值类型数据复制到申请的堆内存空间上,这肯定是要消耗内存和cpu资源的。
在来看拆箱的IL指令
object objValue = 4;
int value = (int)objValue;
上面的两行代码会执行一次装箱操作将整形数字常量4装箱成引用类型object变量objValue;然后又执行一次拆箱操作,将存储到堆上的引用变量objValue存储到局部整形值类型变量value中。
.locals init (
[0] object objValue,
[1] int32 'value'
) //上面IL声明两个局部变量object类型的objValue和int32类型的value变量
IL_0000: nop
IL_0001: ldc.i4.4 //将整型数字4压入栈
IL_0002: box [mscorlib]System.Int32 //执行IL box指令,在内存堆中申请System.Int32类型需要的堆空间
IL_0007: stloc.0 //弹出堆栈上的变量,将它存储到索引为0的局部变量中
IL_0008: ldloc.0//将索引为0的局部变量(即objValue变量)压入栈
IL_0009: unbox.any [mscorlib]System.Int32 //执行IL 拆箱指令unbox.any 将引用类型object转换成System.Int32类型
IL_000e: stloc.1 //将栈上的数据存储到索引为1的局部变量即value
拆箱操作的执行过程和装箱操作过程正好相反,是将存储在堆上的引用类型值转换为值类型并给值类型变量。
装箱操作和拆箱操作是要额外耗费cpu和内存资源的,所以在c# 2.0之后引入了泛型来减少装箱操作和拆箱操作消耗。
int的一点事,读《深入C#内存管理来分析值类型&引用类型,装箱&拆箱,堆栈几个概念组合之间的区别》----kofkyo
使用泛型和不使用泛型情况下的装箱拆箱情况:
使用非泛型集合时引发的装箱和拆箱操作:
var array = new ArrayList();
array.Add(1);
array.Add(2);
foreach (int value in array)
{
Console.WriteLine(“value is {0}”,value);
}
代码声明了一个ArrayList对象,向ArrayList中添加两个数字1,2;然后使用foreach将ArrayList中的元素打印到控制台。
在这个过程中会发生两次装箱操作和两次拆箱操作,在向ArrayList中添加int类型元素时会发生装箱,在使用foreach枚举ArrayList中的int类型元素时会发生拆箱操作,将object类型转换成int类型,在执行到Console.WriteLine时,还会执行两次的装箱操作;这一段代码执行了6次的装箱和拆箱操作;如果ArrayList的元素个数很多,执行装箱拆箱的操作会更多。
你可以通过使用ILSpy之类的工具查看IL代码的box,unbox指令查看装箱和拆箱的过程
使用泛型集合的情况
var list = new List<int>();
list.Add(1);
list.Add(2);
foreach (int value in list)
{
Console.WriteLine("value is {0}", value);
}
代码和1中的代码的差别在于集合的类型使用了泛型的List,而非ArrayList;我们同样可以通过查看IL代码查看装箱拆箱的情况,上述代码只会在Console.WriteLine()方法时执行2次装箱操作,不需要拆箱操作。
可以看出泛型可以避免装箱拆箱带来的不必要的性能消耗;当然泛型的好处不止于此,泛型还可以增加程序的可读性,使程序更容易被复用等等。
当我们用到值类型变引用类型,就要想到进行了装箱操作,耗费了性能。引用类型变为值类型,便进行了拆箱操作。
所以要尽量避免这样的使用。
线程,进程和协程的区别
A:
线程:线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度。
进程:进程拥有自己独立的栈和堆,既不共享堆,也不共享栈,进程由操作系统调度。
协程:协程和线程一样共享堆,不共享栈,协程由程序员在在协程的代码里显示调度。
谈谈C#中的内存分配
A:
值类型的分配:虚拟内存中存在一个叫堆栈的区域,我们并不知道它到底在地址空间的什么地方,在一般开发过程中也没有必要知道,我们知道的是值类型就分配于此。值类型在堆栈上分配的时候,是自上而下填充的,也就是从高内存地址开始填充。
比如当前的堆栈指针为100000,这表明它的下一个自由存储空间从99999开始,当我们在C#中声明一个int类型的变量A,因为int类型是四个字节,所以它将分配在99996到99999这个存储单元中。如果我们接着声明double变量B(8字节),该变量将分配在99988到99995这个存储单元。 如果代码运行到他们的作用域之外,这时候A和B两个变量都将被删除,此时的顺序正好相反,先删除变量B,同时堆栈指针会递增8,也就是重新指向到99996这个位置;接下来删除变量A,堆栈指针重新指向10000。如果两个变量是同时声明的。如int A,B,此时我们并不知道A和B的分配顺序,但是编译器会确保他们的删除顺序正好和分配顺序相反。
引用类型的分配:了解堆栈上的分配方式之后,很明显,它的性能相当高,同时我们发现了它的一个缺点:变量的生存期必须嵌套,这对于某些情况是无法接受的,有时候我们你需要存储一些数据并且在方法退出后仍然能保证这部分数据是可用的。为此,虚拟内存另外分配了一部份区域,我们称之为托管堆。托管堆和传统的堆很大的一个不同点在于,托管堆在垃圾收集器的控制下进行工作。引用类就分配在托管堆上,下面我们来看看引用类型的分配过程,
假设我们需要声明一个Person类并对它进行实例化。
Person p=new Person();
首先,系统会在堆栈上给p这个变量在堆栈上分配存储空间,当然它只是一个引用而已,用来存放Person实例在托管堆上的位置,并没有存放真正的Person实例。因为它仅仅是存放一个地址(一个整数值),所以它将在堆栈上占据4个字节的空间。接下来Person实例将会被存放在托管堆上。和堆栈不同,托管堆是由下往上分配的,假设这个实例需要占据10个字节,假设托管堆上的地址为20000,那么它将分配到20000到20009这个存储单元。
需要注意的是,这个分配和实例的大小有关,如果实例小于85000字节,它会被分配在托管堆上。如果超过了85000字节,它将会被分配在LOH(大对象堆)上。这个分配过程比值类型的分配方式更为复杂,因此也就不可避免地有性能方面的损耗。这又是为什么对于小数据量的数据结构我们更愿意使用结构体而不是类。
当然这些是比较单纯的分配方式,实际情况可能比这个复杂。比如Struct里定义一个类实例。该类实例会被分配在托管堆,而它的地址分配在堆栈上。如果类中定义了一个值类型int变量,那么该变量的值会被分配在托管堆上而不是堆栈。