关于垃圾回收的一个问题

coolnick 2007-05-16 06:27:21
比较典型的垃圾回首策略有两种:
1、引用记数(Reference Counting)
2、追踪回收(Mark-Sweep)
众所周知,引用记数存在交叉引用的问题;

很多大师写的书中讲到追踪回收可以避免这个问题,
但反复思考,感觉追踪回收同样也存在交叉引用的问题。

假设用链表来组织管理内存,链表中每个结点除了包含实际
使用的内存空间外,还包含其它的一些信息,比如使用状态,
“占用”或者“空闲”。
追踪回收的思想在于:
1、每次进行垃圾回收之前,先将所有状态为“占有”
的结点的状态修改为“测试”;
2、遍历进程空间(为简化起见,不考虑多线程),包括数据段、
堆、栈地址空间,逐一检查地址,如果地址指向状态为“测试”
的结点,则将结点的状态修改回“占用”;
3、遍历完之后,重新遍历链表,清楚所有状态为“测试”的结点,
因为这些结点都是垃圾,需要回收。

以上就是对追踪回收的简单描述,很显然,同样存在交叉引用的问题。
如果某个结点内部存在一个指针指向另一个结点,反之另一个结点内部
也存在一个指针指向该结点,那在上述第2步中,这两个结点的状态都会
修改回“占用”,从而永远不会被回收。

不知道各位大虾如何理解这个问题?



...全文
386 5 打赏 收藏 转发到动态 举报
写回复
用AI写文章
5 条回复
切换为时间正序
请发表友善的回复…
发表回复
coolnick 2007-05-20
  • 打赏
  • 举报
回复
感谢shan_ghost的回答, 回答得深入浅出,而且很有文采。

不过我这里还要补充一下。
我之所以提出这个问题,是在看完Bill Blunden的
“Memory Management: Algorithms and Implementation in C/C++”这本书后提出来的。
在本书的第5章“Automatic Memory Management”第2节“malloc() Version 5:Mark-Sweep”
给出了Mark-Sweep的简化的算法实现。
经过这几天的思考,当然,更由于shan_ghost的回答,觉得书中给出的Mark-Sweep算法有问题。
具体说明如下:
void MarkSweepMemoryManager::trace()
{
.......
mark();
sweep();
return;
}
void MarkSweepMemoryManager::mark()
{
//set all OCCUPIED blocks to TESTING
............

//traverse the stack and heap
//if find address to TESTING block, set to OCCUPIED

traverseStack();
traverseHeap(); /*问题出在这里,不应该遍历堆空间,也就是这句多余*/
return;
}
以上的代码很清晰,成员函数trace就是自动内存程序的入口点。其中函数mark标记出垃圾,
而函数sweep则是回收垃圾。 问题就出在函数mark标记垃圾的过程中,只应该遍历栈空间
(也应该遍历全局数据区),而不应该遍历堆空间,否则同样存在交叉引用的问题。

shan_ghost 2007-05-19
  • 打赏
  • 举报
回复
迷糊了不是^_^?

可以把所有这些数据看作一系列的树(标准说法叫森林);但这些树结构并不标准,可能存在网状结构;

所有的树都是以“不需回收型数据”为根节点的,将来的搜索也只是从这些根节点开始。

(基础知识:树的某个分支也是树,称为子树。所以这个搜索可以这样用伪码来表示:从根节点开始,搜索左1子树;搜索左2子树……搜索右子树——一个典型的递归算法)。


如何判断删除标准呢?

就是:

1、凡是从 以“不需回收型数据”为根节点的树 开始的搜索中遍历到的节点,将来都可以通过某种方式访问到,因此必须保留;

2、在上述过程中遍历不到的节点,就是不可能再被访问的,因此必须删除。





举个例子:
fun a()
{
map<int,classtype*> * p = new map<int,classtype>; //new一个map对象
classtype * p2 = new classtype(); //new一个classtype对象
//制造个交叉引用
p->add_pair(makepair(1,p2)); //将这个classtype 对象加入map
p2.set_parent_map(p); //设置这个classtype对象的父map对象为p指向的对象
return
}

