1.引言
不管在哪种系统平台/编程语言下,内存资源管理是非常重要的问题,稍不留意就会导致内存泄漏,更有甚者访问非法空间,导致错误。说到底,没有在合适的时机释放对象,或者访问了已经释放的资源。在有垃圾回收的语言中,由平台环境负责资源的及时回收;在C++中则需要程序员自己把握,在一些多线程状态下,对象资源的释放时机常常不好把握,导致了各种各样的问题。为什么大家喜欢用带有GC功能的语言做开发,是因为少了太多的心智负担(JAVA, C#, Python ...)。
2.C++下的资源管理方式
2.1 针对原生指针的资源管理
针对原生指针的管理方式,学校老师常常这么教:谁创建、谁释放。要配对,不过现实往往没那么理想,在单线程处理程序中这样没有问题,但是在多线程中有时候做不到。我现在通常的做法是做一个簿记工作,对于系统中大量使用的对象资源,尤其是跨线程使用的,会集中管理登记,并配以状态标记,确认对象使用完毕后再行释放。对象自身跟随业务状态变化,有明确的开始和结束状态。
2.2 通过智能指针进行资源管理
在C++ 中有多种类型的智能指针,有些被重用,而有些却被放弃,在陈硕的《Linux多线程服务端编程》中,推荐使用shared_ptr以及weak_ptr进行资源管理。此处梳理一下C++中的智能指针。看看各自如何使用,适合在什么场景下使用。
2.2.1 unique_ptr
unique_ptr负责独占对应对象的所有权,一旦unique_ptr析构,那么对应对象自动销毁,unique_ptr对应对象的控制权可以转移,但不可拷贝,具有独占性。可通过实例查看。
class A
{
public:
A()
{
std::cout << "A construct." << std::endl;
a = 10;
b = 11;
}
~A()
{
std::cout << "A destruct." << std::endl;
}
void func()
{
std::cout << "A::func()" << std::endl;
std::cout << a << " " << b << std::endl;
}
private :
int a;
int b;
};
void unique_ptr_test()
{
{
std::unique_ptr<A> up1(new A());
up1->func();
{
std::unique_ptr<A> up2(std::move(up1));
std::cout << "control from up1 to up2." << std::endl;
up2->func();
//up1->func(); 此处执行会报错,up1为empty
up1 = std::move(up2);
std::cout << "return control from up2 to up1" << std::endl;
up1->func();
}
}
{
A* as = new A[5];
std::unique_ptr<A[]> pas(as);
}
}
A construct.
A::func()
10 11
control from up1 to up2.
A::func()
10 11
return control from up2 to up1
A::func()
10 11
A destruct.
A construct.
A construct.
A construct.
A construct.
A construct.
A destruct.
A destruct.
A destruct.
A destruct.
A destruct.
这个测试用例主要用来说明几个问题:
- unique_ptr对对象的独占性,可以避免忘记delete而导致的资源泄漏。
- unique_ptr的控制转可以转移,一旦转移就变成empty.
- 不能由多个unique_ptr一起控制单个对象。
- unique_ptr可以管理对象数组,并保证多个对象的正确释放
2.2.2 shared_ptr 和 weak_ptr
shared_ptr和weak_ptr是一对组合。shared_ptr是计数型智能指针,属于强引用,每一个关联都有计数,而weak_ptr是弱引用,不会影响计数功能。
shared_ptr独立测试
void shared_ptr_test()
{
std::shared_ptr<A> sp1(new A());
std::cout << sp1.use_count() << std::endl;
{
std::shared_ptr<A> sp2(sp1);
std::cout << sp2.use_count() << std::endl;
std::shared_ptr<A> sp3(sp2);
std::cout << sp3.use_count() << std::endl;
{
std::shared_ptr<A> sp4(sp3);
std::cout << sp4.use_count() << std::endl;
}
}
}
A construct.
1
2
3
4
A destruct.
weak_ptr独立测试 在微软 MSDN上看到如下一段话:
The template class describes an object that points to a resource that is managed by one or more shared_ptr Class objects.
该模板类用于描述一个对象,该对象已经由一个或者多个shared_ptr对象管理控制。
The weak_ptr objects that point to a resource do not affect the resource's reference count.
weak_ptr对象指向一个资源,不会影响该资源的引用计数。
Thus, when the last shared_ptr object that manages that resource is destroyed the resource will be freed, even if there are weak_ptr objects pointing to that resource. This is essential for avoiding cycles in data structures.
当最后一个指向资源的shared_ptr对象析构后,资源被释放,即使还有weak_ptr指向该资源。该方法常用于避免循环引用。
void weak_ptr_test()
{
{
std::shared_ptr<A> sp1(new A());
std::weak_ptr<A> wp1(sp1);
std::cout << "shared use count: " << sp1.use_count() << std::endl;
std::cout << "weak_ptr use count: " << wp1.use_count() << std::endl;
{
std::shared_ptr<A> sp2(new A());
std::weak_ptr<A> wp2(sp1);
std::cout << "shared use count: " << sp2.use_count() << std::endl;
std::cout << "weak_ptr use count: " << wp2.use_count() << std::endl;
}
}
}
A construct.
shared use count: 1
weak_ptr use count: 1
A construct.
shared use count: 1
weak_ptr use count: 1
A destruct.
A destruct.
通过示例可以看到,即使有weak_ptr指向对象,也只显示shared_ptr强类型智能指针的指向数量。
weak_ptr的一个作用是可以有效判断某个对象是否还存活,示例如下:
void shared_weak_ptr_test()
{
std::weak_ptr<A> wp1;
{
std::shared_ptr<A> sp1(new A());
wp1 = sp1;
std::cout << "shared use count: " << sp1.use_count() << std::endl;
std::cout << "weak_ptr use count: " << wp1.use_count() << std::endl;
}
if (wp1.lock() != nullptr)
{
std::cout << "resource exists." << std::endl;
}
else
{
std::cout << "resource not exists." << std::endl;
}
}
A construct.
shared use count: 1
weak_ptr use count: 1
A destruct.
resource not exists.
可以看到,通过weak_ptr进行lock,如果资源存在,那么可以转型为shared_ptr, 如果资源不存在,那么返回的就是nullptr,这常常可以用于多线程程序中判断某个对象是否有效。
shared_ptr和weak_ptr联合测试 为什么shared_ptr和weak_ptr总是联合使用呢,在陈硕《Linux多线程服务端编程》中用了一个非常生动的示例进行说明,就是观察者模式。当被观察者发生某个事件,需要通知多个观察者时,往往是通过指针依次调用。这边存在的一个问题就是,如果某个观察者在其他线程中被删除,所指对象已经被删除,那么在调用方法时,就会出现问题。因为对象已经无效。我自己编写了一个简易示例,并未在多线程中运行,但是可以作为说明。
class Observer
{
public:
Observer(int32_t id) : observer_id(id)
{}
void update()
{
std::cout << observer_id << " Observer " << std::endl;
}
private:
int32_t observer_id;
};
class Observable
{
public:
void notifyall()
{
std::lock_guard<std::mutex> guard(observable_mutex);
for(std::vector<std::weak_ptr<Observer>>::iterator it = observers.begin(); it != observers.end(); )
{
std::weak_ptr<Observer> ov = *it;
std::shared_ptr<Observer> sp(ov.lock());
if (sp != nullptr)
{
sp->update();
it++;
}
else
{
it = observers.erase(it);
}
}
if (observers.size() == 0)
{
std::cout << "no observers. ." << std::endl;
}
}
void reg(std::weak_ptr<Observer> ob)
{
std::lock_guard<std::mutex> guard(observable_mutex);
observers.push_back(ob);
}
private:
std::vector<std::weak_ptr<Observer>> observers;
std::mutex observable_mutex;
};
void observer_test()
{
std::shared_ptr<Observable> bk;
{
std::shared_ptr<Observer> ob1(new Observer(1));
std::shared_ptr<Observer> ob2(new Observer(2));
std::shared_ptr<Observer> ob3(new Observer(2));
std::shared_ptr<Observable> obed1(new Observable);
obed1->reg(ob1);
obed1->reg(ob2);
obed1->reg(ob3);
obed1->notifyall();
bk = obed1;
}
bk->notifyall();
}
1 Observer
2 Observer
2 Observer
no observers. .
通过上面的示例可以看到,即使注册的观察者已经被释放,被观察者也可正确识别对象的可用性,而不会执行导致core的错误。
2.2.3 auto_ptr
cpp还有其他类型的智能指针,比如auto_ptr,不过目前在c++11中并不被推荐使用。 auto_ptr和unique_ptr有些类似,都是表达对资源的唯一所有权,但是区别是,auto_ptr可以通过赋值操作默认转移所有权,而unique_ptr需要显式的表达转移动作。
void auto_ptr_test()
{
{
std::auto_ptr<A> ap(new A());
ap->func();
std::auto_ptr<A> ap2 = ap;
ap2->func();
ap->func(); //此处会出现错误,因为ap已经不拥有A对象的资源,在访问对象内部变量的时候,自然会报错
}
}
这种通过赋值行为就实现资源转移,确实会让人感到诧异。有一种不告而取的感觉。
{
A* a = new A();
std::auto_ptr<A> ap(a);
std::auto_ptr<A> ap2(a);
}
该段测试代码会导致资源的重复释放问题。
2.3 C++下的垃圾回收器
第一次听说C++ 中的垃圾回收器,是在如下知乎中的链接看到。
blink中的垃圾回收器
从一般性考虑来讲,Cpp可以很精确的控制内存,也有各种智能指针使用,为什么还要有垃圾回收呢。这不是剥夺了Cpp程序员DEBUG的乐趣么。文章解释说因为工程规模大,即使有智能指针等各种技术,还是避免不了内存泄漏等问题,最后还是在相应项目下提供一套通用的垃圾回收机制。
源码中使用了很多模板技术,看不懂。
3. JAVA下的资源管理(回收)方式
此处内容主要参考书籍《垃圾回收的算法与实现》,说明JVM下的主要垃圾回收办法。
JAVA下的垃圾回收并没有采用计数法。因为计数无法解决循环引用的问题。思考下C++中相互引用的两个类,都拥有对方的shared_ptr类型的指针,如何正确释放对象。这也是为啥有weak_ptr的原因。
JAVA中所有通过new出来的资源对象存放在堆中,相关的引用放在堆栈上。在垃圾回收时,采取可达性分析。通过一系列的“GCRoots”对象作为起点进行搜索,所有可直接或者间接与GC Roots相连的为有效对象,其他则为无效对象;区分开有效、无效对象后,就可进一步处理。
先说几个通用算法。
3.1 标记清除算法(mark and sweep)
- 标记阶段,通过从GC Roots进行搜索,区分开所有可用对象及无效对象;
- 清除阶段,将所有无效对象串入到空闲空间链表中,用于后续对象的空间分配。
该算法很好理解,但是将各对象放置到空闲链表中会导致空间不连续,容易导致内存碎片,一些大块内存无法成功申请;同时申请空间会变慢,因为每次遍历空闲列表寻找合适内存空间都要花费时间;
3.2 复制算法(copy)
为了避免内存碎片的问题,复制算法将堆空间一分为二,当执行回收时,将有效对象复制到另外的空间,然后将原有空间清空即可。但是这样导致堆空间的使用率大大下降。优点是如果大量对象需要回收,那么只需要移动很少一部分存活对象即可完成垃圾回收。
3.3 标记压缩算法(mark and compact)
标记整理算法可以称的上是标记清除和复制的综合体,在标记完成后,将所有有效对象都往一端移动,保证堆空间的紧凑。
3.4 分代收集算法(Generational Collection)
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为新生代(Young Generation)和老年代(Tenured Generation)。
新生代特点是每次垃圾回收都有大量的对象需要被回收,只剩下少量有效对象。 老年代的特点是每次垃圾收集只有少量对象需要被回收。 不同的特点,可以使用不同的垃圾回收算法进行处理,从而提高整体的回收效率。
目前大部分垃圾收集器对于新生代采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,只有少量有效存活对象,只需要复制少量对象即可完成新生代的垃圾回收。但是实际中并不是按照1:1的比例来划分新生代的空间的,而是按照8:1:1,将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记压缩算法。
另外还有一个代就是永久代(PermanetGeneration),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。在Oracle JVM中,永久代并不属于堆空间。
新生代中的S0,S1是轮流使用。这个技巧在很多开发中用到。类似于双缓冲。
在新生代中的年龄达到一定阈值后,会被转移到老年代。
后续会进一步深入研究各回收算法。
来源:oschina
链接:https://my.oschina.net/u/100374/blog/820042