装箱和拆箱(取消装箱)的概念是类型系统 C# 统一视图的基础,其中任一类型的值都被视为一个对象。
装箱
装箱是将值类型转换为 object 类型 或由此值类型实现的任何接口类型的过程。 当 CLR(公共语言运行库) 对值类型进行装箱时,会将该值包装到 System.Object 内部,再将后者存储在堆(对应于垃圾回收)上(分配并构造一个新对象);装箱是隐式的。
频繁装箱会引发GC(垃圾回收器)开销:
原因:
1.堆内存的分配
- 每次装箱都会在堆上创建一个新的对象,堆内存由GC管理。
- 频繁装箱会快速填充堆内存(尤其是Gen 0代),触发更频繁的垃圾回收。
2.短期对象的激增
- 装箱生产的对象通常是短生命周期的,如方法内部临时生产。这些对象在方法生命周期结束后会变成垃圾,需要GC扫描并回收,增加GC工作量。
3.内存碎片化
- 频繁分配和释放小对象可能会导致内存碎片化,进一步降低GC效率。
装箱实例:
将整型i进行装箱并分配给object
int i = 42;
object o = i;
1:首先从堆中为新生成的引用对象分配内存(大小为值类型实例大小加上一个方法表指针和一个SyncBlockIndex)。
2:然后将值类型的数据拷贝到刚刚分配的内存中。
3:返回堆中新分配对象的地址。这个地址就是一个指向对象的引用了。
进行一次装箱要进行分配内存和拷贝数据这两项比较影响性能的操作。
如何避免装箱引发的GC开销:
- 使用泛型集合: 用List,Dictionary<TKey,TValue>代替ArrayList,Hashtable.
- 避免不必要的类型转换: 减少将值类型赋值给Object或接口类型
- 字符串操作优化:避免多次修改字符串,使用StringBuilder 代替String去进行字符串拼接,因为string是不可变类型,拼接会生成新对象。
- 使用struct实现接口时显示避免装箱:通过泛型约束或接口实现避免装箱:
interface IValue { void Print(); }
struct MyValue : IValue {
public void Print() {}
}
// 正确:通过泛型约束避免装箱
void PrintValue<T>(T value) where T : IValue {
value.Print();
//使用基类作为参数类型
public void PrintValue(IValue value)
{
value.Print();
}
}
// 调用
var val = new MyValue();
PrintValue(val); // 通过分别对有泛型约束和无泛型约束的方法连续调用1000000次后的时间比较得出调用泛型约束方法速度快无装箱,调用无泛型约束方法速度慢有装箱。
关于值类型调用ToString方法时会不会装箱的讨论---参考链接
拆箱(取消装箱)
拆箱(取消装箱)将从对象中提取值类型。 拆箱(取消装箱)是显式的。
o=42;
int i=(int)o;
1、首先获取堆中属于值类型的那部分字段的地址,以确保它是给定值类型的装箱值,这一步是严格意义上的拆箱。
2、将引用对象中的值拷贝到位于栈上的值类型实例中。
经过这2步,可以认为是同装箱是互反操作。严格意义上的拆箱,并不影响性能,但伴随这之后的拷贝数据的操作就会同装箱操作中一样影响性能。
性能
相对于简单的赋值而言,装箱和取消装箱过程需要进行大量的计算。 对值类型进行装箱时,必须分配并构造一个新对象。 取消装箱所需的强制转换也需要进行大量的计算,只是程度较轻。