首页 > 代码库 > (二)学习C#之内存管理
(二)学习C#之内存管理
一、当你运行你的程序通常都会访问哪些内存空间呢?
电脑自言自语道,“这个人要声明一个整数”或“这个人个方法”或“这个人要创建一个对象”
1.这些信息究竟是存在内存里的什么地方呢?
2.或者说用于描述这个整数、这些对象和方法的信息,都在哪呢?
3.这些内存的组织方式又是什么样的呢?
二、有三类不同的空间,但是其中两类与我们的关系最密切。
1.第一类存储空间:用于存储所有静态变量和常量
我们可以把它们想作不会变的变量,这是一块专门划分出来的空间,是一块比较特殊的空间,在这块特殊的空间存放着静态变量和常量,那么这特殊空间在何处呢,我们不是特别关心,总之它就是我们的特殊小可爱。
因为在程序开始运行的时候,这些东西就会被放置在内存中,我们的这些常量永远不会改变,这意味着他们并不需要被放在那些我们常常访问的内存中,他们只要呆在内存中的某个地方,然后我们知道如何访问它就可以了。
2.第二类存储空间:用于存储动态变量
1).什么是动态变量?
动态变量就是利用new申请到的变量,所以当你用new来申请一个对象的时候,比如new Goval(参数),这样就得到一个动态变量,动态变量对应的内存,也就是你用new申请到的叫做堆。
2).什么是堆?
堆就像是一大摞衣服,当你需要衣服的时候你过去说,“嘿,我需要些新衣服”然后它就给你些衣服,你穿完这些衣服就会说,“嘿,我再也不要穿这些衣服了”,然后神奇的事情就是,被你舍弃的衣服就变成了一块布,计算机知道你不再穿这些衣服了,它说,“我要把这些内存拿回来”,这就叫做垃圾回收。
垃圾回收的实质:当堆里的内存不再被需要的时候,就会被收回。
3.第三类存储空间:用于存储本地变量
这些本地变量存放在一个叫栈的地方
什么是栈?
和堆的概念是不一样的。对栈来说,当你在某方法中访问本地变量时,或者说你在调用方法时要传递某些参数时,比如说这些参数,你实际上是把这些参数的值进行了复制,在内存中分配了另一块空间来存储它们,而时间上在栈中就会有一些空间会被自动分配给你,当你在访问本地变量后者是传递参数,或者是调用方法时,这些变量都是活跃的,而当这些变量不再活跃时,比如方法调用结束,或者遇到了右花括号时,这些变量就会消失,它们也就不能再会以任何形式被调用,然后栈就说到,“嘿,我要把这些内存空间拿回来”。
4.介绍静态变量、动态变量和本地变量
1)静态变量: 所有静态变量,或者叫特殊变量,都存储在内存中比较低的位置,也就是说,这些位置对应的数值都是比较小的,比如说在1000这个位置,注意这个1000是十六进制的而不是十进制的。
2)堆:当堆变大时它是在往下变大。往下变大是什么意思?这就是说当从2000开始的时候,你说,“嘿 堆 我需要给我的新GOval准备一些空间”它说,“好的 我给你在这里分配些内存,就在2004这个地方好啦”,然后你说,“我需要给我的新GWreck准备一些空间,它说“好的,2008这地方归你了,我会在这里这给分配空间”,所以说当堆越来越大的时候,我们就说堆在向下变大。
3)栈:栈从一些比较高的地址开始,从FFFF开始,当它分配内存时,它会向上变大。也就是说,这些地址数值会越来越小。还有一个参量用于描述本地变量、参数和动态变量,当然主要说的是动态变量。那就是它们所占用的空间大小。
4)不同变量实际上需要不同大小的存储空间,比如一个整数,int至少在C#,java占四个字节。动态变量还有一些附加内存开销,这实际上就是一些额外的内存空间,是整数和字符本身所占用空间外的一些空间,用来保存你的对象的其他信息。
三、在程序开始运行的时候,或者创建新对象的时候,到底发生了什么......
准备:
1.我们可以通过调用构造方法创建一个点,并告诉它x和y坐标,然后它就会把这个坐标保存下来,你就可以去移动这些点。
2.刚开始只是有这个类,内存尚未被分派,什么事还都没做。我们写个程序,创建点对象,并调用其对应的方法。运行程序,我们就可以知道在个过程中,计算机内存里到底发生了什么。
3.运行:
1)第一件事:调用run方法,当程序刚开始运行时run方法会被调用。
当一个方法被调用时,我们会创建这些本地变量和参数,但是run没有参数,所以不需要为参数安排空间,但它的确有两个本地变量P1和P2,但是还不包括任何信息,因为我的程序刚刚开始,还没运行到new Point这个地方。
2)第二件事:调用new便可以得到一个动态变量,这意味着它将被存储在堆里。
a.记录保存一个点的数据。构造方法的作用是将在计算机分配的空间填进数据(2,3)。
b.overhead里面是额外内存开销。
c.为什么在p1那里写1000呢?其实是我们给这个新的点分配好空间之后,该如何处理这个点呢?我们把这个点分配给P1,这里有个关键的知识点,就是计算机是怎么知道P1和它代表的所有信息都存储在那里呢?每个对象都有一个地址,实际上计算机在内部是通过对象所处的内存地址,来找到这些对象的。所以当我调用new的时候,就会在堆中申请一块空间。然后把这段空间赋值给p1,到底是什么东西被赋值了呢?实际上是把P1的内存地址进行了赋值,确切的讲是起始地址。通过这个起始地址可以找到P1包含的所有数据,这些数据就被存储在stack(1000)里,这里的1000是内存地址1000。
3)第三件事:新建一个新点,需要从堆那里要点内存。
堆说,“1008之前的地址已经分配出去了,那我就接着给你分配一块能放得下一个点的空间,用来保存你的实例变量。
然后计算机就说好,分配我一个空间,然后调用构造方法,将点的数据填入存储单元,再将起始地址100C赋值给p2。px、py是属于p1的实例变量,后来分配的px、py是属于p2的实例变量。
4)第四件事:神奇的事件。调用一个方法,例如调用p1.move。
a.当我们调用这个方法时,我们会有一些参数和本地变量。也就是说我们要在栈上分配内存才能进行方法的调用。所以当我们调用方法时所发生的就是,“嘿,p1我要把你移动一下偏移(10,11)这么多”。那再内存中发生了什么事呢?内存中发生的事情就是:我们会创建一个新的“栈帧”,当你调用一个方法的时候,调用方法的方法,或者说原先运行的方法就会被挂起。内部的情况是这样的,我们在栈上分配内存空间,所以我们管它叫栈帧,它存储了调用这个方法所需要的信息。什么信息,首先是方法的调用,额外内存开销,需要存储所有传进来的参数。
b.关于this,有一个隐藏参数,当我针对某特定对象调用方法后,我如何得知这个方法要调用的对象是哪个?当你对一个对象调用方法的时候,在所谓的内存开销之后,紧接着放在栈上的内容,实际上是指向这个对象的指针;我们把它叫做this指针。this是对象访问自己的一种方式。它需要知道自己在内存的某个地方。
c.很奇怪,为什么对象需要知道自己呆在什么地方呢?
因为当我们写一个方法的时候,我们并没有将这些方法跟已经创建的某些特定对象进行关联。如果我创建一个特定的对象(如p1),然后对其调用那个方法,那个方法得知道,“嘿,你让我改变px py值,你说的是那个px py值”,这个时候,this就起作用了。
this说:“我知道这个方法对应对象在内存中的位置。”这样无论你在程序里进行什么样的操作,都可以通过this指定你要访问的对象。
进行方法调用的时候,首先要知道这个方法所对应的那个特定的对象的地址。我们针对p1进行调用,p1放在1000这个位置。所以p1就是1000,指针this指的值就是1000.接下来逆序存放传入的参数。传入参数值,这些是传入参数的副本。
5)第五件事:现来看看调用move方法时会怎样?现在栈里面已经存储了move的本地变量,和对象对应的指针地址。
a.第五步执行的是px加10,但是是那个px呢?我们顺着this指针找到的那个px。
如何找?
b.去this存储的地址找。于是它到达1000位置那里,读取额外内存开销信息,它就知道px相对起始位置1000的位置。然后它继续说“嘿已经找到了,它的值是2,我在它上面加上10变成12”。
6)第六件事:很有趣的事情。当栈内变量不再活跃时,它们就会被删除。
a.这个时候计算机会自动跑过来说,“嘿,这块变量,这块内存,再也不会用到,我要把它收回。”所以栈上这部分的内存,就是与move方法相关的栈帧。这些就是调用move分配的内存。也就是说所有为move分配的内存被机器自动的收回。这就叫做"栈里弹出"。想象一下,当栈顶的东西用完后,把它从栈顶上弹出去,然后它就没有了。
b.如果再次调用move方法?相关的信息又会重新放在栈帧上,但执行过一次move之后就不需要了。
注意:构造方法和其他方法的区别,构造方法不用this指针。
(二)学习C#之内存管理