堆内存管理:智能指针与垃圾回收
显式内存管理
- 野指针
- 重复释放
- 内存泄漏
C++11 的智能指针
- unique_ptr
- shared_ptr
- weak_ptr
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> up1(new int(11));
// unique_ptr<int> up2 = up1; // error, 不能复制
std::cout << *up1 << std::endl; // 11
std::unique_ptr<int> up3 = std::move(up1); // up3 是数据唯一的 unique_ptr 智能指针
std::cout << *up3 << std::endl; // 11
std::cout << *up1 << std::endl; // error, 但偶尔可能输出正确的值,但是不安全
up3.reset(); // 显式释放内存
up1.reset(); // 不会导致运行时错误
std::cout << *up3 << std::endl; // 运行时错误
std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;
std::cout << *sp1 << std::endl; // 22
std::cout << *sp2 << std::endl; // 22
sp1.reset();
std::cout << *sp2 << std::endl; // 22
return 0;
}
- weak_ptr 可以指向 shared_ptr 指针指向的内存对象,却并不拥有该内存。而使用 weak_ptr 的 lock 成员,可以返回其指向内存的一个 shared_ptr 对象,且在所指向内存已经失效时,返回空值。
#include <iostream>
#include <memory>
void check(std::weak_ptr<int> &wp)
{
std::shared_ptr<int> sp = wp.lock();
if (sp != nullptr) {
std::cout << "Still " << *sp << std::endl;
} else {
std::cout << "Pointer is invalid." << std::endl;
}
}
int main()
{
std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;
std::weak_ptr<int> wp = sp1;
std::cout << *sp1 << std::endl; // 22
std::cout << *sp2 << std::endl; // 22
check(wp); // Still 22
sp1.reset();
std::cout << *sp2 << std::endl; // 22
check(wp); // Still 22
sp2.reset();
check(wp); // Pointer is invalid.
return 0;
}
智能指针虽然好,但是还是要显式使用。
垃圾回收的分类
Garbage Collection
Language | Support |
---|---|
C++ | 部分 |
Java | 支持 |
Python | 支持 |
C | 不支持 |
C# | 支持 |
Ruby | 支持 |
PHP | 支持 |
Perl | 支持 |
Hashkell | 支持 |
Pascal | 不支持 |
垃圾回收主要分为两类:
-
基于引用计数(reference counting grabage collector)的垃圾回收器
引用计数必须存在的问题: retain cycle
-
基于跟踪处理(tracing grabage collector)的垃圾回收器
-
标记——清除(Mark——Sweep)
算法分为两个过程:首先将程序中正在使用的对象视为“根对象”,从根对象开始查找它们所引用的堆空间,并在这些堆空间上做标记。当标记结束后,所有被标记的对象就是可达对象(Reachable Object)或活对象(Live Object),而没有被标记的对象被认为是垃圾,在第二步的清扫(Sweep)阶段会被回收掉。
特点式活的对象不会被移动,但是其存在大量的内存碎片问题。
-
标记——整理(Mark——Compact)
这种算法标记的方法和标记——清除方法一样,但是标记完后,不再便利所有对象清扫垃圾了,而是将活的对象向“左”靠齐,这就解决了碎片问题。
-
标记——拷贝(Mark——Copy)
将堆空间分为两部分:From和To。刚开始系统从 From 堆空间里分配内内存,当 From 分配满的时候系统就开始垃圾回收:从From堆空间里找出所有活的对象,拷贝到To的堆空间里。这样一来,From的堆空间里就全剩下垃圾了。而对象拷贝到To里之后,在To里式排列紧凑的。接下来需要将From和To交换角色,接着从新的From里开始分配。
该算法堆的利用率只有一半,而且也需要移动活的对象。此外,从某种意义上讲,该算法其实是标记——整理算法的一种实现而已。
-
C++ 与垃圾回收
- Boehm 已经支持 标记——清除 方法的垃圾回收,但是可移植性不好
- 为了解决GC中安全性和可移植性的问题,在2007年,HP 的 Hans-J.Boehm 和 Symantec 的 Mike Spertus 共同向 C++ 委员会提交了一个关于 C++ 中垃圾回收的提案。该提案通过添加 gc_forbidden, gc_relaxed, gc_required, gc_safe, gc_strict 等关键字来支持 C++ 语言中的垃圾回收。由于给特定过于复杂,并且存在问题,后来被标准删除了。所以 Boehm 和 Spertus 对初稿进行了简化,仅仅保留了支持GC的最基本部分,即通过语言的约束,来保证安全的GC。这也是我们看到的C++11标准中的“最小垃圾回收的支持”的历史由来。
- 要保证安全的GC,首先必须知道C/C++语言中什么样的行为可能导致GC出现不安全的情况。简单地说,不安全源自于C/C++语言对指针的“放纵”,即允许过分灵活的使用。
int main()
{
int *p = new int;
p += 10; // 移动指针,可能导致 GC
p -= 10; // 回收原来指向的内存
*p = 10; // 再次使用原本相同的指针可能无效
}
int main()
{
int *p = new int;
int *q = (int *)(reinterpret_cast<long long>(p) ^ 2012); // q 隐藏了 p
// 做了一些其他工作,垃圾回收器可能已经回收了 p 指向对象
q = (int *)(reinterpret_cast<long long>(q) ^ 2012); // 这里 q == p
*q = 10;
/*
* 先将 p 隐藏,然后恢复。
* 在隐藏 p 后,很有可能出发 GC,导致恢复 p 后仍为非法。
*/
}
C++11与最小垃圾回收支持
C++11 新标准为了做到最小的垃圾回收的支持,首先对“安全”的指针进行了定义,或者使用C++11中的术语说,安全派生(safely derived)的指针。安全派生的指针指向由 new 分配的对象或其子对象的指针。安全派生指针的操作包括:
- 在解引用基础上的引用: &*p
- 定义明确的指针操作:p + 1
- 定义明确的指针转换:static_cast<void *>(p)
- 指针和整型之间的 reinterpret_cast: reinterpret_cast<intptr_t>(p).
通过 get_pointer_safety 函数查询编译器是否支持 “安全派生” 特性。
pointer_safety get_pointer_safety() noexcept
// 返回值:
- pointer_safety::strict —— 编译器支持最小垃圾回收及安全派生指针等相关概念
- pointer_safety::relax —— 编译器不支持
— pointer_safety::preferred —— 编译器不支持
如果程序中出现了指针不安全使用的状况,C++11 运行程序员通过一些API来通知GC不得回收该内存。
void declare_reachable(void *p);
template <class T> T* undeclare_rechable(T *p) noexcept;
declare_reachable 显式的通知 GC 某一个对象被认为是可达的,即使它所有指针都对回收器不可见。
undeclare_rechable 则可以取消这种可达声明。
#include <iostream>
#include <memory>
int main()
{
int *p = new int;
std::declare_reachable(p); // 在 p 被 不安全派生 前声明,这样 GC 不会回收
int *q = (int *)((long long)p ^ 2012);
// 解除可达声明
q = undeclare_reachable<int>((int *)((long long)q ^ 2012));
*q = 10;
return 0;
}
- 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;
垃圾回收的兼容性
GC 必然会破坏向后兼容。因此,我们必须限制指针的使用或者使用 declare_reachable/undeclare_reachable, declare_no_pointers/undeclare_no_pointers 来让一些不安全的指针使用免于垃圾回收的检查。因此想让老代码毫不费力地使用垃圾回收,现实情况对大多数代码不太可能。
此外, C++11 标准对指针垃圾回收的支持仅限于 new 操作符分配的内存;
malloc 分配的内存会被总认为是 可达的,即无论何时垃圾回收器都不予回收。