如何更好的回收js 内存回收空间,有没有强制性的

【操作系统】实验四 主存空间的分配和回收
时间: 14:46:47
&&&& 阅读:56
&&&& 评论:
&&&& 收藏:0
1.目的和要求
1.1.&实验目的
& & & &用高级语言完成一个主存空间的分配和回收程序,以加深对动态分区分配方式及其算法的理解。
1.2.&实验要求
& & & &采用连续分配方式之动态分区分配存储管理,使用首次适应算法、循环首次适应算法、最佳适应算法和最坏适应算法4种算法完成设计。
& & & & & &(1)**设计一个作业申请队列以及作业完成后的释放顺序,实现主存的分配和回收。采用分区说明表进行。
& & & & & &(2)或在程序运行过程,由用户指定申请与释放。
& & & & & &(3)设计一个空闲区说明表,以保存某时刻主存空间占用情况。把空闲区说明表的变化情况以及各作业的申请、释放情况显示。
2.实验内容
& & &根据指定的实验课题,完成设计、编码和调试工作,完成实验报告。
3.实验环境
& & &可以选用Visual C++作为开发环境。也可以选用Windows下的VB,CB或其他可视化环境,利用各种控件较为方便。自主选择实验环境。
4.参考数据结构:
& & &#include&stdio.h&
& & #include&conio.h&
& & #include&string.h&
& & #define MAX 24
& & struct partition{&&
& & & &char pn[10];
& & & &&& ////////
& & & && //////////
& & &typedef struct partition PART;
第一步:(第13周完成)
完成程序数据结构的创建,初始化内存分配情况,创建空闲分区表和已分配分区表。&
第二步:(第14周完成)
完成为某作业分配内存空间。
用户输入作业名称;
判断作业名称是否已经存在,如果存在则要求用户重新输入;
用户输入作业所占空间大小;
判断是否能够在剩余的空闲区域中找到一块放置该作业,如果不行则要求用户重新输入;
显示菜单,由用户选择使用哪一种分配算法:
1) 首次适应算法
2) 循环首次适应算法
3) 最佳适应算法
4) 最坏适应算法
& & 6.为该作业分配内存空间,分配处理流程图如下(size的值设定为1K):
& & 7.屏幕显示分配后的内存分区情况。
第三步:(第15周完成)
完成内存空间回收;
由用户输入作业的ID,决定所要终止的作业;
判断是否存在用户所输入的ID,如果存在则进行终止,否则提示作业不存在;
判断即将终止的作业前后是否有空闲区域,如果没有则作业所占的空间独立成为一个空闲块,在未分配区表中增加一项;
(思考:如何判断前后是否有空闲块?)
即将终止作业所占空间前后有空闲块的情况:(X代表即将被终止的作业,黑色代表内存中的空闲块)
程序中表示内存区块的结构体如下:
struct partition {
&&& char&&& pn[10];
&&& int&&&
&&& int&&&
&&& int&&&
&&& char&&&
& & & & &所以,判断某个即将被终止的作业所占空间前面是否有空闲块的方法是:作业空间的起始地址A.begin是否等于某个空闲块的结束地址B.end,若相等,则前面有空闲块,则需要合并;若不相等则再判断后面是否有空闲块。
回答:如何判断?
& & & & &进行四种情况的判断,然后分别做出相应的区块回收操作。
回答:如何处理回收?
& & & & &显示回收后的内存使用情况。
#include&stdio.h&#include&conio.h&#include&string.h&#include&stdlib.h&#define MAX 24struct partition{
char pn[10];
};typedef struct partition PART;
PART part[MAX];
int main(){
int i=0; int flag=1; int count1=0,count2=0;
char j[10];
// 初始化内存空间 printf("初始化,设置内存总容量512k\n"); printf("系统从低地址部分开始使用,占用:"); scanf("%d",&memory); printf("\n");
strcpy(part[0].pn,"SYSTEM"); part[0].begin=0; part[0].size= part[0].status=‘u‘; printf("已分配分区表Used:\n"); printf("\tNo.\tproname\tbegin\tsize\tstatus\n"); printf("\tNo.1\t%s\t%d\t%d\t%c\n\n",part[0].pn,part[0].begin,part[0].size,part[0].status);
printf("=============================================================\n");
printf("空闲分区表Free:\n"); printf("\tNo.\tproname\tbegin\tsize\tstatus\n"); printf("\tNo.1\t----\t%d\t%d\tf\n\n",part[0].begin+part[0].size,512-part[0].size);
printf("内存使用情况,按起始增长的排序:"); printf("printf sorted by address:\n"); printf("\tNo.\tproname\tbegin\tsize\tstatus\n"); printf("\t======================================\n"); printf("\tNo.1\t%s\t%d\t%d\t%c\n",part[0].pn,part[0].begin,part[0].size,part[0].status); printf("\tNo.2\t----\t%d\t%d\tf\n\n",part[0].begin+part[0].size,512-part[0].size);
i=1; while(1) {
printf("0.退出 1.分配 2.回收:");
scanf("%d",&choose);
switch(choose)
{ //为作业分配内存空间
printf("输入进程名:");
scanf("%s",&part[i].pn);
printf("输入进程所占空间大小:");
scanf("%d",&part[i].size);
part[1].begin=100;
part[i].status=‘u‘;
flag++; //作业数
printf("分配成功!\n");
printf("空闲区表Free:\n");
printf("\tNo.\tproname\tbegin\tsize\tstatus\n");
for (i=0;i&i++)
part[i+1].begin=part[i].begin+part[i].
count1=part[i+1]. // 已用的空间大小
count2=512-count1; // 剩余的空间大小
printf("\tNO.1\t----\t%d\t%d\tf\n\n",count1,count2);
part[0].status=‘u‘;
printf("\t======================================\n");
printf("已分配分区表Used:\n");
printf("\tNo.\tproname\tbegin\tsize\tstatus\n");
for(i=0;i&i++)
part[i+1].begin=part[i].begin+part[i].
printf("\tNO.%d\t%s\t%d\t%d\t%c\n",i+1,part[i].pn,part[i].begin,part[i].size,part[i].status);
printf("\n\n");
printf("内存使用情况,按起始增长的排序:\n");
printf("printf sorted by address:\n");
printf("\tNo.\tproname\tbegin\tsize\tstatus\n");
printf("\t======================================\n");
for (i=0;i&i++)
printf("\tNo.%d\t%s\t%d\t%d\t%c\n",i+1,part[i].pn,part[i].begin,part[i].size,part[i].status);
printf("\tNO.%d\t----\t%d\t%d\tf\n\n",flag+1,count1,count2);
printf("输入进程名:");
scanf("%s",&j);
printf("回收成功!\n");
for (i=0;i&i++)
if (strcmp(j,part[i].pn)==0)
strcpy(part[i].pn,"----");
part[i].status=‘f‘;
printf("空闲区表Free:\n");
printf("\tNo.\tproname\tbegin\tsize\tstatus\n");
for (i=0;i&i++)
if (part[i].status==‘f‘)
printf("\tNO.%d\t%s\t%d\t%d\t%c\n",i,part[i].pn,part[i].begin,part[i].size,part[i].status);
printf("\tNO.%d\t----\t%d\t%d\tf\n\n",flag+1,count1,count2);
printf("\t======================================\n");
printf("已分配分区表:\n");
printf("\tNo.\tproname\tbegin\tsize\tstatus\n");
for(i=0;i&i++)
if (part[i].status==‘u‘)
printf("\tNO.%d\t%s\t%d\t%d\t%c\n",i+1,part[i].pn,part[i].begin,part[i].size,part[i].status);
printf("\n\n");
printf("内存使用情况,按起始增长的排序:\n");
printf("printf sorted by address:\n");
printf("\tNo.\tproname\tbegin\tsize\tstatus\n");
printf("\t======================================\n");
for (i=0;i&i++)
printf("\tNo.%d\t%s\t%d\t%d\t%c\n",i+1,part[i].pn,part[i].begin,part[i].size,part[i].status);
printf("\tNO.%d\t----\t%d\t%d\tf\n\n",flag+1,count1,count2);
} } return 0;}
初始化内存分配
为进程a分配内存空间
回收进程a的内存空间
&标签:原文地址:/Leey0917/p/5613971.html
&&国之画&&&& &&&&chrome插件&&
版权所有 京ICP备号-2
迷上了代码!Java垃圾回收原理
我的图书馆
Java垃圾回收原理
我们知道,许多程序设计语言都允许在程序运行期动态地分配内存空间。分配内存的方式多种多样,取决于该种语言的语法结构。但不论是哪一种语言的内存分配方式,最后都要返回所分配的内存块的起始地址,即返回一个指针到内存块的首地址。&当已经分配的内存空间不再需要时,换句话说当指向该内存块的句柄超出了使用范围的时候,该程序或其运行环境就应该回收该内存空间,以节省宝贵的内存资源。&&&& 在C,C++或其他程序设计语言中,无论是对象还是动态配置的资源或内存,都必须由程序员自行声明产生和回收,否则其中的资源将消耗,造成资源的浪费甚至死机。但手工回收内存往往是一项复杂而艰巨的工作。因为要预先确定占用的内存空间是否应该被回收是非常困难的!如果一段程序不能回收内存空间,而且在程序运行时系统中又没有了可以分配的内存空间时,这段程序就只能崩溃。通常,我们把分配出去后,却无法回收的内存空间称为"内存渗漏体(Memory Leaks)"。&以上这种程序设计的潜在危险性在Java这样以严谨、安全著称的语言中是不允许的。但是Java语言既不能限制程序员编写程序的自由性,又不能把声明对象的部分去除(否则就不是面向对象的程序语言了),那么最好的解决办法就是从Java程序语言本身的特性入手。于是,Java技术提供了一个系统级的线程(Thread),即垃圾收集器线程(Garbage Collection Thread),来跟踪每一块分配出去的内存空间,当Java 虚拟机(Java Virtual Machine)处于空闲循环时,垃圾收集器线程会自动检查每一快分配出去的内存空间,然后自动回收每一快可以回收的无用的内存块。&&&& 垃圾收集器线程是一种低优先级的线程,在一个Java程序的生命周期中,它只有在内存空闲的时候才有机会运行。它有效地防止了内存渗漏体的出现,并极大可能地节省了宝贵的内存资源。但是,通过Java虚拟机来执行垃圾收集器的方案可以是多种多样的。&下面介绍垃圾收集器的特点和它的执行机制:&&&& 垃圾收集器系统有自己的一套方案来判断哪个内存块是应该被回收的,哪个是不符合要求暂不回收的。垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用System. gc 方法来"建议"执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。&垃圾收集器的主要特点有:&1.垃圾收集器的工作目标是回收已经无用的对象的内存空间,从而避免内存渗漏体的产生,节省内存资源,避免程序代码的崩溃。&2.垃圾收集器判断一个对象的内存空间是否无用的标准是:如果该对象不能再被程序中任何一个"活动的部分"所引用,此时我们就说,该对象的内存空间已经无用。所谓"活动的部分",是指程序中某部分参与程序的调用,正在执行过程中,尚未执行完毕。&3.垃圾收集器线程虽然是作为低优先级的线程运行,但在系统可用内存量过低的时候,它可能会突发地执行来挽救内存资源。当然其执行与否也是不可预知的。&4.垃圾收集器不可以被强制执行,但程序员可以通过调用System. gc方法来建议执行垃圾收集器。&5.不能保证一个无用的对象一定会被垃圾收集器收集,也不能保证垃圾收集器在一段Java语言代码中一定会执行。因此在程序执行过程中被分配出去的内存空间可能会一直保留到该程序执行完毕,除非该空间被重新分配或被其他方法回收。由此可见,完全彻底地根绝内存渗漏体的产生也是不可能的。但是请不要忘记,Java的垃圾收集器毕竟使程序员从手工回收内存空间的繁重工作中解脱了出来。设想一个程序员要用C或C++来编写一段10万行语句的代码,那么他一定会充分体会到Java的垃圾收集器的优点!&6.同样没有办法预知在一组均符合垃圾收集器收集标准的对象中,哪一个会被首先收集。&7.循环引用对象不会影响其被垃圾收集器收集。&8.可以通过将对象的引用变量(reference variables,即句柄handles)初始化为null值,来暗示垃圾收集器来收集该对象。但此时,如果该对象连接有事件监听器(典型的 AWT组件),那它还是不可以被收集。所以在设一个引用变量为null值之前,应注意该引用变量指向的对象是否被监听,若有,要首先除去监听器,然后才可以赋空值。&9.每一个对象都有一个finalize( )方法,这个方法是从Object类继承来的。&10.finalize( )方法用来回收内存以外的系统资源,就像是文件处理器和网络连接器。该方法的调用顺序和用来调用该方法的对象的创建顺序是无关的。换句话说,书写程序时该方法的顺序和方法的实际调用顺序是不相干的。请注意这只是finalize( )方法的特点。&11.每个对象只能调用finalize( )方法一次。如果在finalize( )方法执行时产生异常(exception),则该对象仍可以被垃圾收集器收集。&12.垃圾收集器跟踪每一个对象,收集那些不可到达的对象(即该对象没有被程序的任何"活的部分"所调用),回收其占有的内存空间。但在进行垃圾收集的时候,垃圾收集器会调用finalize( )方法,通过让其他对象知道它的存在,而使不可到达的对象再次"复苏"为可到达的对象。既然每个对象只能调用一次finalize( )方法,所以每个对象也只可能"复苏"一次。&13.finalize( )方法可以明确地被调用,但它却不能进行垃圾收集。&14.finalize( )方法可以被重载(overload),但只有具备初始的finalize( )方法特点的方法才可以被垃圾收集器调用。&15.子类的finalize( )方法可以明确地调用父类的finalize( )方法,作为该子类对象的最后一次适当的操作。但Java编译器却不认为这是一次覆盖操作(overriding),所以也不会对其调用进行检查。&16.当finalize( )方法尚未被调用时,System. runFinalization( )方法可以用来调用finalize( )方法,并实现相同的效果,对无用对象进行垃圾收集。&17.当一个方法执行完毕,其中的局部变量就会超出使用范围,此时可以被当作垃圾收集,但以后每当该方法再次被调用时,其中的局部变量便会被重新创建。&18.Java语言使用了一种"标记交换区的垃圾收集算法"。该算法会遍历程序中每一个对象的句柄,为被引用的对象做标记,然后回收尚未做标记的对象。所谓遍历可以简单地理解为"检查每一个"。&19.Java语言允许程序员为任何方法添加finalize( )方法,该方法会在垃圾收集器交换回收对象之前被调用。但不要过分依赖该方法对系统资源进行回收和再利用,因为该方法调用后的执行结果是不可预知的。&通过以上对垃圾收集器特点的了解,你应该可以明确垃圾收集器的作用,和垃圾收集器判断一块内存空间是否无用的标准。简单地说,当你为一个对象赋值为null并且重新定向了该对象的引用者,此时该对象就符合垃圾收集器的收集标准。&判断一个对象是否符合垃圾收集器的收集标准,这是SUN公司程序员认证考试中垃圾收集器部分的重要考点(可以说,这是唯一的考点)。所以,考生在一段给定的代码中,应该能够判断出哪个对象符合垃圾收集器收集的标准,哪个不符合。下面结合几种认证考试中可能出现的题型来具体讲解:&Object obj = new Object ( ) ;&我们知道,obj为Object的一个句柄。当出现new关键字时,就给新建的对象分配内存空间,而obj的值就是新分配的内存空间的首地址,即该对象的值(请特别注意,对象的值和对象的内容是不同含义的两个概念:对象的值就是指其内存块的首地址,即对象的句柄;而对象的内容则是其具体的内存块)。此时如果有 obj = null; 则obj指向的内存块此时就无用了,因为下面再没有调用该变量了。&请再看以下三种认证考试时可能出现的题型:&程序段1:&1.fobj = new Object ( ) ;&2.fobj. Method ( ) ;&3.fobj = new Object ( ) ;&4.fobj. Method ( ) ;&问:这段代码中,第几行的fobj 符合垃圾收集器的收集标准?&答:第3行。因为第3行的fobj被赋了新值,产生了一个新的对象,即换了一块新的内存空间,也相当于为第1行中的fobj赋了null值。这种类型的题在认证0考试中是最简单的。&程序段2:&1.Object sobj = new Object ( ) ;&2.Object sobj =&3.Object sobj = new Object ( ) ;&一起jquery,17jquery&4.sobj = new Object ( ) ;&问:这段代码中,第几行的内存空间符合垃圾收集器的收集标准?&答:第2行和第4行。因为第2行为sobj赋值为null,所以在此第1行的sobj符合垃圾收集器的收集标准。而第4行相当于为sobj赋值为null,所以在此第3行的sobj也符合垃圾收集器的收集标准。&如果有一个对象的句柄a,且你把a作为某个构造器的参数,即 new Constructor ( a )的时候,即使你给a赋值为null,a也不符合垃圾收集器的收集标准。直到由上面构造器构造的新对象被赋空值时,a才可以被垃圾收集器收集。&程序段3:&1.Object aobj = new Object ( ) ;&2.Object bobj = new Object ( ) ;&3.Object cobj = new Object ( ) ;&4.aobj =&5.aobj =&6.cobj =&7.aobj =&问:这段代码中,第几行的内存空间符合垃圾收集器的收集标准?&答:第4,7行。注意这类题型是认证考试中可能遇到的最难题型了。&行1-3分别创建了Object类的三个对象:aobj,bobj,cobj行4:此时对象aobj的句柄指向bobj,原来aojb指向的对象已经没有任何引用或变量指向,这时,就符合回收标准。行5:此时对象aobj的句柄指向cobj,所以该行的执行不能使aobj符合垃圾收集器的收集标准。&行6:此时仍没有任何一个对象符合垃圾收集器的收集标准。&行7:对象cobj符合了垃圾收集器的收集标准,因为cobj的句柄指向单一的地址空间。在第6行的时候,cobj已经被赋值为null,但由cobj同时还指向了aobj(第5行),所以此时cobj并不符合垃圾收集器的收集标准。而在第7行,aobj所指向的地址空间也被赋予了空值null,这就说明了,由cobj所指向的地址空间已经被完全地赋予了空值。所以此时cobj最终符合了垃圾收集器的收集标准。 但对于aobj和bobj,仍然无法判断其是否符合收集标准。&总之,在Java语言中,判断一块内存空间是否符合垃圾收集器收集标准的标准只有两个:&1.给对象赋予了空值null,以下再没有调用过。&2.给对象赋予了新值,既重新分配了内存空间。&最后再次提醒一下,一块内存空间符合了垃圾收集器的收集标准,并不意味着这块内存空间就一定会被垃圾收集器收集。
TA的最新馆藏
喜欢该文的人也喜欢[CLR via C#]21. 自动内存管理(垃圾回收机制)
来源:博客园
目录







































一、理解垃圾回收平台的基本工作原理
值类型(含所有枚举类型)、集合类型、String、Attribute、Delegate和Event所代表的资源无需执行特殊的清理操作。
如果一个类型代表着或包装着一个非托管资源或者本地资源(比如数据库连接、套接字、mutex、位图等),那么在对象的内存准备回收时,必须执行资源清理代码。
CLR要求所有的资源都从托管堆分配。
进程初始化时,CLR要保留一块连续的地址空间,这个地址空间最初没有对应的物理存储空间。这个地址空间就是托管堆。托管堆还维护着一个指针,可以称为NextObjPtr。它指向下一个对象在堆中的分配位置。刚开始时,NextObjPtr设为保留地址空间的基地址。IL指令使用newobj创建一个对象。newobj指令将导致CLR执行以下步骤:计算类型(及其所有基类型)所需要的字节数。

加上对象的额外开销的字节数——“类型对象指针”和“同步块索引”。 

CLR检查保留区域是否能分配出相应的字节数。如果托管堆有足够的可用空间,对象将被放入。注意对象这在NextObjPtr指针指向的地址放入的,并且为它分配的字节会被清零。接着,调用类型的实例构造函数(为this参数传递NextObjPtr),IL指令newobj将返回对象的地址。就在地址返回之前,NextObjPtr指针的值会加上对象占据的字节数,这样就会得到一个新的NextObjPtr值,它指向下一个对象放入托管堆时的地址。

托管堆之所以能这么做,是因为它做了一个相当大胆的假设——地址空间和存储是无限的。这个假设显然是荒谬的。所以,托管堆必须通过某种机制来允许它做这样的假设。这种机制就是垃圾回收。
对象不断的被创建,NextObjPtr也在不断的增加,如果NextObjPtr超过了地址空间的末尾,表明托管堆已满,就必须强制执行一次垃圾回收。
二、 垃圾回收算法
每个应用程序都包含一组根。每个根都是一个存储位置,其中包含指向引用类型对象的指针。该指针要么引用托管堆中的一个对象,要么为null。只有引用类型的变量才会被认为是根;值类型的变量永远不被认为是根。
垃圾回收开始执行时,它假设堆中所有对象都是垃圾。第一个阶段为标记阶段。这个阶段,垃圾回收器沿着线程栈向上检查所有根。如果发现一个根引用了一个对象,就进行”标记”。该标记具有传递性。标记好根和它的字段引用的对象之后,垃圾回收器会检查下一个根,并继续标记对象。如果垃圾回收期试图标记先前已经标记了的根,就会停止沿着这个路径走下去。检查好所有根之后,堆中将包含一组已标记和未标记的对象。已标记的对象是通过应用程序的代码可以到达的对象,而未标记的对象是不可达的。不可达的对象就是垃圾,它们的内存是可以回收的。
第二个阶段为压缩(可以理解成"内存碎片整理")阶段。在这个阶段中,垃圾回收器线性遍历堆,以寻找未标记对象的连续内存块。如果这个内存块较小,垃圾回收器会忽略它们。反之,垃圾回收器会把非垃圾的对象移动到这里已压缩堆,其实在这是内存碎片整理或许更会适用。自然的,包含那些”指向这些对象的指针”的变量和CPU寄存器现在都会变得无效。所以,垃圾回收器必须重新访问应用程序的所有根,并修改它们来指向对象的新内存位置。堆内存压缩之后,托管堆的NextObjPtr指针将指向紧接在最后一个非垃圾回收对象之后的位置。

所以,垃圾回收器会造成显著的损失,这是使用托管堆的主要缺点。当然,垃圾回收只在第0代满的时候才会发生。在此之前,托管堆性能远远高于C运行时堆。
三、垃圾回收与调试
当JIT编译器将方法的IL代码编译成本地代码时,JIT编译器会检查两点:定义方法的程序集在编译时没有优化;进行当前在一个调试器中执行。如果这两点都成立,JIT编译器在生成方法的内部根表时,会将变量的生存期手动延长至方法结束。 
四、使用终结操作来释放本地资源
终结是CLR提供的一种机制,允许对象在垃圾回收器回收其内存之前执行一些得体的清理工作。
任何包装了本地资源的类型都必须支持终结操作。简单的说,类型实现了一个命名为Finalize的方法。当垃圾回收期判断一个对象是垃圾时,会调用对象的Finalize方法。
C#团队认为,Finalize方法是编程语言中需要特殊语法的一种方法。在C#中,必须在类名前加一个~符号来定义Finalize方法。

Internal sealed class SomeType {

~SomeType(){

//这里的代码会进入Finalize方法

}

}

  5. 编译上述代码,会发现C#编译器实际是在模块的元数据中生成一个名为Finalize的protected override方法。方法主体被放到try块中,finally块放入了一个对base.Finalize的调用。
  6.实现Finalize方法时,一般都会调用Win32 CloseHandle函数,并向该函数传递本地资源的句柄。
五、对托管资源使用终结操作
永远不要对托管资源使用终结操作,这是有一种非常好的编程习惯。因为对托管资源使用终结操作是一种非常高级的编码方式,只有极少数情况下才会用到。
设计一个类型时,处于以下几个性能原因,应避免使用Finalize方法:可终结的对象要花费更长的时间来分配,因为指向它们的指针必须先放到终结列表中。("终结列表"在第七节会说到)
可终结对象会被提升到较老的一代,这会增加内存压力,并在垃圾回收器判定为垃圾时,阻止回收。除此之外,对该对象直接或间接引用的对象都会提升到较老的一代。("代"在第十三节会说到)
可终结的对象会导致应用程序运行缓慢,因为每个对象在进行回收时,需要对它们进行额外操作。

我们无法控制Finalize方法何时运行。CLR不保证各个Finalize的调用顺序。
六、是什么导致Finalize方法被调用
第0代满 只有第0代满时,垃圾回收器会自动开始。该事件是目前导致调用Finalize方法最常见的一种方式。("代"在第十三节会说到)
代码显式调用System.GC的静态方法Collect
代码可以显式请求CLR执行即时垃圾回收操作。
Windows内存不足
当Windows报告内存不足时,CLR会强制执行垃圾回收。
CLR卸载AppDomain
一个ApppDomain被卸载时,CLR认为该AppDomain不存在任何根,因此会对所有代的对象执行垃圾回收。
CLR关闭
一个进程结束时,CLR就会关闭。CLR关闭会认为进程中不存在 任何根,因此会调用托管堆中所有的Finalize方法,最后由Windows回收内存。
七、终结操作揭秘
应用程序创建一个新对象时,new操作符会从堆中分配内存。如果对象的类型定义了Finalize方法,那么在该类型的实例构造器调用之前,会将一个指向该对象的指针放到一个终结列表(finalization list)中。
终结列表是由垃圾回收器控制的一个内部数据结构。列表中的每一项都指向一个对象,在回收该对象之前,会先调用对象的Finalize方法。
下图1展示了包含几个对象的一个托管堆。有的对象从应用程序的根可达,有的不可达(垃圾)。对象C,E,F,I,J被创建时,系统检测到这些对象的类型定义来了Finalize方法,所有指向这些对象的指针要添加到终结列表中。


垃圾回收开始时,对象B,E,G,H,I和J被判定为垃圾。垃圾回收器扫描终结列表以查找指向这些对象的指针。找到一个指针后,该指针会从终结列表中移除,并追加到freachable队列中。freachable队列(发音是“F-reachable”)是垃圾回收器的内部数据结构。Freachable队列中的每个指针都代表其Finalize方法已准备好调用的一个对象。图2展示了回收完毕后托管堆的情况。


从图2中我们可以看出B,E和H已经从托管堆中回收了,因为它们没有Finalize方法,而E,I,J则暂时没有被回收,因为它们的Finalize方法还未调用。
一个特殊的高优先级的CLR线程负责调用Finalize方法。使用专用的线程可避免潜在的线程同步问题。freachable队列为空时,该线程将睡眠。当队列中有记录项时,该线程就会被唤醒,将每一项从freachable队列中移除,并调用每一项的 Finalize方法。
如果一个对象在freachable队列中,那么意味这该对象是可达的,不是垃圾。
原本,当对象不可达时,垃圾回收器将把该对象当成垃圾回收了,可是当对象进入freachable队列时,有奇迹般的”复活”了。然后,垃圾回收器压缩(内存脆片整理)可回收的内存,特殊的CLR线程将清空freachable队列,并调用其中每个对象的Finalize方法。
垃圾回收器下一次回收时,发现已终结的对象成为真正的垃圾,因为应用程序的根不再指向它,freachhable队列也不再指向它。所以,这些对象的内存会直接回收。
 整个过程中,可终结对象需要执行两次垃圾回收器才能释放它们占用的内存。可在实际开发中,由于对象可能被提升到较老的一代,所以可能要求不止两次进行垃圾回收。图3展示了第二次垃圾回收后托管堆中的情况。


八、Dispose模式:强制对象清理资源
Finalize方法非常有用,因为它确保了当托管对象的内存被释放时,本地资源不会泄漏。但是,Finalize方法的问题在于,他的调用时间不能保证。另外,由于他不是公共方法,所以类的用户不能显式调用它。
类型为了提供显式进行资源清理的能力,提供了Dispose模式。
所有定义了Finalize方法的类型都应该同时实现Dispose模式,使类型的用户对资源的生存期有更多的控制。
九、使用实现了Dispose模式的类型
调用Dispose或Close只是为了能在一个确定的时间强迫对象执行清理;这两个方法并不能控制托管堆中的对象所占用的内存的生存期。这意味着即使一个对象已完成了清理,仍然可在它上面调用方法,但会抛出ObjectDisposedException异常。
建议只有在以下两种情况下才调用Dispose或Close:a)
确定必须清理资源
b)
确定可以安全的调用Dispose或Close,并希望将对象从终结列表中删除,禁止对象提升到下一代,从而提升性能。