只要你有一定的c/c++开发经验,那么你肯定会大声喊出来:什么啊,你的垃圾代码有内存泄露!

没关系,我故意的。我想说明的是即使有交叉引用,仍然可以出现内存泄露(废话!)。


但为什么有内存泄露?我的意思是根本原因是什么?

根本原因是:new得数据是一个运行期的动态过程;要想在程序中控制这些数据,就必须把它关联到一个编译期有效的变量上,然后通过这个编译期有效变量去间接引用。否则就没有可以访问它的办法,就是内存泄露。


那么什么是编译期有效变量?

静态变量;全局变量;以及局部变量(栈变量)——它们,也就是我前面说的"不需回收型数据”;或者说,它们有“自然回收机制”,因此永不必在代码中刻意控制。



于是,我们可以把内存泄露重新表述为:
由于代码逻辑问题,导致new得的数据与编译期有效变量间的关联被破坏,造成new得的内存空间无法再在程序逻辑中得到管理。

而垃圾回收则可以表述为:
自动检测内存泄露,并自动回收泄露内存的一种机制。


于是:
引用计数就是“跟踪到new得数据的每次关联(以及这些关联的撤销);如果已无任何关联,则说明new得的数据已进入泄露状态,应自动删除它”。
这种办法由于无法区分是从编译期有效数据过来(包括间接过来)的“有效关联”,还是从已“泄露”的new得数据过来的“无效关联”,使得交叉引用情况下的内存泄露无法被检测。

而追踪回收机制则从编译器有效数据出发,去查找它对其他数据的链接;凡是能直接/间接被编译期有效数据关联上的内存区域,它们必定都不是泄露的内存;否则,它们就是泄露的内存。


相比之下,引用计数消耗更小,回收也更为“实时”,但有无法处理交叉引用的缺陷;而追踪回收机制消耗大(要遍历所有的指针/引用),回收时机不可确定,但可以处理任何情况。
coolnick 2007-05-18
  • 打赏
  • 举报
回复
楼上两位所言即是, 垃圾回收是从"不需回收型数据开始"。
但是遍历完“不需回收型数据区”之后,仍然还是要遍历“需回收数据区”呀。
而且只有全部遍历完之后才会开始进行垃圾回收,总不能一边遍历一边回收吧。

假设这里只考虑栈与堆, 那么先遍历栈空间,后也要遍历堆空间。
这样不还是存在交叉引用的问题吗?
shan_ghost 2007-05-17
  • 打赏
  • 举报
回复
关键是要把所有对象、变量和指针分为两类,一类是“不需回收”型数据,包括静态、全局以及栈上分配的东西;另一类是“需回收”型数据,就是new/malloc出来的东西(堆上的数据)。

垃圾回收过程只能从不需回收型数据开始。凡是从“不需回收型数据”开始直接、间接都关联不到的“需回收型数据”,就是程序已经不可能通过正常方式访问的数据,就需要被回收。
shan_ghost 2007-05-17
  • 打赏
  • 举报
回复
2、遍历进程空间(为简化起见,不考虑多线程),包括数据段、
堆、栈地址空间,逐一检查地址,如果地址指向状态为“测试”
的结点,则将结点的状态修改回“占用”;


从其它指针开始检查而不是从节点发起。即如果有 A->B B->C C->A,那么除非有非动态的指针指向三个节点之一,否则这三个节点都会被回收。

举例来说,定义两个函数 fa和fb 如下:
fa()
{
A * pa;
pa = new A(new B(1)); //A->B ,B->A
fb();
//1 执行到这里开始垃圾回收
....
}

fb()
{
A * pb;
pb = new A(new B(2));//A->B, B->A
}

那么,当执行到//1 处时,

pb已经无效,因此不会处理它所指向的节点;于是pb指向的A的对象仍为测试态,最终被销毁;同时A对象内部指向的B的对象也不会被找到,于是也被销毁;

pa仍然有效,于是标记它指向的A的对象为占用;顺藤摸瓜,这个A对象内部指向的B的对象也被标记为占用,于是不会被销毁。

3,881

社区成员

发帖
与我相关
我的任务
社区描述
C/C++ 其它技术问题
社区管理员
  • 其它技术问题社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