c++智能指针和java垃圾回收对比
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<A>()和std::make_shared<B>()分别创建了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 中注册的回调、本地代码缓存等。
(这里还需要后续持续去理解)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)