十、C#的using语句
如果决定显式地调用Dispose和Close这两个方法之一,强烈建议把它们放到一个异常处理finally中。这样可以保证清理代码得到执行。
Using语句就是一种对第1点进行简化的语法。
十一、手动监视和控制对象的生存期
CLR为每一个AppDomain都提供了一个GC句柄表。该表允许应用程序监视对象的生存期,或手动控制对象的生存期。
在一个AppDomain创建之初,该句柄表是空的。句柄表中的每个记录项都包含以下两种信息:一个指针,它指向托管堆上的一个对象;一个标志(flag),它指出你想如何监视或控制对象。
为了在这个表中添加或删除记录项,应用程序要使用如下所示的System.Runtime.InteropServices.GCHandle类型。
十二、对象复活
前面说过,需要终结的一个对象被认为死亡时,垃圾回收器会强制是该对象重生,使它的Finalize方法得以调用。Finalize方法调用之后,对象才真正的死亡。
需要终结的一个对象会经历死亡、重生、在死亡的”三部曲”。一个死亡的对象重生的过程称为重生。
复活一般不是一件好事,应避免写代码来利用CLR这个”功能”。
十三、代
代是CLR垃圾回收器采用的一种机制,它唯一的目的就是提升应用程序的性能。
一个基于代的垃圾回收器做出了以下几点假设:对象越新,生存期越短。
对象越老,生存期越长。
回收堆的一部分,速度快于回收整个堆。

