(二)CSharp-关于内存分配的总结和疑惑(未解决)

同时看了好几本书,对变量的内存分配概念总是稀里糊涂的。所以干脆专门写一篇文章来对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 静态字段)

(二)CSharp-关于内存分配的总结和疑惑(未解决)_第1张图片

好吧,结论改下:

  • 对于引用类型的任何对象,它所有的数据成员都存放在堆里,无论它们是值类型还是引用类型。(关键字:所有)
  • 值类型为非数据成员时(改为不是引用实例的所有数据为非数据成员时),都存放在线程的堆栈上。

暂时还不能解除心中的疑惑,所以目前先勉强理解到这一程度,希望将来能够确切的理清楚这一问题。

你可能感兴趣的:(CSharp,c#,.net)