文章概述
内存治理对于每种开发语言来说都是一个非常重要的话题;即便像 Java 这种领有“简单”垃圾收集器的语言,也会面临 GC 带来的各种困扰。
C++ 程序设计中的很多 bug 也是因为内存治理不善导致的,而且难以发现和排除;如何有条理地治理内存,对于 C ++ 开发尤为重要。
“他山之石可以攻玉”,在钻研如何做好 C ++ 内存治理之前,咱们也能够看下其余语言是怎么做内存治理的,有什么模式或者模型能为咱们所借鉴,可能更好地帮忙咱们了解和做好 C ++ 的内存治理。
01 不同语言的内存管理机制介绍
C/C++、Java、Objective-C/Swift 和 Golang 是几种应用宽泛的语言,内存管理机制也绝对典型。
1.1 C/C++
C 也常被称作“可移植的汇编”,诞生之初次要是解决汇编语言的移植问题,在嵌入式和操作系统等绝对底层的开发畛域利用宽泛;对于简单的业务问题,因为它没有面向对象的能力(不好形象业务逻辑),显得难以应酬。
C++ 是一种很弱的面向对象的语言(模版和 Interface 简直是一种互相违反的思维)。为了兼容简直所有的 C 个性,背上了比拟重的历史包袱。在 C ++11 之后,这种景象有了比拟大程度的改善,各种新的语言个性能够让 C ++ 开发者开发出更优雅、强壮的代码。
C 语言的内存治理是典型的手动管理机制,通过 malloc 申请,free 开释。
C++ 语言除了手动治理之外,还领有弱的“垃圾回收机制”(智能指针的反对)。
C/C++ 中常见的内存治理问题有:
a. 数组拜访越界(Java 语言可抛出 ArrayIndexOutOfBoundsException)
b. 内存拜访超过生命周期‣ 栈弹出之后,仍旧进行拜访(函数返回外部栈地址)‣ 堆内存开释,仍旧进行拜访
c. 内存泄露 (没有开释不再应用的内存)
d. 悬空指针导致的问题‣ 指针指向内存开释之后,指针没有复位(设置为 nullptr)‣ 应用没有复位(不为 null)的有效内存(已开释或者未申请的内存)
e. C++ 独有的问题‣ 非预期内的拷贝结构函数调用带来的适度复制(性能问题)‣ 不合理的复制、拷贝构造函数的实现,导致的意外数据共享(没有设置为 nocopyable)
1.2 Java
Java 是一种面向对象的古代语言,有着丰盛的语言个性和开发生态。Java 语言是为了实现下一代智能家电的通用零碎而设计的。在借鉴 C ++ 语言的根底上,又摒弃了 C ++ 的一些简单个性(可能升高软件开发品质)。
比方:
a. 不容许多继承
b. 更纯正的 interface
c. 所有皆对象(根底类型除外)
d. non-static 办法默认反对多态
e. 等等
不想“有心栽花花不开,无心插柳柳成荫”。Java 在家电市场毫无起色,却因为优异的网络编程反对能力、平台无关性、垃圾回收等能力,加上恰逢互联网时代的到来,而后在企业级市场上大放异彩。
Java 因为有虚拟机的反对(先编译成字节码,由虚拟机解释成不同平台的“语言”),能够做到“一次编译,到处运行”。
Java 目前在后盾开发、大数据以及 App 开发畛域(Kotlin 也是类 Java 语言)有着十分宽泛的利用。
Java 的内存治理依靠于 JVM 的垃圾回收器(Garbage Collections)一般而言,垃圾回收的步骤包含两步:
a. 找到可被回收的对象;
b. 进行内存回收和整顿
JVM(HotSpot) 的 GC 也是如此。
(一)可收回对象判断 JVM GC 基于可达剖析,来查找可回收对象;能够防止援用计数计划的循环援用问题。
(图 1 基于根的可达对象剖析)
(二)可回收策略和算法简直所有的垃圾回收器,都存在 STW 问题,高效回收以及升高对业务代码执行的影响是一件很难的事件。为了尽可能地优化性能,GC 采纳 分代收集 和 标记 - 革除 / 标记 - 革除 - 整顿 / 标记 - 复制 进行内存回收。
‣ 分代收集(新生代和老年代)不难理解,新申请的内存比拟大的概率能够在不久后删除;如果一个内存存在比拟久了,那么接下来被回收的概率就会比拟低;新生代的回收会比拟轻量和高效,老年代的 GC 绝对会比拟重。
G1 之前根本都是下图这种典型的分代内存模型。
(图 2 典型的内存分代模型)
G1 依然保留了新生代和老年代的概念,然而新生代和老年代的内存区域不再固定,都是一系列的动静汇合。
(图 3 G1 的内存分代模型)
‣ 标记 - 革除 / 标记 - 革除 - 整顿 / 标记 - 复制
标记 - 革除 办法绝对简略、高效,然而会存在内存碎片;
标记 - 革除 - 整顿 能够解决内存碎片的问题,然而会减少 GC 的持续时间(益处大于害处);
标记 - 复制 办法相似于 ping-pang 机制,须要有两片内存区域;在内存清理阶段,会将存活对象对立放到一个区域,而开释另外一个区域;和整顿办法一样,也不会产生内存碎片,并且复制和标记能够同时进行,然而须要更多的内存区域。
JVM 有多种垃圾收集器可供选择,须要依据业务需要(低提早 or 高吞吐)进行衡量,CMS 和 G1 应用绝对较多。
a. CMS 用于老年代的垃圾回收,谋求最短进展;
b. G1 老年代和新生代都能够应用,并且绝对高效;
c. Java11 推出的 Z Garbage Collector(ZGC)有着不错的性能,目前根本能够投入生产。(https://docs.oracle.com/en/ja…)
1.3 Objective-C/Swift
Objective-C 是基于 C 语言倒退出的面向对象的开发语言(Objective);Objective- C 的语法绝对繁琐、不够便捷,所以苹果在 2014 推出了 Swift,领有脚本语言般的表现力。
Objective-C 的内存治理基于简略的援用计数,能够分为两类:
‣ MRR:Manual Retain-Release
(图 4 Objective-C MRR 机制)
‣ ARC:Automatic Reference CountingARC 底层还是 MRR,只是由编译器在失当的地位帮咱们插入 retain 和 release。是否开启 ARC 反对和编译器版本以及编译器选项无关。
1.4 Golang
Golang 也是具备垃圾回收的一种语言,次要利用在后端开发畛域;回收策略也是基于可达对象剖析和标记 - 革除 - 整顿 / 复制算法。
和 Java 的比对,能够参考以下链接:
https://blog.mooncascade.com/…
02 援用模型对对象生命周期的影响
不同的援用类型对对象的生命周期影响不一样,从语义上可分为三类:
‣ 强援用(Strong reference)
强援用对象,不能够被回收
如果是基于援用计数,援用计数会被影响
‣ 软援用(Soft reference)
非必要不回收,比方 JVM 在 OOM 之前会尝试对 Soft reference 对象进行回收 - 如果基于援用计数,会进化为弱援用
‣ 弱援用(Weak reference)
不影响对象生命期 - 如果基于援用计数,不会影响援用计数
03C++ 的内存治理计划
准则:尽量应用智能指针,不要放心智能指针带来性能损耗。
3.1 手动治理内存在某些场景下,C++ 须要手动治理内存;咱们能够应用一些技巧来更平安地应用和治理内存。
a. 防止悬空指针
(点击查看大图)
b. 基于 Allocator 策略进行内存调配
通过 Allocator 能够扭转 stl 容器的内存分配机制,比方为 vector 在栈上分配内存;或者应用内存池进行内存治理;
(点击查看大图)
3.2 COM 接口式内存治理
COM(Component Object Model)是微软在 1993 年提出的一种二进制兼容的计划或者规范,其中的思维还是挺值得插件开发借鉴(非 windows 平台)。
3.2.1 应用 COM 接口的劣势
(一)COM 接口能够解决插件开发畛域的两个典型的兼容问题
a. 接口的内存布局构造变动带来的兼容问题
(图 5 接口的内存布局变动导致的兼容问题)
b. 不同的编译器、不同零碎源码库带来的兼容问题
(图 6 内存治理不同版本带来的兼容问题)
(二)COM 接口为什么能够解决上述的问题?
a. COM 强调面向接口,插件的边界只能是 interface,COM 接口不容许有任何的数据域
(图 7 COM 严格以接口为边界)
b. COM 接口须要裸露 AddRef 和 Release 接口,用来进行闭环(插件申请插件开释)的内存治理
3.2.2 COM 接口例子
‣ 场景:Application 须要一个插件来提供读和写的性能
‣ 准则:所有的接口都要继承 IUnknown,公布的 interface 都须要有惟一 ID
‣ DEMO:
a. com.h
(点击查看大图)
b. interface.h 插件对外公布的接口
(点击查看大图)
c. export_api.h 是插件的惟一接口裸露点
(点击查看大图)
d. interface_impl.cpp 插件的性能实现,能够应用继承,也能够应用聚合的办法
(点击查看大图)
e. 插件的应用
(点击查看大图)
‣ Output:
(点击查看大图)
3.3 C++ 智能指针
C++11 的很多个性都是先从 boost 引入技术报告(TR),而后进入到 C ++ 规范,智能指针就是如此。
(图 8 C++ 规范演进)
不同类型智能指针的比拟:
(点击查看大图)
3.3.1 shared_ptrshared_ptr 是应用最宽泛的智能指针,能够进行所有权共享;当没有任何人持有,援用计数为 0 的时候内存主动开释。智能指针外部有两个重要的块:
a. 数据块 指向内存地址的指针
b. 管制块 寄存援用计数等信息
(图 9 shared_ptr 原理)
3.3.2 weak_ptrweak_ptr 是 shared_ptr 的伴生品,weak_ptr 没有独立存在意义。
(图 10 weak_ptr 和 shared_ptr 的关系)
weak_ptr 能够解决 share_ptr 在两个场景下的问题:
a. shared_ptr 的循环援用,会造成内存泄露
b. 观察者模式 被察看对象 subject 不应该影响 observer 的生命周期
(点击查看大图)
Output:
(点击查看大图)
3.3.3 unique_ptr
unique_ptr 指向对象的所有权独享,在出作用域 unique_ptr 析构时开释内存(和 boost::scoped_ptr 相似)。
如果要转移所有权,须要应用 std::move。(相似的有 std::thread,所有权独占)
(图 11 unique_ptr 的所有权转移)
3.3.4 intrusive_ptr
侵入式(智能)指针,和 share_ptr 用起来很像。intrusive_ptr 提供了自定义援用计数的能力,适宜用来治理第三方接口。
比方用 intrusive_ptr 来治理 COM 接口。只须要实现 IUnknown 类型的 intrusive_ptr_add_ref 和 intrusive_ptr_release 办法,就能够像 share_ptr 一样来应用 COM 接口了。
(点击查看大图)
Output:
(点击查看大图)
3.3.5 utilities
a. 应用 make_shared/make_unique 结构智能指针,缩小结构性能损耗;(https://en.cppreference.com/w…)b. owner-based (as opposed to value-based) order(https://en.cppreference.com/w…)‣ owner-based order
能够看作为管制块的比拟,看受智能指针影响生命期的对象是不是一个;
‣ value-based order
能够看作数据块的比拟,比拟内存地址
(点击查看大图)
Output:
(点击查看大图)
c. shared_from_this
‣ 应用场景:一个被智能指针治理的对象(class A 的对象),在对象的外部,又要调用一个应用 std::shared_ptr 的函数。
‣ 网络场景示例:connection 示意一个链接,连贯胜利之后,在 run 函数外部调用 async_run,实现异步读操作;这个时候须要把本人的智能指针传递进去,从而进行生命期的托管。
(点击查看大图)
模仿一个网络连接产生
(点击查看大图)
测试
(点击查看大图)
即便马上将 connection 变量开释,出了作用域之后,咱们依然能够进行 read 操作。
例子应用的 Thread pool
(点击查看大图)
std::enable_shared_from_this 实质上是一个语法糖,在基类中应用 weak_ptr 来帮咱们防止了循环援用(本人援用本人)。
(点击查看大图)
谢谢观看!