一、垃圾回收机制
虽然智能指针帮助开展者简化了堆内存回收问题,但是它需要开发者显式声明,需要使用时判断等,还是不够便捷。而像java、Python、C#等开发语言直接支持垃圾回收机制。程序开发上,通常会将不再使用或没有任何指针指向的内存空间称为“垃圾”,将这些“垃圾”收集起来再次利用的机制就是“垃圾回收机制”。
垃圾回收一般采用对象引用计数或对象关系图谱跟踪这两个策略。
对象引用计数,就是使用系统记录对象被引用(引用、指针)的次数,当对象引用的次数变为0是,该对象内存就被视为“垃圾”而回收。使用引用计数的优点就是实现简单,计数的增减与对象使用紧密结合,不会造成程序暂停,也不会对系统缓存或交换内存造成冲击,“副作用”较少。但缺点就比较难处理“环形引用”的问题,另外每个对象内存管理都引入计数,还是有不少额外内存开销。
对象关系跟踪处理,通过跟踪对象内存使用的关系图,通过标记-清除,标记-整理,标记拷贝等垃圾回收算法来实现垃圾内存回收。
标记-清除,分为标记阶段和清扫阶段。主要工作是在标记阶段,将程序中正使用的对象视为"根对象",从根对象开始查找他们所引用的堆空间,斌在这些堆空间做标记,当标记结束后,所有被标记的对象为可达对象或者活对象,而没有被标记的对象被认为是“垃圾”,这些垃圾在清扫阶段就会被回收,清扫过后,会出现内存碎片问题。
标记-整理,和标记-清除类似,只是在标记完成之后,不再变量所有对象执行清扫,而是将活对象向“左”靠齐重新整理,解决垃圾回收和内存碎片问题。但是由于移动了活的对象操作,因此会造成堆对象引用的更新。
标记-拷贝,该算法会将堆空间划分为from、to两部分,系统只从from部分分配内存,当from部分分配满时,就开始垃圾回收:在from堆空间查找出所有活对象,并拷贝到to堆空间内及紧凑排列。然后将from整个空间清除,最后交换from和to空间角色,即from空间变为to空间,to空间为from空间,系统从新的from空间分配内存。这个算法会造成堆内存利用率只有一半。
二、c++最小垃圾回收支持
2.1 最小垃圾回收支持由来
c/c++一些第三方的库已经支持标记-清除方法的垃圾回收,比如一个比较著名的C/C++垃圾回收库-Boehm。该垃圾回收器需要程序员使用库中的堆内存分配函数显式地替代 malloc,继而将堆内存的管理交给垃圾回收器来完成垃圾回收。不过由于C/C++中指针类型的使用非常灵活,这样的库在实际使用中会有一些限制,可移植性也不好。
为了解决垃圾回收中的安全性和可移植性问题,在2007年,惠普的Hans-J.Bochm (Boehm的作者)和赛门铁克的Mike Spertus共同向C++委员会递交了一个关于C++中垃圾回收的提案。该提案通过添加 gc_forbidden、gc_relaxed、gc_required、gc_safe、gc_strict 等关键字来支持C++语言中的垃圾回收。该提案甚至可以让程序员显式地要求垃圾回收。刚开始这得到了大多数委员的支持,后来却在标准的初稿中删除了,原因是该特性过于复杂,并且还存在一些问题(比如与显式调用析构函数的现有的库的兼容问题等)。所以,Boehm和Spertus对初稿进行了简化,仅仅保留了支持垃圾回收的最基本的部分,即通过对语言的约束,来保证安全的垃圾回收。这也是我们现在看到的C++11标准中的“最小垃圾回收支持”的历史来由。
2.2 c++11标准的最小垃圾回收支持试水
c++11标准做了最小垃圾回收支持,即设计了安全派生指针,支持以下操作:
- 在解引用基础上的引用,例如:&*p。
- 定义明确的指针操作,例如:p+1。
- 定义明确的指针转换,例如:static_cast<void*>(p)。
- 指针与整型之间的reinterpret_cast转移,例如:reinterpret_cast<long long>(p)。
除了智能指针及上述操作支持外,c++11标准在头文件 <memory>还提供了垃圾收集相关函数支持,但又在新的c++23标准中移除:
//(C++11)(C++23 中移除)
/*
*声明一个对象不能被回收:声明 p 所引用的对象可抵达。可抵达对象将不为垃圾收集器删除,
*或不被泄漏检测器认为是泄露,即使所有指向它的指针都被销毁。对象可以声明为多次可抵达,
*该情况下会需要多次调用 *std::undeclare_reachable 以移除此属性。
*/
void declare_reachable( void* p );
/*
*声明一个对象可以被回收:移除指针p所引用对象的可抵达状态,若先前为std::declare_reachable所设。
*若多次声明对象为可抵达,则需要相等次数的到 undeclare_reachable 的调用移除此属性。
*一旦对象不再声明为可抵达,且没有指针引用它,则它可能为垃圾收集器所回收,
*或被泄露检测器报告为泄露。
*/
template< class T > T* undeclare_reachable( T* p );
/*
*告诉垃圾收集器或泄漏检测器,指定的内存区域(从 p 所指向的字节开始的 n 个字节)不含可追踪指针。
*若区域的任何部分在分配的对象内,则同一对象中必须含有整个区域
*/
void declare_no_pointers( char *p, std::size_t n );
/*解除先前注册std::declare_no_pointers 的效果。*/
void undeclare_no_pointers( char *p, std::size_t n );//(函数)
/*列出指针安全模式(枚举)*/
enum class pointer_safety { relaxed, preferred, strict};
/*返回当前的指针安全模式,即获得实现定义的指针安全模式,它是 std::pointer_safety 类型的值。*/
std::pointer_safety get_pointer_safety() noexcept;
在C++11的规则中,最小垃圾回收支持是基于安全派生指针这个概念的。程序员可以通过get_pointer_safety函数查询来确认编译器是否支持这个特性。get_pointer_safety的原型如下:
pointer_safety get_pointer_safety() noexcept其返回一个 pointer_safety 类型的值。如果该值为 pointer_safety::strict,则表明编译器支持最小垃圾回收及安全派生指针等相关概念,如果该值为 pointer_safety:relax 或是 pointer_safety:preferred,则表明编译器并不支持,基本上跟没有垃圾回收的C和C++98一样。不过按照一些解释,pointer_safety:preferred和pointer_safety:relax也略有不同,前者垃圾回收器可能被用作一些辅助功能,如内存泄露检测或检测对象是否被一个错误的指针解引用(事实上,几乎没有编译器实现了最小垃圾回收支持,甚至连get_pointer_safety 这个函数接口都还没实现)。
此外,如果编译器支持到最小垃圾回收支持的话,程序员在代码中出现了指针不安全使用的状况,C++11允许程序员通过一些API来通知垃圾回收器不得回收该内存。C++11的最小垃圾回收支持使用了垃圾回收的术语,即需声明该内存为“可到达”的。
void declare_reachable( void* p );
template <class T>T *undeclare_reachable(T *p) noexcept;
declare_reachable()显式地通知垃圾回收器某一个对象应被认为可达的,即使它的所有指针都对回收器不可见。undeclare_reachable()则可以取消这种可达声明。
#include <iostream>
#include <memory>
using namespace std;
void func1(void)
{
int *pi = new int(1);
declare_reachable(pi); //在pi被隐藏前声明为可达的
int *qi = (int*)((long long)pi^2023);
cout << "*pi = " << *pi << "\n";
cout << "*qi = " << *qi << "\n";
//解除可达声明
qi = undeclare_reachable<int>((int*)((long long)qi^2023));
*pi = 11;
cout << "*pi = " << *pi << "\n";
cout << "*qi = " << *qi << "\n";
*qi = 22;
cout << "*pi = " << *pi << "\n";
cout << "*qi = " << *qi << "\n";
}
int main(int argc, char* argv[])
{
func1();
return 0;
}
//out log
*pi = 1
*qi = 538976288 //随机
*pi = 11
*qi = 11
*pi = 22
*qi = 22
上述代码是一个能够运行的例子。这里,我们在p指针被不安全派生(隐藏)之前使用declare reachable 声明其是可达的。这样一来,它会被垃圾回收器忽略而不会被回收。而在我们通过可逆的异或运算使得q指针指向p所指对象时,我们则使用了undeclare reachable来取消可达声明。注意undeclare reachable不是通知垃圾回收器 p所指对象已经可以回收。实际上,declare reachable和undeclare_reachable只是确立了一个代码范围,即在两者之间的代码运行中,p所指对象不会被垃圾回收器所回收。
declare_reachable只需要传入一个简单的void*指针,但undeclare reachable却被设计为一个函数模板。这是一个极不对称的设计。但事实上undeclare_reachable使用模板的主要目的是为了返回合适类型以供程序使用。而垃圾回收器本来就知道指针所指向的内存的大小,因此 declare reachable 传入 void*指针就已经足够了。
有的时候程序员会选择在一大片连续的堆内存上进行指针式操作,为了让垃圾回收器不关心该区域,也可以使用declare_no_pointers 及undeclare_no_pointers 函数来告诉垃圾回收器该内存区域不存在有效的指针。
void declare_no_pointers(char *p,size_t n) noexcept;
void undeclare_no_pointers(char *p, size_t n) noexcept;
其使用方式与declare_reachable及undeclare_reachable类似,不过指定的是从p开始的连续n的内存。
此外,C++11标准中对指针的垃圾回收支持仅限于系统提供的new 操作符分配的内存,而 malloc分配的内存则会被认为总是可达的,即无论何时垃圾回收器都不予回收。因此使用malloc等的较老代码的堆内存还是必须由程序员自己控制。
2.3 基于boost::pool的垃圾回收机制
Pool库 的作者是 Steve Cleary,boost::pool库提供了一个内存池分配器,它是一个工具,用于管理在一个独立的、大的分配空间里的动态内存。当你需要分配和回收许多不的对象或需要更高效的内存控制时,使用内存池是一个好的解决方案。
boost::pool每次向系统申请一大块内存,然后分成同样大小的多个小块,形成链表连接起来。每次分配的时候,从链表中取出头上一块,提供给用户。链表为空的时候,pool继续向系统申请大块内存。
另外还有在此基础上提供的boost::object_pool,它与boost::pool的区别在于pool需要指定每次分配的块的大小,object_pool需要指定每次分配的对象的类型。
要注意的是,boost::pool库虽然可以按开发者意愿实现垃圾自动回收或主动回收,但它本质上不是一种通用的垃圾回收机制,仅是对象管理库。