65.9K
CodeProject 正在变化。 阅读更多。
Home

.Net中的内存分配 – 值类型、引用类型、堆栈、堆、装箱、拆箱、Ref、Out和Volatile

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (30投票s)

2015年11月22日

CPOL

7分钟阅读

viewsIcon

80336

本文讨论了.Net中的内存分配以及JIT编译器如何优化非易失性代码。它还讨论了值类型、引用类型、堆栈、堆、装箱、拆箱、Ref、Out和Volatile。

引言

在本文中,我将详细讨论类型和引用类型以及它们在堆栈上的内存分配。然后,我将讨论Volatile关键字的用法。

目录

在深入研究内存分配和其他细节之前,让我们先看看内置数据类型

内置数据类型

数据类型

Range

数据类型

byte

0 .. 255

值类型

sbyte

-128 .. 127

值类型

short

-32,768 .. 32,767

值类型

ushort

0 .. 65,535

值类型

int

-2,147,483,648 .. 2,147,483,647

值类型

uint

0 .. 4,294,967,295

值类型

long

-9,223,372,036,854,775,808 .. 9,223,372,036,854,775,807

值类型

ulong

0 .. 18,446,744,073,709,551,615

值类型

float

-3.402823e38 .. 3.402823e38

值类型

double

-1.79769313486232e308 .. 1.79769313486232e308

值类型

decimal

-79228162514264337593543950335 .. 79228162514264337593543950335

值类型

char

Unicode字符。

值类型

字符串

Unicode字符字符串。

引用类型

bool

真或假。

值类型

object

一个对象。

引用类型

 

值类型和引用类型

在.Net中,我们有2种数据类型:类型和引用类型。了解CLR如何管理数据和内存对于编写优化代码以提高性能至关重要。

上面表格中给出的所有内置数据类型,当用于在函数中声明变量或作为参数传递时(非ref传递),它将是值类型,除了string和object数据类型,它们将是引用类型。

堆栈和堆

值类型数据将在堆栈上分配,引用类型数据将在上分配。但是,当相同的值类型声明为数组或用作类的成员数据时,它们将被存储在上。此外,当值类型用于struct时,它们将被存储在堆栈上。

例如                  

class Program
{
    public int intMember = 3; // Data members will be part of object instance 
    public bool flag = true; // so they will be stored on the heap
    public void func1(int f, bool b, int[] intAry)//value type parameters will be allocated on the stack
    {
        int index = 5; // Integer local variable will be allocated on the Stack
        string str = "string"; // string local variable will be allocated on the Heap
        int[] ary = { 1, 2, 3 };// Integer array will be allocated on the Heap
        for (int i = 0; i < intAry.Length; i++)
        {
            intAry[i] = intAry[i] + 100;
        }          
        f = 123;
        b = true;
    }

    static void Main(string[] args)
    {
        Program obj = new Program();
        int[] ary1 = { 1, 2, 3, 4, 5 };
        int intLocal = 5;
        bool boolLocal = false;
        obj.func1(intLocal, boolLocal, ary1);
        // The changes done for the array in the called function will be reflecting
        for (int i = 0; i < ary1.Length; i++)
        {
            Console.WriteLine("ary1 [" + i + "]=" + ary1[i]);
        }
        // The changes done for value types in the called function will not reflect
        Console.WriteLine("intLocal=" + intLocal + ", boolLocal = " + boolLocal);
    }
}

在上面的示例代码中,当创建对象实例时,值类型成员变量intMember和flag将在上分配,而不是在堆栈上。而在Main函数中,intLocal、boolLocal将在堆栈上分配,整数数组ary1将在堆上分配。当我们调用成员函数func1时,参数变量f、b和局部变量index将存储在堆栈上,而字符串变量str和整数数组ary将在上分配。这里需要注意的是,当任何值类型声明为数组时,它们将被视为引用类型。如果您使用并运行上面的代码,您可以看到在func1函数中对整数数组所做的更改在函数返回后仍然有效,而局部值类型变量intLocal和boolLocal在调用函数func1后将不会反映更改。

何时使用struct和class

