Unity GC优化学习(一):认识堆(heap)&栈(stack)

发表于2018-06-07
评论0 3.5k浏览
尽管在.NET framework 下我们并不需要担心内存管理和垃圾回收(GarbageCollection),但是我们还是应该了解它们,以优化我们的应用程序。同时还需要具备一些基础的内存管理工作机制的知识,这样有助于解释日常程序编写中的变量的行为。

本文就给大家介绍下堆和栈的基本知识,变量类型以及为什么一些变量能够按照它们自己的方式工作。

在.NET framework环境下,当我们执行代码时,内存中有两个地方用来存储这些代码:堆和栈。

一、堆和栈有什么不同?
【申请速度】
·堆的申请速度较慢,容易产生内存碎片,但是用起来比较方便。
·而栈的申请速度较快,但却不受程序员的控制。
【申请大小】
·栈申请的容量较小1-2M,堆申请大小虽受限于系统中有效的虚拟内存,但较大
【存储内容】
·栈负责保存我们代码执行或调用的路径,而堆则负责保存对象或者说数据的路径。

可以将栈想象成一堆从顶向下堆叠的盒子。每当调用一次方法是,我们应将应用程序中所要发生的事情记录在栈顶的一个盒子中。而我们每次只能够使用栈顶的那个盒子。当我们栈顶的盒子被使用完之后,或者方法执行完毕之后,我们将抛开这个盒子然后继续使用栈顶上的新盒子。

堆的工作原理比较相似,但大多数时候堆被用作保存信息而非保存执行路径,因此能够在任意时间被访问。
与栈相比堆没有任何访问限制,堆就像一个仓库,存储着我们使用的各种对象等信息,而栈就像储物柜里堆叠的鞋盒,我们只能从最顶层的盒子开始取, 直到发现那只最适合的。
和栈不同的是,堆中存储的信息在被调用完毕不会立即被清理掉。

栈是自行维护的,也就是说内存自动维护栈,当栈顶的内容不再被使用,该内容将会被跑出。相反,堆需要考虑垃圾回收。

二、堆和栈里有些什么?

当我们的代码执行的时候,堆和栈中主要放置了四种类型的数据:
值类型(Value Type)
引用类型(Reference Type)
指针(Pointer)
指令(Instruction)

1.值类型
在C#中,所有被声明为一下类型的事物被称为值类型:
bool
byte
char
decimal
double
enum
float
int
long
sbyte
struct
unit
ulong
ushort

2.引用类型:
所有被声明为一下类型的事物被称为引用类型
class
interface
delegate
object
string
StringBuilder

3.指针
在内存管理方案中放置的第三种类型就是类型引用,引用通常就是一个指针。指针或引用是不同于引用类型的,这是因为当我们说某个事物是一个引用类型时就意味着我们是通过指针来访问它的。指针是一块内存空间,而它指向另一个内存空间。

大白话:
对于引用类型而言,其内存地址(指针)存放在栈上
其实际内容则由栈上的地址索引存储在堆上

4.指令
如何决定放哪?这里有一条黄金规则:
引用类型总是放在堆中。
值类型和指针总是放在他们被声明的地方。
就像前面提到的那样,栈是负责保存我们的代码执行或调用时的路径。当我们的代码开始调用一个方法时,将放置一段编码指令(在方法中)到栈上,紧接着放置方法的参数,然后代码执行到方法中被“压栈”至栈顶的变量位置。通过以下的例子很容易理解:

下面是一个方法:
public int PlusSix(int paramValue)
{
    int result;
    result = paramValue + 6;
    return result;
}

现在就来看看在栈顶发生了些神马。

首先方法入栈(只包含需要执行的逻辑字节,即执行该方法的指令,而非方法体内的数据),紧接着是方法的参数入栈。

接着,控制(即执行方法的线程)被传递到堆栈中PlusSix()的指令上,

当方法执行时,我们需要在栈上为“result”变量分配一些内存,

方法执行完成,然后方法的结果被返回。

通过将指针指向PlusSix()方法曾使用的可用的内存地址,所有栈上的该该方法所使用的内存都被清空,且程序将自动回到栈上最初的方法调用的位置(本例中不会看到)。

在这个例子中,我们的“result”变量是被放置在栈上的,事实上,当值类型数据在方法体重被声明时,它们都是被放置在栈上的。
值类型数据有时也被放置在堆上。
记住这条规则——值类型总是放在它们被声明的地方。好滴,如果一个值类型数据在方法体外被声明,而且存在于一个引用类型中,那么他将被堆中的引用类型所取代。

来看另外一个例子:

假如我们有这样一个MyInt类:
public class MyInt
{
    public int MyValue;
}

然后执行下面的方法:
public MyInt PlusSix(int praramValue)
{
    MyInt result = new MyInt();
    result.MyValue = praramValue + 6;
    return result;
}

就像前面提到的,方法以及方法的参数被放置到栈上,接下来,控制被传递到堆栈中的PlusSix()的指令上。

接着会出现一些有趣的现象……
因为“MyInt”是一个引用类型,它将被放置在堆上,同时在栈上生成一个指向这个堆的指针引用。

在PlusSix()方法执行之后,我们将清空刚刚使用的栈顶部分。

我们将剩下孤独的MyInt对象在堆中(栈中将不会存在任何指向 MyInt对象 的指针!)

这就是垃圾回收器(后简称GC)起作用的地方。
当我们的程序达到了一个特定的内存阈值,我们需要更多的堆空间的时候,GC开始起作用。
GC将停止所有正在运行的线程,找出堆中存在的所有不再被主程序访问的对象,并删除它们。

然后GC会重新组织堆中所有剩下的对象来节省空间,并调整栈和堆中所有与这些对象相关的指针。
你肯定会想到这个过程非常耗费性能,所以这是你就会知道为什么我们需要如此重视栈和堆里有些什么,特别是在需要编写提高性能的代码是。

那么,它是如何影响我的?
当我们使用引用类型时,我们实际上是在处理该类型的指针,而非该类型本身。当我们使用值类型是,我们是在使用值类型本身。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引