我们都知道C#编程语言中,数据类型被分为了两种:
- 值类型
- 引用类型
那么什么是值类型?什么是引用类型呢?它们的区别又是什么?
为了搞清楚这些问题,我们先列举一下我们开发中会碰到的值类型和引用类型。
- 常见的值类型为:byte,short,int,long,float,double,decimal,char,bool 和 struct
- 常见的引用类型为:class array interface delegate string System.Object dynamic
为了更清晰的对比值类型和引用类型,我们从内存的分配和回收两个角度来进行分析
内存分配
我们都知道,创建对象就需要有一块内存来承载相应的对象,我们也知道,在程序运行过程中内存会分为栈内存和堆内存,那么到底我们的值类型和引用类型的内存是分配在哪块内存上了呢?
要搞清楚这个问题首先我们先要了解值类型和堆内存,内存分配的差异:
-
值类型只需要一段单独的内存,用于存储实际的数据
-
引用类型需要存储两段内存
- 第一段存储实际的数据。
- 第二段存储的是一个引用,指向实际数据的存放位置。
其实很多熟悉开发的小伙伴都口熟能详的知道一个概念,“值类型被存储在内存栈上,引用类型被存储在内存堆上”。这句话对不对呢。继续往下看…
其实这句话呢,也对也不对,但要分使用场景数据不是其它类型的成员的情况下“值类型被存储在内存栈上,引用类型被存储在内存堆上”,大致如下图所示:
但我们实际开发中很多时候类型都不是单独存在的,看下面的一段代码:
public class ClassA{
public int a;
public string b;
}
看到代码是不是感觉 a内存分配在内存栈,b内存分配在内存堆。很遗憾,实际情况是a和b都被分配在内存堆中。
那么哪个环节出现问题了呢?
因为a是ClassA的成员属性,而ClassA是一个引用类型,所以ClassA的数据部分是被存储在内存堆上的,大致如下图所示:
总结:
引用类型的数据一定是被分配在内存堆上的,而引用类型的引用以及值类型的数据却并不一定分配在内存栈上。
- 局部变量:
引用类型的引用和值类型的数据分配在内存栈上
- 公共变量
引用类型的引用和值类型的数据的分配根承载它的对象所在的内存有关,如果承载它的对象在堆内存中那么它就跟着被分在堆内存中,如果承载它的对象被分配在栈内存中那么它就跟着被分在栈内存中。
内存回收
在说回收之前我们需要先了解一下栈内存和堆内存的定义和结构如下:
栈内存
栈是一个内存数组,是一个LIFO(last-in first-out,后进先出)的数据结构。栈存储几种类型的数据:某些类型变量的值、程序当前的执行环境、传递给方法的参数。
栈的特点:(1)数据只能从栈的顶端插入和删除。(2)把数据放到栈顶称为入栈。(3)从栈顶删除数据称为出栈。(4)内存连续 (5)内存自行维护
堆内存
堆是一块内存区域,在堆里可以分配大块的内存用于存储某种类型的数据对象。与栈不同,堆里的内存能够以任意顺序存入和移除。
堆的特点: (1)内存无序。 (2)内存不可自行维护需要借助CLR的GC机制
由于栈内存的内存连续性以及内存的自行维护,所以栈内存的申请和释放相对于堆内存要快。
而堆内存的内存回收完全借助于CLR的GC机制,什么时候回收几乎是不可控的,且由于堆内存的不连续性的特点,在GC之后容易产生内存碎片,从而造成内存浪费。
什么时候触发GC?
- 在堆内存上进行内存分配操作时,内存不够的时候会触发GC
- 自动触发,Unity会不定时的自动触发GC
- 代码强制执行
GC是怎样工作的?
- 挂起所有正在运行的线程
- 检查堆内存上的每个对象
- 搜索对象的所有引用
- 没有被引用的对象都是垃圾,被标记为可删除
- 遍历删除所有被标记的对象,释放内存
看到GC的工作机制你就会知道一次GC是多么的困难,并且随着我们的程序复杂性的提高,占用的CPU算力也会越高就会造成程序卡顿,所以在项目开发过程中我们一定要想办法减少GC,或选择在合适的位置进行GC
怎么减少GC?
- 对象池
- stringbuilder的使用
- 减少装箱操作
- 避免频繁的调用协程,每一次StartCoroutine()实际上是new一个新的对象
- 用for代替foreach foreach会在堆上产生一个system.object
- …
最后我们简单用一个示例图来模拟一下一个引用类型的申请: