.NET中C#堆VS栈:Part I

本文深入探讨了.NET Framework中堆和栈的基本概念,解释了它们如何影响变量行为,并提供了实例来说明值类型和引用类型的存储位置及其对程序性能的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

尽管在.NET framework中我们不必担心内存管理和垃圾回收(GC),但是我们仍然要关注内存管理和GC,以优化程序性能。并且,对内存管理的工作原理有一个基本的了解,可以帮助我们解释程序中变量的行为。这篇文章中,会带来堆和栈的基础知识,变量类型以及一些变量为什么是这样的。

当你的代码运行时,.NET framework有两个地方用于存储。如果你对它们还不熟悉,我会为你介绍堆和栈。堆和栈在执行代码时都有用处。它们驻留在机器的内存中,并且包含程序执行需要的信息。

栈VS堆:有什么区别?

栈或多或少负责追踪我们的代码在执行什么(或者什么被调用了)。堆负责追踪我们用到的对象(数据,嗯。。。大多数是这样;稍后会说到它)。

想象一下栈是一堆连续堆放的一个压着一个的箱子。每当我们调用函数时,我们都把程序要发生的事情记录在一个盒子里放在最顶端。当最顶端的事完成了(函数执行结束),就将它抛弃,并继续使用之前放在栈顶的盒子中的东西。在堆中,就没有像栈这样的存取限制。堆就像在床上一堆洗好的衣服还没来得及收拾;我们可以快速拿到想要的东西。栈就像衣柜中的一摞鞋盒,必须取下最顶上的才能拿到下面的。

上面这张图片,不代表内存中真实发生的事情,只是帮助我们区分堆和栈。

堆和栈里都有什么?

在程序执行过程中,有4种主要类型的东西会放到堆和栈里:值类型,引用类型,指针和指令。

值类型:

  • bool
  • byte
  • char
  • decimal
  • double
  • enum
  • float
  • int
  • long
  • sbyte
  • short
  • struct
  • uint
  • ulong
  • ushort

引用类型:

所有使用下面列表中声明的东西都是引用类型(从System.Object继承的,除此之外,当然也包括System.Object类型的对象):

  • class
  • interface
  • delegate
  • object
  • string

指针:

第三种被放到内存管理计划的是类型的引用。引用通常被归类到指针。我们并没有明确的使用指针,它们被CLR管理。当我们说某个东西是引用类型时,它和指针是不同的,它意味着通过指针访问引用类型。指针是内存当中的一块地方,它指向了内存的另一块空间。指针与我们放入堆和栈的其他东西一样使用空间,它的值是一个内存地址或者是空。

指令:

稍后你会看到指令是怎么工作的。。。

怎么决定放在哪?

这里有两个黄金法则:

1. 引用类型永远放在堆中。

2. 值类型和指针永远放在声明它们的地方。这个地方有一点小复杂,并且需要理解一点栈的工作方式才能理解这些东西被声明在哪。

栈,就像之前提到的,栈负责跟踪代码执行时每个线程的位置。你可以把它想象成是每个线程的状态,并且每个线程有它自己的栈空间。当代码调用一个函数时,所在线程开始执行被JIT编译并寄存在方法表中的指令,同时也将函数参数压入执行线程的栈。接下来,当执行到函数内部的变量时,它们被压入栈顶。下面的例子会帮助理解。

执行下面函数:

public int AddFive(int pValue)
{
    int result;
    result = pValue + 5;
    return result;
}

这里正好是栈顶发生的事情。记住,我们看到的栈,实际上在栈顶已经存在很多其他的东西了。

当我们开始执行函数,函数参数首先被入栈(之后会谈到参数传递)。

注意:函数并不在栈上,只是一个引用。

接下来,控制权交给函数表中的AddFive()函数中的指令,如果这个函数是首次执行会触发JIT编译器。

函数执行,我们需要一些内存来存放执行结果变量,这些内存分配在栈上。

函数执行结束,返回执行结果。

所有在栈上分配的内存清空,方式是将内存地址指针移动到AddFive()函数开始的地方,在栈上继续执行调用AddFive()函数之前的方法。

在这个例子中,结果变量存放在栈上。一个重要的事实是,在函数体内部定义的值类型变量都在栈上。当然,值类型有时同样也会在堆上。记住规则,值类型永远在它被声明的地方。如果一个值类型在方法外部声明,但是在一个引用类型内部,那么它会随着引用类型被分配到堆上。

下面是另一个例子。

有一个MyInt类(一个引用类型)

public class MyInt
{          
    public int MyValue;
}

执行下面的函数:

public MyInt AddFive(int pValue)
{
    MyInt result = new MyInt();
    result.MyValue = pValue + 5;
    return result;
}

就像上一个例子一样,线程开始执行函数并将参数入栈。

下面就有意思了。

因为MyInt是一个引用类型,它被分配在堆上,在栈上有一个指针指向它。

在AddFive()执行结束后,开始清理栈

我们把堆中的MyInt对象变成了孤儿(在栈中没有指针指向MyInt)

这就该GC登场了。一旦程序到达某一内存阈值并且需要更多的堆空间,GC开始清理。GC会停止所有运行的线程(是完全停止),在堆中找到所有不再需要的对象并删除它们。GC会重新整理对象以腾出更多的空间,同时调整栈中对象的引用指针。你可以想象,这对性能的代价非常大,所以,这就是为什么注意堆和栈的使用对编写高性能的代码这么重要。

这对我有什么影响?

当我们使用引用类型时,我们实际上是通过指针访问它,而不是它本身。当使用值类型时,使用的是它本身。

下面是个很好的描述例子

执行下面函数:

public int ReturnValue()

{
    int x = new int();
    x = 3;
    int y = new int();
    y = x;
    y = 4;
    return x;
}

我们会得到结果3

然而,如果我们使用之前定义的MyInt这个类

public class MyInt
{
    public int MyValue;
}

并且执行下面的函数

public int ReturnValue2()
{
    MyInt x = new MyInt();
    x.MyValue = 3;
    MyInt y = new MyInt();
    y = x;
    y.MyValue = 4;
    return x.MyValue;
}

我们会得到什么结果,4!

在第一个例子中,和预想的一样

public int ReturnValue()
{
    int x = 3;
    int y = x;
    y = 4;
    return x;
}

在第二个例子里,没有得到3,因为x和y变量同时指向了同一个堆中的对象

public int ReturnValue2()
{
    MyInt x;
    x.MyValue = 3;
    MyInt y;
    y = x;
    y.MyValue = 4;
    return x.MyValue;
}

原文链接:

https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-i/

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值