转载

垃圾回收机制,是不是这样理解?

目录

  • 前言
  • 分配内存和资源初始化
  • 清理本地资源
  • 回收内存 & 垃圾回收算法
  • 垃圾回收机制:代

前言:资源的生存周期

1、new一个对象时,调用IL命令newobj,为资源类型分配内存。

2、初始化内存,构造函数初始化资源的状态。

3、程序中来回的调用、访问资源。

4、摧毁资源的状态并进行清理。

5、释放内存。垃圾回收执行这一步。

一、分配内存和资源初始化

第1与第2步—如何分配内存和资源初始化?

首先CLR规定所有的资源都从托管堆中分配。此托管堆维护对象资源,会为我们自动管理对象状态。

进程初始化时,CLR会预留一块连续的地址空间即托管堆,但是没有对应的物理存储空间。此托管堆上维护者一个指针,它指向下个对象在托管堆中的分配位置。

new操作符会生成一个IL指令newobj,指令会指导CLR进行以下工作。

A、计算类型以及基类的字段所需要的空间。

B、加上对象的开销所需的字节数(类型对象指针以及同步块索引)。

C、CLR检查保留区域是否能够提供分配对象所需的字节数,如果有就提交存储。对象会在指针NewObjPtr指向的位置放入,为对象分配的字节数清零,并调用实例化构造器返回对象的地址。

当前指针会加上对象占据的字节数,成为一个新址,下一个对象的存储地址。

托管堆上分配对象的前提是空间内存足够,那就引出了下一个机制—垃圾回收机制,来回收内存,释放资源。

二、清理本地资源

第4步—怎样摧毁本地资源的状态并进行清理?

方式一:隐式终结—Finalize()

定义:垃圾回收器会在回收内存之前执行Finzlize()(如果此类实现了Finalize方法),垃圾回收会自动调用此方法。垃圾回收器会因隐式执行。

语法:是在类名前加~,例如:~方法名(){}—Finalize方法。

触发:第0代满、显示调用System.GC的Collect方法、内存不足、卸载AppDomain

原理:一个实现了Finalize方法的对象new之后,会在垃圾回收器维护的一个终结列表中添加一个指针指向这个新分配的这个对象。在回收内存之前调用它的Finalize方法。

不足:如果开启一个数据库连接,我们不显示进行关闭。那么垃圾回收器会在进行垃圾回收的时候释放连接资源,我们不确定下次的垃圾回收发生在何时,我们想提前摧毁状态。

所以下面就使用显示摧毁资源状态(前提是确定不再使用,确认需要关闭)

方式二:显示终结—Dispose()、Close()

我们通过书上的例子更直接:

垃圾回收机制,是不是这样理解?

下面具体讨论一下我们经常使用的Close()以及Dispose()

首先我们看到了熟悉的 Finalize()方法—>~SafeHandle() ,还有需要我们讨论的Dispose()方法和Close()方法。另外,还有一个带有参数的Dispose(Boolean disposeing)虚方法。

这里的资源释放统一处理 Dispose(Boolean disposeing) 方法,如果参数为true,会标记此对象资源显示关闭,但是没有终结,可以正常访问字段。如果是false,那就终结对象,回收内存。

因为继承了IDispose接口,所以要实现一个无参的Dispose()方法,而我们经常使用的Close()是因为出于习惯觉得有一个叫Close()的方法似乎更亲切。所以就添加了一个Close()方法。没有其他特殊用途。

当我们调用Dispose()或者Close()方法时,对象本身的内存还没有释放,仍然需要垃圾回收器来回收内存。

所以到目前为止我们可以通过三种方式来摧毁资源:

1、显示调用Dispose()

2、显式调用Close()

3、等待垃圾回收时,垃圾回收器自动调用Finalize方法进行摧毁。

4、一种Dispose()和Close()方法的变相模式using(){}

三、垃圾回收

第5步—如何释放内存?

垃圾回收器检查托管堆中是否有应用程序不再使用的对象。有,回收内存(如果回收后,内存仍然不够,就抛出内存溢出异常)。

垃圾回收器怎样判断对象正在使用?

每个应用程序都包含一组根,每个根都是一个存储盒子,里面包含着引用对象指针(要么引用一个对象,要么为null)。

例如类中定义的任何静态字段会被认为有一个根,方法中的任何参数和局部变量也会被认为有一个根。只有引用类型的变量才被认为是根。

当然这里有个前提:只有引用类型才能认为是根,值类型除外。

借用一下书上的例子:类

垃圾回收机制,是不是这样理解?

JIT在生成CPU代码的同时还会生成方法在本地CPU指令中的一个字节偏移范围的记录项,这个记录项也包含着根的一组内存地址和CPU寄存器。

上面的类在第一次调用方法 WriteBytes 的时候,JIT会将IL代码翻译成CPU指令,如下(x86 CPU):

垃圾回收机制,是不是这样理解?

1、寄存器:

ebx在偏移到 00000003 处开始为寄存器的根,到循环结束 00000028 处结束根。此类为实例,所以会有一个this指针,通过','后面的 ecx寄存器传递 ,并 存储到前面的寄存器ebx。

