c++智能指针和java垃圾回收对比
我们都知道C++和java语言的一个巨大差异在于垃圾回收方面,这也是C++程序开发者和java程序开发者之间经常讨论的一个话题。
在C++语言中,一般栈上的内存随着函数的生命周期自动进行回收,但是堆上内存(也就是自己new/malloc出来的空间),需要自己手动进行delete/free,否则会造成内存泄漏。为了解决这个问题,C++中使用shared_ptr,对对象进行保护,shared_ptr的原理是引用计数,每对shared_ptr进行一次拷贝,会使ref_cnt++,当ref_cnt为0,会释放掉内存空间,从而避免了程序员主动控制内存释放,减少了内存泄漏的机会。使用引用计数方法,会导入一个新的问题:循环引用。
循环引用:
class A{
public:
std::shared_ptr<B> b_ptr;
};
class B{
public:
std::shared_ptr<A> a_ptr;
};
int test(){
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a.b_ptr = b;
b.a_ptr = a;
}
-
我们通过std::make_shared()和std::make_shared()分别创建了A和B对象的shared_ptr。在这个过程中,A对象和B对象的引用计数各自初始化为1。
-
我们将B对象的shared_ptr赋值给A对象的成员变量b_ptr。这将使B对象的引用计数增加1。此时,B对象的引用计数为2。
-
我们将A对象的shared_ptr赋值给B对象的成员变量a_ptr。这将使A对象的引用计数增加1。此时,A对象的引用计数为2。
-
当a和b变量超出作用域时,它们的析构函数会被调用。这将导致A对象和B对象的引用计数各自减1。然而,由于A对象的成员变量b_ptr仍然持有对B对象的引用,且B对象的成员变量a_ptr仍然持有对A对象的引用,所以它们的引用计数都为1。
所以当test函数执行结束,a对象和b对象不会被shared_ptr释放掉,但是我们也不能访问到对象的内存空间,也就导致了内存泄漏。
解决方法:使用weak_ptr
weak_ptr:
它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。其可以理解为shared_ptr的一个助手,可以通过lock将weak_ptr转化为shared_ptr,这样就会影响到引用计数,从而方便我们使用指针去操作响应对象。
因此,我们只需要将上述A和B类中shared_ptr改成weak_ptr即可。
class A{
public:
std::weak_ptr<B> b_ptr;
};
class B{
public:
std::weak_ptr<A> a_ptr;
};
除了解决引用技术,weak_ptr也可以解决共享对象的线程安全问题。
#include <iostream>
#include <memory>
#include <thread>
class Test {
public:
Test(int id) : m_id(id) {}
void showID() {
std::cout << m_id << std::endl;
}
private:
int m_id;
};
void thread1(Test* t) {
std::this_thread::sleep_for(std::chrono::seconds(2));
t->showID(); // 打印结果:0
}
int main()
{
Test* t = new Test(2);
std::thread t1(thread1, t);
delete t;
t1.join();
return 0;
}
t对象创建在堆上,可以被多线程共享。由于t1线程先sleep了2s,当执行showID时,一定已经被主线程delete掉了。从而导致内存非法访问,导致程序崩溃。
可以使用weak_ptr来避免这种问题
#include <iostream>
#include <memory>
#include <thread>
class Test {
public:
Test(int id) : m_id(id) {}
void showID() {
std::cout << m_id << std::endl;
}
private:
int m_id;
};
void thread2(std::weak_ptr<Test> t) {
std::this_thread::sleep_for(std::chrono::seconds(2));
std::shared_ptr<Test> sp = t.lock();
if(sp)
sp->showID(); // 打印结果:2
}
int main()
{
std::shared_ptr<Test> sp = std::make_shared<Test>(2);
std::thread t2(thread2, sp);
t2.join();
return 0;
}
此时,即便Test对象在主线程被释放,当使用weak_ptr时必须要lock,获取到shared_ptr,才能访问对象内存。lock过程中,是通过检测它所观察的强智能指针保存的Test对象的引用计数,来判定Test对象是否存活。此时Test对象被释放,lock失败,返回nullptr,再加空指针判断,即可避免内存非法访问的问题。
java中的垃圾回收机制,并不是采用引用计数的方式来实现的。参考《深入理解java虚拟机》中的代码:
public class Ref_cnt {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) {
Ref_cnt objA = new Ref_cnt();
Ref_cnt objB = new Ref_cnt();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
运行发现没有响应的日志打印,觉得应该时配置参数的问题,经过一番查找,需要再Configuration中引入VM options: -XX: +PrintGCDetails
打印部分结果如下:
虽然不太看得懂...但是应该是回收了空间的意思?这也说明java不是使用引用计数来判断对象是否存活的。那么java的虚拟机是如何判断对象存活的呢?
<hr/
可达性分析算法
可达性分析算法,简单来说就是图的可达性判断,在系统中引入一些GC Roots(类比图的起点),通过引用链构成图的各条边,能够通过起点遍历到的顶点(对象),即表明可达,也就不会被回收。只有那些不能从GC Roots出发遍历到的才可以被回收。
GC Roots的选取方法:
在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法 堆栈中使用到的参数、局部变量、临时变量等。
在方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。 ·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
在本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象。
Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象 (比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
所有被同步锁(synchronized 关键字)持有的对象。
反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
(这里还需要后续持续去理解)
标签:std,java,对象,c++,计数,引用,shared,ptr,指针 From: https://www.cnblogs.com/xyfhsy/p/17924318.html