堆栈比堆更快。由于struct的内存是在堆栈上分配的,因此它们比在堆上分配内存的class更快。但是,由于堆栈的内存大小有限(最大1MB),因此我们应该仅在数据量较小时使用struct。如果您需要将大量数据存储在内存中(例如,一个大的struct),并且需要长时间保留该变量,那么最好使用class而不是struct来将其分配到堆上,而struct仅适用于少量数据。如果您处理的是只需要在函数使用期间存在的较小变量,则应使用堆栈。

装箱和拆箱

装箱是将值类型转换为引用类型的过程,在此过程中,它通过在堆上分配内存来创建一个新的object类型引用变量。下面是装箱的示例代码

int i = 67;                              // i is a value type 
object o = i;                            // i is boxed

拆箱是将引用类型转换为值类型的过程,前提是object(引用变量)中的值是值类型,否则将抛出运行时异常。

但这会将值类型转换为引用类型以及反向转换的负担,从而影响性能。因此,除非非常必要,否则我们应避免装箱和拆箱。通常,当处理仅接受object类型值的类时,我们会使用此过程。例如,当我们将在ArrayList中存储整数时,ArrayList只接受object类型,此时就会发生装箱。当我们检索并将object值类型转换为其相应的数据类型时,就会发生拆箱。下面是拆箱的示例代码

System.Collections.ArrayList list = 
    new System.Collections.ArrayList();  // list is a reference type 
int n = 67;                              // n is a value type
list.Add(n);                             // n is boxed
n = (int)list[0];                        // list[0] is unboxed

何时使用Ref和Out关键字

当值类型参数传递到方法时,每个参数的副本将在堆栈上创建。如果参数是大型数据类型,例如包含许多元素的自定义结构,或者方法执行次数很多,这可能会影响性能。在这些情况下,最好使用ref关键字传递类型的引用。out关键字类似于ref关键字,区别在于使用out时,它会告诉编译器在返回之前必须为该参数分配一个值,否则将导致编译错误。

多线程应用程序中线程之间的内存共享

在多线程应用程序中,每个线程都有自己的堆栈,大约1MB。但是,所有不同的线程将共享堆内存。由于堆内存将在所有线程之间共享,因此我们必须非常小心,并通过使用锁或其他.net同步技术来实现线程同步,以避免竞态条件、死锁等。如果我们有非易失性字段,那么在一个线程中更新的数据将不会反映在其他线程中。这就是volatile关键字的用武之地。

Volatile关键字的使用

在单线程应用程序中,我们不会遇到堆内存中分配的数据问题。但在多线程应用程序中,尽管堆内存被所有线程共享,CLR内部会进行优化和重排代码,这可能导致数据同步问题。例如,一个线程修改了对象obj的值,但读取其值的另一个线程不会获得obj的更新值。通过将volatile关键字应用于字段可以解决此问题。让我们看一个示例代码

class MultiThreadingHeapIssue
{
   bool stopLoop = true; // by default value is set to true
   public void Run()
   {
       new Thread(() => { Console.WriteLine("Loop started"); while (stopLoop); 
                          Console.WriteLine("Loop ended"); 
                        }).Start();
       // loop runs until the stopLoop field is set to false
       Thread.Sleep(1000);
       stopLoop = false;
       Console.WriteLine("value set to false.");
    }
    static void Main(string[] args)
    {
        MultiThreadingHeapIssue obj = new MultiThreadingHeapIssue();
        obj.Run();
    }
}

在release模式下使用ctr+F5运行上述代码后的结果

使用bool stopLoop = true;的结果

使用volatile的结果:volatile bool stopLoop = true;

深入了解JIT编译器对易失性和非易失性字段的优化

在上面的代码中,由于使用了非易失性字段stopLoop,.Net JIT编译器可能会将while循环重写成类似这样:

if (stopLoop) { while (true); }

如果JIT编译器在单线程应用程序中优化代码,那将没问题。但在多线程应用程序中,如果在另一个线程中将stopLoop设置为false,优化可能会导致无限循环。当我们标记stopLoop字段为易失性时,JIT编译器将不会为上述代码进行优化,因此条件将与原始代码保持一致。

源代码

我没有附加任何源代码,因为我编写的用于解释的程序非常简单,可以直接复制并执行以查看结果。

以下是一些值得阅读的好参考文献,以获得对主题的深入了解

以下是一些非常好的文章,其中包含太多细节,可能会让一些读者感到困惑。因此,我试图收集和解释有关这些核心.Net内存概念需要了解的必要要点。

© . All rights reserved.