- 注册时间
- 2007-12-27
- 最后登录
- 1970-1-1
- 威望
- 星
- 金币
- 枚
- 贡献
- 分
- 经验
- 点
- 鲜花
- 朵
- 魅力
- 点
- 上传
- 次
- 下载
- 次
- 积分
- 40288
- 在线时间
- 小时
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?欢迎注册
×
最近看到很多关于C++标准可能采用GC (Gabage Collection:垃圾收集器)的讨论,讨论的沸沸扬扬的。
C++标准的变换,通常我不会去关心,毕竟大部分写程序时,我用的都是一些最基本的C/C++功能,甚至于连虚拟函数都很少使用。
而像模板类,STL之类的功能,虽然也会偶尔用一下,但是基本很少使用。最重要的是,提供这些新功能以后,如果不去使用,影响也
不会很大,我照样可以用过去的开发模式去开发。
不过最近看到一篇关于C++中将可能采用的GC的使用方法,觉得要有问题了:
http://blog.csdn.net/g9yuayon/archive/2007/07/23/1702694.aspx
按照上面的说明,在缺省模式下,将会使用GC安全模式,也就是说,一旦这个方案被采用,如果使用默认模式编译,就会使用GC功能。
如果GC的使用过程真的能够对程序员完全透明(比如Java之中),那也就是罢了;不过根据我对GC的了解,在C++中使用GC,要做到完全对
程序员透明实在太难了,对于不了解GC行为的程序员,很可能会遇上一些莫名其妙的问题。
我的观点是在C++里面引入GC是可以的,但是绝对不能作为默认的选项,这个是由C++的语言特性(从C里面继承了太多的东西了)决定的。
当然这仅仅是个人观点,仅供参考。
要详细解释这些问题,先要了解一下GC的原理,下面先以Java为例子介绍一下GC的原理,然后在比较一下C++语言特性的不同以及带来的问题:
GC的原理是计算机系统会事先给用户程序分配了一个充分大的内存空间,然后每次用户程序调用new来为一个对象分配内存时,
直接从那块预分配的空间里面划分一块空闲空间。所有分配的内存都不需要释放,直到这个预先分配的内存空间被用完时,计算机会
开始一个垃圾内存收集过程,来判断那些内存空间实际上现在已经用不上了,如果发现某块已经不会被使用了,就释放。
而判断内存空间是否已经用不上,通常用下面的方法:
首先,我们将所有在GC中分配的内存块全部打上一个空闲的标志
然后我们扫描整个数据空间,查找数据空间中所有指针数据,如果发现某个指针正好指向一块GC中的内存块,那么我们就把
这个内存块的标记改为使用中。(同样,如果这个内存块中又包含指针数据,又要判断这个指针是否指向GC中的内存块)
在所有指针被扫描以后,那些标志还是"空闲"的内存块就是真正空闲的内存空间,我们就可以释放了(程序数据中没有保存那些内存块
的指针,所以它们不会再被使用了)
当然上面有个问题,就是我们要扫描的数据空间是什么,如何去判断数据空间中的数据是否指针数据(其实Java中应该是引用)。
通常一个程序所用到的所有数据空间包括:
i)堆栈空间,保存各种局部变量,通常,堆栈空间中的数据在做垃圾收集时是很难判断其数据类型的,GC的通常做法就是认为堆栈空间中
所有数据都有可能是指针,然后全部把它们当成指针处理。当然如果非常不幸某个不是指针的局部变量的值凑巧等于某个GC中内存块
内存的起始地址,GC就会错误的认为那块内存不空闲,从而不会释放。
ii)静态数据空间,保存各种全局变量。通常,编译器能够实现知道那些全局变量是指针,那些不是。
iii)如果是C/C++语言,还存在不通过GC分配的动态分配内存,我们称为堆空间,堆空间中的数据在传统的C/C++里面也是很难判断数据类型的。
而即使采用了新标准,对于用户不采用GC管理的数据,通常我们也应该很难去维护其类型信息,特别是对于简单数据类型。特别是在为了同过去
数据兼容的情况下面,我们无法为这些数据中另外添加动态类型信息。
iv)GC管理的数据类型。通常,GC管理的数据类型中都动态维护了数据的类型信息。所以对于这些数据,我们很容易去判断对应的
数据是否指针,以及如果它是对象,内部是否包含指针数据。
v)此外还有在寄存器中的数据,这些数据通常同堆栈空间中的数据类似处理。
而我们在扫描数有数据空间时,我们就会扫描所有i),iii),v)类中的数据,把所有连续的4个字节(起始地址是4的倍数)看成一个指针,
然后判断这个指针是否指向一个gc中内存块。如果是,将那个内存块标上“使用中”。
然后我们再扫描ii)中所有指针数据,同样,如果对应的指针指向一个gc中内存块,将那个内存块标上"使用中"。
最后,对于GC中的任何一个被标上“使用中”的内存块,判断它是否是指针或包含指针数据(Java中只可能是对象,不可能直接是指针),
如果是指针或包含指针,同样判断这些指针是否指向gc中内存块,如果是,把对应内存块标上"使用中"。由于这一步扫描过程会继续
产生新的被标上“使用中”的内存块,我们需要递归下去知道没有新的“使用中”的内存块。
上面是GC判断内存是否为垃圾内存的基本原理。
其实如果是在Java中,除了找出使用中的内存,GC还有可能去移动内存块,从而使所有空闲的内存空间连续起来,从而达到消除内存碎片
的目的。当然这个功能在将来的C++版GC中很难去实现,即使提供了,效率也无法同Java版本比较。这里简单介绍一下,让大家进一步了解
一下Java中使用GC的好处:
如果在上面的扫描过程中,我们发现我们找到的指向某个内存块的所有指针都是真正的指针(不包含猜测的指针,而i),iii),v)类都是猜测的指针),
那么我们知道,我们可以移动这个内存块,只要同时修改那些指针值使它们指向移动后的内存块地址就可以了。我们还可以通过对每个内存块
添加一项引用计数来改善可移动内存块的数目。如果我们发现一个内存块被引用的次数已经等于那些可以确定的指针的数目,
那么我们就可以移动它,因为这时,所有i),iii),v)中如果有恰好相同的数据,肯定不是指向这个内存的指针,不然,引用计数值就不对了。
当然,这个引用计数功能同样只能在Java里面使用,我们不可能为C++语言的指针赋值语句产生引用计算,如果这样就同已经存在的代码不兼容了。
下面在看看在C++中和Java中使用GC的不同。
I. 如前面所述,在Java中我们在判断一个指针是否指向GC中分配的内存块时,我们只需要判断这个指针是否正好指向某个被GC分配的内存的首地址就可以了。
但是到了C/C++中,我们就不能这样了。
在C++中,如果一个类S中包含一个成员f,如果我们取了S.f的地址,那么即使不存在指向整个对象的指针,我们也不能释放这个对象;而在Java中,不存在这样的
问题(这是因为Java中其实所有的非基本类型数据成员都是引用)。甚至,C/C++语法允许用户通过成员的地址计算类对象的地址。
也就是说,完全可能在某个时候程序只记录了对象某个成员的地址,但是后来又通过这个成员的地址计算出整个成员的地址,然后访问成员中的所有数据。
所以,当GC来到C++中,我们判断一个指针是否指向GC中分配的内存块时,只要指针落在对应内存块的任何部分,我们都必须认为是指向这个内存块的。
对于真正的指针,这样的判断没有任何副作用,但是对于那些计算机无法判断类型的“猜测指针”(i),iii),v)类中),这会带来很大的问题,这会导致大量空闲内存
块被错误标志成使用中。同样对于动态分配的数组(如 new int[n]),有同样的结论。
假设现在我们写了一个使用GC的C++程序,它调用了一个不使用GC的动态库(所以动态库中非配的内存都属于iii)类)。现在假设
那个动态库分配了400K左右的内存,还没有释放,然后回到我们用GC分配内存的代码中,
现在假设在新的代码中我们使用了一句 new int[10000]的代码(也就是分配了40K的内存),很快这个分配的内存就不再使用了,
然后我们看看如果这时候GC开始收集垃圾会是什么情况。
我们知道,这时我们至少需要扫描400K左右iii)类中的内存,我们全部把它看成了指针,所以大概又100K个指针,我们假设这些全部是随机数据,
我们知道,只要又一个数据(看成指针)正好指向上面分配的40K内存的任何一个位置,我们就无法释放上面的内存。
我们看看这段内存被错误标志为使用中的概率是多大。在现在32位模式下,整数可以有4G中选择,所以一个随机数据不指向上面40K内存的概率是
(1-40K/4G)约等于(1-1/100K),所以100K个指针全部不指向这段数据的概率为(1-1/100K)^(100K),大概为e^(-1)=0.37.
也就是有63%的可能性这段内存在下一次GC中无法被释放。同样如果用户在GC段代码分配的内存量增加到k倍,那么成功释放的概率就降低到e^(-k),
所以如果使用的是new int[100000]的话,那么能够成功被释放的概率就要降低到4.5*10^(-5),也就是说这时我们几乎可以认定GC无法释放这样的内存块了。
而只要这样的new多来几次,很快内存空间耗尽,程序无法运行下去。
当然如果改成64位系统,这种情况会大大改善。
这个问题即使在用户不使用过去的库文件也可能会出现,比如用户程序由于存在递归调用使用了大量的堆栈空间,在GC管理的数据中,对于某些非指针成员,
用户没有显式标上gc_strict(没有仔细学习过新的GC功能的用户估计会如此,这时,编译器也无法判断这个数据是指针类型转化而来), 用户混合使用GC和非GC管理模式
II.现在我们考虑另外一个问题,假设一个传统的C/C++程序员,现在想根据过去的经验写一个动态库,这个动态库里面会动态分配很多数据,但是程序员不希望用户直接获得
这些数据的真正内存地址,比如他要提供这样一些接口:
HANDLE CreateObject(...);
void UseObject(HANDLE obj, ...);
...
ReleaseObject(HANDLE obj);
但是,程序员不希望HANDLE是真正的内存地址(这样可以增加逆向工程的难度)。
按照过去的经验,程序员可能会这样:
HANDLE CreateObject(...){
MyObject *p=new MyObject(...); //GC new
return (HANDLE)(((int)p)^C);
}
这里C是一个常数
同样在任何使用对象的代码里面,程序员首先会将HANDLE通过同C异或,然后转化为指向MyObject的指针。
这样的代码在传统的C++编译器里面不会有任何问题,但是如果这个代码用GC版的C++编译,问题就来了。
如果在CreateObject(...)返回以后,这时GC被激活了,这时由于这个MyObject的真正地址没有保存在内存中(内存中保存的是
p^C),所以GC就会错误的判断这个对象是垃圾对象,从而会错误的删除这个对象。
而一个不懂GC原来的程序员是很可能写出这样的代码的,结果他在遇上内存问题后只能是一头雾水,完全找不出问题所在。
所以说,如果将来C++默认编译模式改成使用GC的话,那么C/C++程序员们最好赶紧乖乖去学习GC的原理,而不像过去一样,实在不行我先不使用这功能还不行吗?要不然
迟早会遇上问题。而且即使努力钻研过,还是有可能遇上这样那样的问题,毕竟gC本身的设计模式是针对Java这样的强类型语言,并且语言中没有显示的指针的使用的。 |
|