代的工作原理:托管堆在初始化时不包含任何对象。添加到堆的对象称为第0代对象。第0代对象就是那些新构造的对象,垃圾回收器从未检查过它们。图4展示了一个新启动的应用程序,它分配了5个对象。过会儿,对象C和E将变得不可达。
CLR初始化时,它会为第0代对象选择一个预算容量,假定为256K(实际容量可能有所不同)。所以,如果分配一个新对象造成第0代超过预算,就必须启动一次垃圾回收。假定对象A到E刚好占用256K内存。对象F分配时,垃圾回收器必须启动。垃圾回收器判定对象C和E为垃圾,因为会压缩(内存碎片整理)对象D,使其与对象B相邻。之所以第0代的预算容量为256K,是因为所有这些对象都能装入CPU的L2缓存,使之压缩(内存碎片整理)能以非常快的速度完成。在垃圾回收中存活的对象(A、B和D)被认为是第1代对象。第1代对象已经经历垃圾回收的一次检查。此时的对如图5所示。
一次垃圾回收后,第0代就不包含任何对象了。和前面一样,新对象会分配到第0代中。在图6中,应用程序继续运行,并新分配了对象F到对象K。另外,随着应用程序继续运行,对象B、H和J变得不可达,它们的内存将在某一个回收。
现在,假定分配新对象L会造成第0代超过256KB的预算。由于第0代达到预算,所以必须启动垃圾回收器。开始一次垃圾回收时,垃圾回收器必须决定检查哪些代。
前面说过,当CLR初始化时,他为第0代对象选择了一个预算。同样的,它还必须为第1代选择一个预算。假定为第1代选择的预算为2MB。
垃圾回收开始时,垃圾回收器还会检查第1代占据了多少内存。由于在本例中。第一代占据的内存远远小于2MB,所以垃圾回收器只检查第0代。因为此时垃圾回收器只检查第0代,忽略第1代,所以大大加快了垃圾回收器的速度。但是,对性能最大的提升就是现在不必遍历整个托管堆。如果一个对象引用了一个老对象,垃圾回收器就可以忽略那个老对象的所有内部引用,从而能更快的构造好可达对象的图。
如图7所示,所有幸存下来的第0代对象变成了第1代的一部分。由于垃圾回收器没有检查第1代,所以对象B的内存并没有被回收,即使它在上次垃圾回收时变得不可达。在一次垃圾回收后,第0代不包含任何对象,等着分配新对象。
假定程序继续运行,并分配对象L到对象O。另外,在运行过程中,应用程序停止使用对象G,I,M,是它们变得不可达。此时的托管堆如图8所示。
假设分配对象P导致第0代超过预算,垃圾回收发生。由于第1代中所有对象占据的内存仍小于2MB,所以垃圾回收器再次决定只回收第0代,忽略第1代不可达的垃圾(对象B和G)。回收后,堆的情况如图9所示。
从图9中可以看到,第1代正在缓慢增长。假定第1代的增长导致它所有对象占据的内存刚好达到2MB。这时,随着应用程序的运行,并分配了对象P到对S,使第0代对象达到了它的预算容量。这是的堆如图10所示。
应用程序试图分配对象T时,由于第0代已满,所以必须开始垃圾回收。但是,这次垃圾回收器发现第1代占据的内存超过了2MB。所以垃圾回收器这次决定检查第1代和第0代中的所有对象。两代都被回收之后,托管堆情况如图11所示。

  
4. 像前面一样,垃圾回收后,第0代的幸存者被提升到了第1代,第1代的幸存者被提升到了第2代,第0代再次空出来,准备迎接新对象的到来。第2代中的对象会经过2次或更多次的检查。只有在第1代到达预算容量是才会检查第1代中的对象。而对此之前,一般已经对第0代进行了好几次垃圾回收。
  5. CLR的托管堆只支持三代:第0代、第1代和第2代。第0代的预算约为256KB,第1代的预算约为2MB,第2代的预算容量约为10MB。
