同时看了好几本书,对变量的内存分配概念总是稀里糊涂的。所以干脆专门写一篇文章来对C#内存分配进行研究和总结。
1、值类型和引用类型
对值类型:
- 值类型实例通常存在线程的堆栈里。即所有值类型的非成员数据都放在线程的堆栈里。
- 如果值类型是类的数据成员,那么值类型实例是存放在托管堆里的。
- 如果值类型装箱转换为引用类型,其类型的数据拷贝到托管堆里的,也就是说,装箱后的值类型是存放在托管堆里。
- 可能还有其他情况,我知识有限,暂时没法知道。
对引用类型:
-
1)引用类型实例为非成员数据时(一般为局部变量),引用是在线程的堆栈上,而引用的所有数据都存在托管堆里。
-
2)引用类型实例为成员数据时,引用和其指向的所有数据都放在托管堆上。
比如 class A 包含 class B;A 实例作为非成员数据,A放在线程的堆栈里,而A里的所有数据
都放在托管堆里。因为 B 被 A 包含,B和B的所有数据都放在托管堆里。(由于目前没有找到满意的答案,书上也没有细说,所以也不太确定B和B的所有数据分配的托管堆是一块邻近的内存地址,还是 B 放在分配给A的托管堆以及B的所有数据放在分配给B的托管堆里)
2、疑惑点
静态和非静态数据分别存放在哪一块内存里呢?
有些书哪怕是经典的书(比如《C# 高级编程》、《C# 图解教程》、《CLR C#》、《Microsoft.NET 框架程序设计》),都没有提到静态以及非静态数据的内存分配。按理说,根据 C/C++ 分配内存的规则,静态数据是存放在静态区的。
可是C# 似乎没有提到“静态区”这一概念。
有些书对引用类型和值类型的一些相关知识点的描述是这样的:
1)
出自:《C# 图解教程》
-4.8.1 存储引用类型对象的成员:
对于引用类型的任何对象,它所有的数据成员都存放在堆里,无论它们是值类型还是引用类型
- 4.8 值类型和引用类型
非成员数据的存储:“对于值类型,数据存放在栈里”
我的理解是:
我的理解:
1、”它所有的数据成员都存放在堆里“
我会默认为,所有数据成员包括了静态和非静态的数据成员。
可是仔细想想,如果静态数据存放在堆里,那么这块数据不可能在程序运行过程中,被释放掉。
不过是由 CLR 来托管堆的,静态数据内存可以被 CLR 控制着一直不被释放,也能勉强理解。
2、“对于值类型,数据存放在栈里”
根据书上的描述,我的理解是,不管是静态还是非静态,数据都存放在栈里。
问题就来了,如果静态的值类型,存放在栈里,就违背了栈的先进后出的规则。
2)
出自:《C# 高级编程》
在处理器的虚拟内存中,有一个区域称为栈。栈存储不是对象成员的值数据类型。
在调用一个方法时,也使用栈存储传递给方法的所有参数的副本。
我的理解是:
我的理解:
“栈存储不是对象成员的值数据类型”
我开始懵在于理解错了,以为是“栈存储不是值数据类型”
本书原意是:“非成员数据为值类型时,存放在栈里”
3、理清楚了关于堆的概念:
-
C # 的存储内存主要是托管堆和线程的栈(也称堆栈)。(托管堆就是 CLR 里各个线程里共享的堆,并没有其他如非 CLR 堆这一区域)
-
C/C ++ 的堆主要是需要由程序员自己去分配和释放的。而C# 的堆是一个托管堆(查到相关资料,说托管堆分为大象堆、小象堆、GC 堆),由 CLR 来处理。(当然也可以通过特殊手段,由程序员处理)
4、还是这个疑问:
C# 到底有没有静态区,静态数据存放在哪里呢?
我的想法:
根据之前书上没有明说的描述:
“对于引用类型的任何对象,它所有的数据成员都存放在堆里”,
“非成员数据为值类型时,存放在栈里”
假设结果(这是假设,由以下文代码例子来验证结论):
以上这两点都不包括静态数据。
所有静态数据(包括基元类型,值类型,引用类型)在静态区里;
而非静态数据根据不同情况而决定存放在托管堆还是栈里。
后来想想,还是给出测试的例子来说事吧,我不能随便去猜测结论,而是需要验证的。
例子:
class MemoryInto
{
//从 GC 获取对象指向的内存地址
public void getGCMemory(string title, object o)
{
//Pinned 防止GC对对象进行内存地址移位
GCHandle handle = GCHandle.Alloc(o, GCHandleType.Pinned);
var addr = handle.AddrOfPinnedObject();
Console.WriteLine(title + ":" + addr);
}
}
class TestA
{
public static int StaticData = 0;
public int Data = 0;
public const int ConstData = 0;
public readonly int ReadonlyData = 0;
public TestB B;
public TestC C;
public TestA()
{
B = new TestB();
C = new TestC();
}
}
class TestB
{
public static int StaticData = 0;
public int Data = 0;
public const int ConstData = 0;
public readonly int ReadonlyData = 0;
}
class TestC
{
public int Data = 0;
}
class Program
{
static void Main(string[] args)
{
MemoryInto gc = new MemoryInto();
TestA a = new TestA();
TestB b = new TestB();
TestC c = new TestC();
int varInt = 0;
//语法错误,C#不能对局部变量进行声明定义静态变量
//static int varStaticInt = 0;
//根据网上有些相关资料,说是GetHashCode函数返回的是对象的内存地址;
//但是我怀疑这可能并不是对象的地址;
Console.WriteLine("类A对象:" + a.GetHashCode());
Console.WriteLine("在类A中,类B对象:" + a.B.GetHashCode());
Console.WriteLine("在类A中,类C对象:" + a.C.GetHashCode());
Console.WriteLine("类B对象:" + b.GetHashCode());
Console.WriteLine("类C对象:" + c.GetHashCode());
gc.getGCMemory("非成员的局部变量:", varInt);
Console.WriteLine();
//访问类A
//开始运行报错
gc.getGCMemory("类A对象指向的内存", a);
gc.getGCMemory("类A的数据成员", a.Data);
gc.getGCMemory("类A的静态成员数据", TestA.StaticData);
gc.getGCMemory("类A的数据成员常量", TestA.ConstData);
gc.getGCMemory("在类A中,类B的数据成员常量", a.ReadonlyData);
Console.WriteLine();
//在类A中,访问类B
gc.getGCMemory("在类A中,类B对象指向的内存", a.B);
gc.getGCMemory("在类A中,类B的数据成员", a.B.Data);
gc.getGCMemory("在类A中,类B的静态成员数据", TestB.StaticData);
gc.getGCMemory("在类A中,类B的数据成员常量", TestB.ConstData);
gc.getGCMemory("在类A中,类B的数据成员常量", a.B.ReadonlyData);
Console.WriteLine();
//在类A中,访问类C
gc.getGCMemory("在类A中,类C对象指向的内存", a.C);
Console.ReadKey();
}
}
运行后报错:
“System.ArgumentException”类型的未经处理的异常在 mscorlib.dll 中发生
其他信息: Object 包含非基元或非直接复制到本机结构中的数据。
我也是扶了!花了那么多时间写测试代码,最后把 GCHandleType.Normal 改为 GCHandleType.Pinned 结果运行报错。GetHashCode 也似乎在误导我。
不太懂 Normal 和 Pinned 到底有什么区别,而且总感觉使用 Normal 不太靠谱,打印结果并不是自己预想的那样。
报错分析:
我尝试了下,只要Object 是 基元类型或是系统提供的数据类型,才能获取得到其地址。其他自定义的引用类型,以及引用实例里的基元类型就会报错。如可以获取 int、String的内存地址。而如果把
GCHandleType 为 Normal 就不会报错;
从例子中学到了几点:
-
常量 const 和 Readonly。const 是名副其实的常量,在程序整个运行过程中,其内存不能被释放掉,由编译时期决定的,即多个同一个类的对象共享同一个常量。而 Readonly 可由类的多个对象根据不同情况而决定,即多个同一个类的对象都有各自的常量。
-
假如 C# 存在内存静态区,并且是类对象的数据成员,const 变量存放在静态区里,而 Readonly 变量存放在托管堆里。(除非 Readonly 标记为 static)
-
Readonly 变量不能作为非数据成员来使用。
-
GetHashCode 方法只不过是主要用来判断两个对象是否相同,值为散列码,作为对象的唯一标识。不能拿来获取对象的地址。GetHashCode 方法返回值也有可能是负值(听网友说的,不知道什么情况下才会出现负值),也可以对此方法进行重写;(比如使用扩展方法重写该方法)
-
静态变量、const 常量一直在程序运行中都存在,所以他们都存在静态区(或还有常量区这一说法)。
-
以上几点,涉及到静态和常量,我没法用数据来说话。因为很难直接使用 C# 来获取一些变量的地址,并不像 C/C++ 那样可以在变量名前加 & 就能直接获取。但是由于C# 和 Java 在内存上有很多相似的地方。所以根据 Java JVM 中有 静态区和常量区的概念,我会默认 C# 也会是这样子的。(可残酷的是,下文内容会打自己的脸)
5、小结
关于变量的内存分配情况(待验证结论):
- 对于引用类型的任何对象,它所有的数据成员都存放在堆里,无论它们是值类型还是引用类型。(此处我再加上条件:非静态,非const 常量,但包含基元类型)
- 值类型为非数据成员时(此处我再加上条件:非静态(不太肯定 const 常量),但包含基元类型),都存放在线程的堆栈上。
可是,但是,然而…
我翻了翻,《C# 图解教程》书…(书中章节7.4 静态字段)
好吧,结论改下:
- 对于引用类型的任何对象,它所有的数据成员都存放在堆里,无论它们是值类型还是引用类型。(关键字:所有)
- 值类型为非数据成员时(改为不是引用实例的所有数据为非数据成员时),都存放在线程的堆栈上。
暂时还不能解除心中的疑惑,所以目前先勉强理解到这一程度,希望将来能够确切的理清楚这一问题。