首页 > 编程语言 >c++智能指针和java垃圾回收对比

c++智能指针和java垃圾回收对比

时间:2023-12-24 13:55:06浏览次数:39  
标签:std java 对象 c++ 计数 引用 shared ptr 指针

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;
}
  1. 我们通过std::make_shared()和std::make_shared()分别创建了A和B对象的shared_ptr。在这个过程中,A对象和B对象的引用计数各自初始化为1。

  2. 我们将B对象的shared_ptr赋值给A对象的成员变量b_ptr。这将使B对象的引用计数增加1。此时,B对象的引用计数为2。

  3. 我们将A对象的shared_ptr赋值给B对象的成员变量a_ptr。这将使A对象的引用计数增加1。此时,A对象的引用计数为2。

  4. 当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,这样就会影响到引用计数,从而方便我们使用指针去操作响应对象。

image-20231224123808838

因此,我们只需要将上述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

image-20231224133001930

打印部分结果如下:

image-20231224133347990

虽然不太看得懂...但是应该是回收了空间的意思?这也说明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

相关文章

  • C++(指针常量、常量指针)
    在C++中,常量指针和指针常量是两个不同的概念,它们涉及到指针和常量的组合。让我们来详细解释它们的含义:常量指针(ConstantPointer):一个常量指针是指针本身是常量,它指向的内容可以被修改。一旦指针被初始化指向某个变量,就不能再指向其他变量。通过指针可以修改所指向的变量的......
  • of type [class java.lang.String] to [class java.util.Date]
    报错:javax.el.ELException:Cannotconvert[2023-11-1422:35:34]oftype[classjava.lang.String]to[classjava.util.Date]  问题分析:相应数据的类型不正确bean层写的是: privateStringcreateDate; jsp写的是:<fmt:formatDatevalue="${file.createDate}"patte......
  • Java多线程​(三)线程安全:同步及锁
    线程安全问题考虑如下情景:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票。publicclassTicketSellerextendsThread{//定义票的数量staticintticket=0;//取值范围:0~99@Overridepublicvoidrun(){while(true){......
  • C++出现“printf/cout不明确”的问题的原因和解决方法(转)
    如果你有幸看到这个博客,那么我猜你应该已经碰上这个问题,而且正急着寻求方法,因此,我先把解决方法用最简单的一句话写出来解决方法为:**将文件中原有的usingnamespacestd;删除后保存cpp文件,再加上usingnamespacestd;保存cpp文件。问题即可解决**如果根据上述方法已经解决了这个问......
  • java 数组想等
    实现"Java数组相等"作为一名经验丰富的开发者,我非常乐意教你如何实现"Java数组相等"的功能。在本文中,我将向你展示整个过程,并逐步指导你完成每一步所需的代码。流程概述下面是实现"Java数组相等"功能的整体流程:创建两个数组。检查两个数组的长度是否相等。逐个比较两个数组......
  • java 判断字符串a中包好几个字符串b
    Java判断字符串a中是否包含字符串b在Java编程中,我们经常需要判断一个字符串是否包含另一个字符串。这种需求在很多实际场景中都会遇到,比如搜索功能、数据过滤等。本文将介绍如何使用Java判断一个字符串中是否包含多个子字符串,并给出相关代码示例。方案一:使用String类的contains方......
  • java 判断一个值是否为null
    Java判断一个值是否为null作为一名经验丰富的开发者,我会在本文中教你如何在Java中判断一个值是否为null。首先,我将为你展示整个流程,并使用表格形式展示每个步骤。然后,我将详细说明每个步骤需要做什么,并提供相应的代码示例,并对代码进行注释解释。流程概述以下是判断一个值是否为......
  • java 判断一个集合是否包含
    Java判断一个集合是否包含1.整体流程下面是判断一个集合是否包含某个元素的整体流程:步骤描述步骤一创建一个集合对象步骤二向集合中添加元素步骤三判断集合是否包含指定的元素步骤四根据判断结果输出对应的信息2.详细步骤2.1步骤一:创建一个集合......
  • java 判断文件是否是视频
    Java判断文件是否是视频引言在开发中,我们经常需要判断一个文件是否是视频文件,这对于文件管理和处理来说非常重要。本文将指导你如何使用Java来实现判断文件是否是视频的功能。流程概述下面是整个流程的步骤概览:pietitle文件是否是视频"1.获取文件扩展名":20......
  • java 判断图片背景是深色的
    判断图片背景是深色的流程flowchartTDA[获取图片]-->B[将图片转换为灰度图]B-->C[获取灰度图像素点信息]C-->D[统计灰度图中像素点的颜色值]D-->E[根据颜色值判断背景色是深色还是浅色]E-->F[返回判断结果]具体步骤及代码实现1.获取图片首先,我们需要获取一张......