概述
应用程序开发中,内存管理是个重要的话题。
简单而言,语言层面的内存管理基本有三类:
1. 纯粹的手动管理
如C和曾经的C++。
char *some_string = malloc(BUFFER_SIZE);
// do something
free(some_string);
这个简单的例子里用完就释放还好,但是有时候这个some_string被传来传去不知道飞哪儿去了,就比较尴尬。
纯手动管理的代价是程序员的心智负担比较重。
即使后来C++程序员们抽象出RAII这样的实践规范,一定程度上降低了管理的复杂度,但是相对来说成本还是略高。
随着语言的发展,已经很少有语言只依赖手动管理内存了。
2. 基于某些机制实现半自动管理
这里的某些机制其实通常就是引用计数。毕竟这是最简单的内存管理辅助手段。
引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。
引用计数大家都了解,不多说,单纯的自动使用引用计数问题在于无法解决循环引用的问题。很多语言选择让程序员付出一点劳动来解决这个问题。
早年,Objc选择的是退一步,完全让程序员来管理引用计数的加减,称为MRC,显然管理成本偏高。后来推出了ARC,提供了更健全的机制,程序员只要标识出对象间的引用关系是强引用还是弱引用就可以了,大大降低了程序员的负担。
虽然走这个路子的语言不算多,但除了Objc之外还是有好几个的。
C++的智能指针跟Objc的ARC就比较相似。而Rust的所有权模型本质上也是类似的。
3. 自动垃圾回收
通过GC自动管理内存大概是现在的主流了。对程序员来讲实在是太舒适了,开发时几乎不用考虑内存管理的问题。Java、JavaScript、Python、go等一大票语言都是走的这条路。
GC是基于可达性分析算法的,即,从根节点(全局变量、局部变量等等)出发,遍历引用到的对象,所有没遍历到的对象就可以释放了。
当然从原理到实际应用中间差了十万八千里。朴素的GC会经常造成Stop The World。一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。
于是很多GC算法被发明出来用于优化、减少。常见的CMS、G1回收算法都极大地减少了STW的时间,但仍然不能完全避免。
R大在Java 大内存应用(10G 以上)会不会出现严重的停顿?中提到Zing JVM采用的C4算法是可以完全避免STW的,不过看起来为了避免STW,C4算法会吃掉更多的内存,程序吞吐量会受到影响。
小结
总结一下吧,纯手动管理基本上已被淘汰,ARC(暂且把方法二这类都称为ARC吧)和GC对比之下,
对开发者,ARC需要程序员付出一定的代价进行管理,GC则基本上完全解放双手;
性能上,GC通常会造成STW现象,对响应时间比较敏感的程序,比如高频交易系统,是很难接受的,而ARC不会对性能造成明显影响。
几点有趣的事情
1. 总体性能
总体性能上,只要不是内存跑得特别满,ARC的总体代价是高于GC的。其实想想就知道了,GC只关注两次回收间的变化,而ARC要对每一次引用的改变进行计数,总体性能上比GC差是很正常的,但由于ARC的耗时是均匀分布在运行时间里的,通常我们不用很关注。关于这个问题可以参考这篇论文。
2. cpython的方案
另外比较特别的是,Python的默认解释器CPython中应用了引用计数与垃圾回收相结合的手法,没有循环引用的对象会被引用计数回收,剩下的交给GC处理,大大降低了GC的压力。感觉很有意思。
3. ARC的性能
在类ARC方案上,C++提供的能力是比较全面的。这可能跟c++常用于一些性能比较苛刻的场景有关。
出于性能原因,使用c++智能指针时有如下指导思想:
- 对象的所有权不重要时 ,用裸指针
- 对象的所有权唯一时,用unique_ptr,能用unique_ptr就不要用shared_ptr。
- 要处理复杂情况时,可以使用shared_ptr,但需要注意不要滥用。当引用关系不影响所有权时,用weak_ptr。
Rust也有类似的能力。
而python和Objective-C就没有这么多讲究,所有的引用计数其实都是shared_ptr。Objc在iOS上这么多年,而后来的swift也传承了ARC,基本上可以认为,移动端应用从小到大都不差这么一丢丢性能。
以此推论,绝大部分c++应用也完全没必要关注这几个指针间的差异,操起shared_ptr就是干。