同样,esi在偏移到 00000005 处开始为寄存器的根,直到 00000028 根结束。它通过寄存器 edx 传递bytes[],并将数组存入到寄存器 esi。

对于 edi 来说,它传递的是Int32类型,值类型不会有根。

后面的 ecx0000000f 开始作为根,到 000001e 处根结束。

2、垃圾回收:如果在0000017处发生垃圾回收

首先确定,00000017处发生的垃圾回收,没有到达 ebx(this指针)、esi(byte[]) 的根结束位置 00000028 ,也没有到达 ecx(m_textWriter)根结束位置0000001e。

A、收集根:

(1)这三个寄存器中引用指向的对象都是根,而且这些根中所引用的堆中的对象也不能回收。

(2)其次垃圾回收器会检查线程栈上行,检查每个方法的内部表来确定所有调用方法的根。

(3)最后垃圾回收器将遍历所有类型对象,来获取静态字段中存储的根集合。

B、标记阶段

(1)垃圾回收器开始执行时,它会假设堆中的所有对象都是垃圾。它会假设线程栈和堆没有引用关联,没有CPU寄存器引用堆中的对象。也没有静态字段引用堆中的对象。

(2)接着进入标记阶段,沿着线程栈上行检查所有根,如果发现一个根引用了一个对象,就对这个对象进行标记(同步块索引字段上开启一个bit=1的标识)。

收集根并标记完后,会有标记和未标记的对象。标记的就是程序可以继续访问的,反之就是不可达的垃圾。就会对垃圾进行回收内存。

C、压缩阶段

(1)垃圾回收器会线性遍历堆,遇到垃圾对象时,检查一下连续内存块,如果较小就忽略,较大就会将非垃圾对象移动到这里。

(2)但是非垃圾对象之前的地址和寄存器等都会失效,垃圾回收器也会重新访问根,生成新的地址等等。

这样程序内存的碎片化就得到大幅度的控制,当然这也是牺牲了些许的性能。

四、代

代:是垃圾回收器采用的一种机制,目的就是为了提高程序性能。

根据代机制,我们可以做出以下假设:

  • 对象越新,回收可能性非常大。
  • 对象越老,回收可能性非常小。
  • 回收堆中的部分,速度快于回收整个堆。

我们的托管堆初始化时不会包含任何对象,我们初始化的对象会添加到托管堆上,这些对象我们称为第0代。第0代的存储上限为256KB,第一代为2M·····

此时我们标记第0代对象在堆上的存储上限为256KB(只是假设一下),如下图:

5个对象A、B、C、D、E。程序运行一会之后,C和E变得不可达,等待垃圾回收器来回收内存。

垃圾回收机制,是不是这样理解?

每当第0代满时,也就是当前堆中的对象达到了上限256KB,这时垃圾回收器开始执行垃圾回收,

如果此时分配新的对象F时,出现A~E达到分配上限256KB,垃圾回收器就会压缩D使得和之前可用内存连续起来。

C和E回收了内存。这样A、B、D进入第一代,第0代空,如下图:

垃圾回收机制,是不是这样理解?

运行一段时间,B、H、J也是不可达状态,同时在新分配L时第0代达到了上限256KB,如下图:

现在第0代满,垃圾回收器运行,它会压缩I、K的内存与G连续。同时回收H、J。

此时第一代这个虽然有不可达的B对象,但是第一代没有达到上限2M,垃圾回收器就不会对B进行回收。

垃圾回收机制,是不是这样理解?

回收后:我们看到没有对B进行回收,那是因为第一代没有达到上限2M,这一切为了性能,因为第一代中出现垃圾的频率一般远远低于第0代甚至没有垃圾。

这样垃圾回收器就宁愿让垃圾暂时呆在那里,暂时不去回收。这样节省了时间,增加了效率。

垃圾回收机制,是不是这样理解?

程序依然运行,就会有更多的对象分配到堆上,同时产生更多的的垃圾,如下图:

此时当分配P时,第0代满,执行垃圾回收,回收地0代P、R

此时垃圾回收器检测到第一代也超过限额2M,就检测第一代中的对象进行垃圾回收。知道这个时候第一代的垃圾才有幸回收。

垃圾回收机制,是不是这样理解?

回收后:此时会将第一代剩下的升级到第二代中,原来在第0代存活的对象,也会升级到第一代中。

垃圾回收器只有三代。因为CLR会根据实际情况进行自动调节。3代足够。

垃圾回收机制,是不是这样理解?

PS:在CLR初始化时,会对第0代、第一代、第二代进行内存限额设定:256KB、2M、10M。限额越大执行垃圾回收的频率越低。

只在第0代满时进行垃圾回收,当第0代满,此时第一代也满,才会对第一代进行垃圾回收。所以效率上是可以保证的。

当CLR检测到回收第0代对象后,几乎没有回收多少内存,此时就会调整上限到512KB。同样如果回收的垃圾很多,那调整到128KB。以此类推,自动调剂。

这样的调节也会应用于第一代、第二代。

正文到此结束
Loading...