文档章节

C++和JAVA下的内存资源管理

西昆仑
 西昆仑
发布于 2017/01/06 16:11
字数 3281
阅读 37
收藏 0

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.

这个测试用例主要用来说明几个问题:

  1. unique_ptr对对象的独占性,可以避免忘记delete而导致的资源泄漏。
  2. unique_ptr的控制转可以转移,一旦转移就变成empty.
  3. 不能由多个unique_ptr一起控制单个对象。
  4. 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的乐趣么。文章解释说因为工程规模大,即使有智能指针等各种技术,还是避免不了内存泄漏等问题,最后还是在相应项目下提供一套通用的垃圾回收机制。

源码中使用了很多模板技术,看不懂。

blink github链接

3. JAVA下的资源管理(回收)方式

此处内容主要参考书籍《垃圾回收的算法与实现》,说明JVM下的主要垃圾回收办法。

JAVA下的垃圾回收并没有采用计数法。因为计数无法解决循环引用的问题。思考下C++中相互引用的两个类,都拥有对方的shared_ptr类型的指针,如何正确释放对象。这也是为啥有weak_ptr的原因。

JAVA中所有通过new出来的资源对象存放在堆中,相关的引用放在堆栈上。在垃圾回收时,采取可达性分析。通过一系列的“GCRoots”对象作为起点进行搜索,所有可直接或者间接与GC Roots相连的为有效对象,其他则为无效对象;区分开有效、无效对象后,就可进一步处理。

先说几个通用算法。

3.1 标记清除算法(mark and sweep)

  1. 标记阶段,通过从GC Roots进行搜索,区分开所有可用对象及无效对象;
  2. 清除阶段,将所有无效对象串入到空闲空间链表中,用于后续对象的空间分配。

该算法很好理解,但是将各对象放置到空闲链表中会导致空间不连续,容易导致内存碎片,一些大块内存无法成功申请;同时申请空间会变慢,因为每次遍历空闲列表寻找合适内存空间都要花费时间;

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中,永久代并不属于堆空间。

JVM 各代结构

新生代中的S0,S1是轮流使用。这个技巧在很多开发中用到。类似于双缓冲。

GC收集过程

在新生代中的年龄达到一定阈值后,会被转移到老年代。

后续会进一步深入研究各回收算法。

© 著作权归作者所有

西昆仑

西昆仑

粉丝 137
博文 141
码字总数 102735
作品 0
南京
高级程序员
私信 提问
Java finalize方法

《JAVA编程思想》: java提供finalize()方法,垃圾回收器准备释放内存的时候,会先调用finalize()。 (1).对象不一定会被回收。 (2).垃圾回收不是析构函数。 (3).垃圾回收只与内存有关。 (4)....

清风伴月
2017/10/22
35
0
浅析:Java与C++的主要区别

Java区别于C++ 表面看来两者最大的不同在于Java没有指针,或者说,Java满地都是指针。对于编程者而言Java的这种设计是安全且更易用的。说Java满地是指针的原因在于它把指针的功能隐藏了,其实...

Ace☞Tseng
2012/10/09
313
0
内存泄漏检测工具

内存泄漏(memory leak),指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。 在编程时进行动态内存分配是非常必要的,它可以在程序运行的过程中帮助分配所需的内存,而不是在进程...

长平狐
2013/01/06
1K
1
Java程序员如何高效而优雅地入门C++

Java程序员如何高效而优雅地入门Cpp,由于工作需要,需要用C++写一些模块。关于C++ 的知识结构,虽说我有过快速学习很多新语言的经验,但对于C++ 我也算是老手,但也还需要心生敬畏,本文会从...

小欣妹妹
2018/04/23
82
1
开源领袖谈UNIX下的编程语言

C语言 虽说C语言在内存管理方面存在严重的缺陷,不过它还是在某些应用领域里称王称霸。对于那些要求最高的效率,良好的实时性,或者与操作系统内核紧密关联的程序来说,C仍然是很好的选择。 ...

kouxunli1
2015/01/07
273
0

没有更多内容

加载失败,请刷新页面

加载更多

Giraph源码分析(八)—— 统计每个SuperStep中参与计算的顶点数目

作者|白松 目的:科研中,需要分析在每次迭代过程中参与计算的顶点数目,来进一步优化系统。比如,在SSSP的compute()方法最后一行,都会把当前顶点voteToHalt,即变为InActive状态。所以每次...

数澜科技
今天
4
0
Xss过滤器(Java)

问题 最近旧的系统,遇到Xss安全问题。这个系统采用用的是spring mvc的maven工程。 解决 maven依赖配置 <properties><easapi.version>2.2.0.0</easapi.version></properties><dependenci......

亚林瓜子
今天
10
0
Navicat 快捷键

操作 结果 ctrl+q 打开查询窗口 ctrl+/ 注释sql语句 ctrl+shift +/ 解除注释 ctrl+r 运行查询窗口的sql语句 ctrl+shift+r 只运行选中的sql语句 F6 打开一个mysql命令行窗口 ctrl+l 删除一行 ...

低至一折起
今天
10
0
Set 和 Map

Set 1:基本概念 类数组对象, 内部元素唯一 let set = new Set([1, 2, 3, 2, 1]); console.log(set); // Set(3){ 1, 2, 3 } [...set]; // [1, 2, 3] 接收数组或迭代器对象 ...

凌兮洛
今天
4
0
PyTorch入门笔记一

张量 引入pytorch,生成一个随机的5x3张量 >>> from __future__ import print_function>>> import torch>>> x = torch.rand(5, 3)>>> print(x)tensor([[0.5555, 0.7301, 0.5655],......

仪山湖
今天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部