【IT168 技术文档】使用引用计数的方法实现的内存垃圾回收不能自动回收循环引用的内存,循环引用的内存需要使用手工进行释放,但手工释放很可能由于编程人员的失误导致内存泄漏,所以必须检查是否存在内存泄漏。要完成这项任务,只要在程序退出时检查一下哈希表中还有哪些内存的引用计数不为0,就可以知道有哪些内存还没有被释放,内存没有被释放的原因有以下两种。
① 内存是供全局使用的,必须等到要退出时才能释放;
② 内存中有循环引用,导致不能释放。
还需要知道哈希表中保存的内存地址指向的内存是在程序的哪一行分配的,才能定位到具体发生泄漏的位置。C语言中有一个宏可以获取源程序中的行号,下面就用这个宏来实现内存泄漏检查。
内存泄漏只是在软件的DEBUG版本才需要用到,因此下面就来设计一个DEBUG版本的内存垃圾回收。
首先要修改内存中需要保存的数据,除了保存引用计数外,还需要保存分配这块内存的源程序文件名和行号,还要保存分配内存的大小信息。为方便起见,可以将引用计数和内存大小保存在分配内存的头部,行号和文件名保存在分配内存的尾部。
关于获取源程序文件名和行号,可以使用宏_FILE_和_LINE_来获取,要注意的是,_LINE_和_FILE_是调用它们的地方的源文件名和行号,如果简单地在GC_Malloc()函数中使用这两个宏,那么所有调用GC_Malloc()函数的地方获得的源文件名和行号都是 GC_Malloc()函数的源文件名和行号,所有获取的源文件名和行号均相同,显然达不到预想的目的。
预想的目的是为了获取调用GC_Malloc()处的源文件名和行号,因此需要将GC_Malloc()函数定义成宏,这样在调用它的地方就会将编码展开,便能得到正确的调用GC_Malloc()处的源文件名和行号,有助于确认发生内存泄漏的位置及原因。
通常只要在程序的调试版本中检查内存泄漏就可以了,修改后对应的GC_Malloc()和GC_Free()函数如下。
#ifdef_DEBUG
#defineGC_Malloc(size)
{
void*p;
INT*q;
char*psz;
p=malloc(size+DOUBLE_INT_LEN+INT_LEN+strlen(__FILE__)+1);
if(p==NULL)
{
GC_Collect();
p=malloc(size+DOUBLE_INT_LEN+INT_LEN+strlen(__FILE__)+1);
if(p==NULL)
{
returnNULL;
}
}
HashTable_Insert(g_pTable,p,HashInt);
*((INT*)p)=0;
*((INT*)p+1)=size;
q=(INT*)((char*p)+size+DOUBLE_INT_LEN);
*q=__LINE__;
psz=(char*)p+size+DOUBLE_INT_LEN+INT_LEN;
strcpy(psz,__FILE__);
return(void*)((char*)p+DOUBLE_INT_LEN);
}
/** 垃圾内存收集算法的内存释放函数
@param void*p——要释放的内存地址
@return void——无
*/
voidGC_Free(void*p)
{
void*pFree=(void*)((char*)p-DOUBLE_INT_LEN);
free(pFree);
}
#endif
下面就来实现内存泄漏的检查。内存泄漏检查只需要在程序退出时检查哪些内存的引用计数不为0,只要对哈希表做一个遍历操作就可以获取哪些内存的引用计数不为0,编码如下。
/** 垃圾内存收集算法的内存泄漏检查函数
@return void——无
*/
voidGC_CheckMemoryLeak()
{
void*p;
HashTable_EnumBegin(g_pTable);
while((p=HashTable_EnumNext(g_pTable))!=NULL)
{
INT*pRef;
INT*pSize;
INT*pLine;
char*pszFile;
pRef=(INT*)p;
pSize=pRef+1;
if(*pRef !=0) /*判断引用计数是否为0*/
{
pLine=(INT*)((char*)p+*pSize+DOUBLE_INT_LEN);
pszFile=(char*)pLine+INT_LEN;
printf("File:%s,Line:%dhavememoryleak.\n",pszFile,*pLine);
}
}
}
注意:程序中使用了printf()函数将文件名和行号打印出来,这里使用printf()函数只是做一个示意,实际应用中可能需要改成其他类型的信息输出函数。
可以使用函数调用来实现在程序退出时自动调用GC_CheckMemory()函数。
atexit(GC_CheckMemoryLeak);
这里使用了C标准库的atexit()函数,这个函数是在整个程序退出时调用它的参数指向的函数来执行,但是调用的时间比全局变量的释放要早,所以如果使用了全局变量,全局变量也被当作泄漏报告出来。