十四、
线程劫持
前面讨论的垃圾回收算法有一个很大的前提就是:只在一个线程运行。
在现实开发中,经常会出现多个线程同时访问托管堆的情况,或至少会有多个线程同时操作堆中的对象。一个线程引发垃圾回收时,其它线程绝对不能访问任何线程,因为垃圾回收器可能移动这些对象,更改它们的内存位置。
CLR想要进行垃圾回收时,会立即挂起执行托管代码中的所有线程,正在执行非托管代码的线程不会挂起。然后,CLR检查每个线程的指令指针,判断线程指向到哪里。接着,指令指针与JIT生成的表进行比较,判断线程正在执行什么代码。
如果线程的指令指针恰好在一个表中标记好的偏移位置,就说明该线程抵达了一个安全点。线程可在安全点安全地挂起,直至垃圾回收结束。如果线程指令指针不在表中标记的偏移位置,则表明该线程不在安全点,CLR也就不会开始垃圾回收。在这种情况下,CLR就会劫持该线程。也就是说,CLR会修改该线程栈,使该线程指向一个CLR内部的一个特殊函数。然后,线程恢复执行。当前的方法执行完后,他就会执行这个特殊函数,这个特殊函数会将该线程安全地挂起。
然而,线程有时长时间执行当前所在方法。所以,当线程恢复执行后,大约有250毫秒的时间尝试劫持线程。过了这个时间,CLR会再次挂起线程,并检查该线程的指令指针。如果线程已抵达一个安全点,垃圾回收就可以开始了。但是,如果线程还没有抵达一个安全点,CLR就检查是否调用了另一个方法。如果是,CLR再一次修改线程栈,以便从最近执行的一个方法返回之后劫持线程。然后,CLR恢复线程,进行下一次劫持尝试。
所有线程都抵达安全点或被劫持之后,垃圾回收才能使用。垃圾回收完之后,所有线程都会恢复,应用程序继续运行,被劫持的线程返回最初调用它们的方法。
实际应用中,CLR大多数时候都是通过劫持线程来挂起线程,而不是根据JIT生成的表来判断线程是否到达了一个安全点。之所以如此,原因是JIT生成表需要大量内存,会增大工作集,进而严重影响性能。
十五、大对象
任何85000字节或更大的对象都被自动视为大对象。
大对象从一个特殊的大对象堆中分配。这个堆中采取和前面小对象一样的方式终结和释放。但是,大对象永远不压缩(内存碎片整理),因为在堆中下移850000字节的内存块会浪费太多CPU时间。
大对象总是被认为是第2代的一部分,所以只能为需要长时间存活的资源创建大对象。如果分配短时间存活的大对象,将导致第2代被更频繁地回收,进而会损害性能。
免责声明:本站部分内容、图片、文字、视频等来自于互联网,仅供大家学习与交流。相关内容如涉嫌侵犯您的知识产权或其他合法权益,请向本站发送有效通知,我们会及时处理。反馈邮箱&&&&。
学生服务号
在线咨询,奖学金返现,名师点评,等你来互动}

我要回帖

更多关于 js 内存回收 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信