《代码的未来》读书笔记:内存管理与GC那点事儿
一、内存是有限的
近年来,我们的电脑内存都有好几个GB,也许你的电脑是4G,他的电脑是8G,公司服务器内存是32G或者64G。但是,无论内存容量有多大,总归不是无限的。实际上,随着内存容量的增加,软件的内存开销也在以同样的速率增加着。因此,最近的计算机系统会通过“双重”幻觉,让我们以为内存容量是无限的。
第一重幻觉:垃圾回收(GC)机制

在C/C++中,内存空间的分配是由人工手动进行管理的,当需要内存空间时,要请求OS进行分配,不需要的时候则需要返回给OS。如果不再需要的内存空间没有及时返还给OS,这些无法访问的内存空间就会一直保留下来,造成内存的白白浪费,最终引发性能下降和产生抖动。
将内存管理,尤其是内存空间的释放实现自动化,这就是GC。
第二重幻觉:OS提供的虚拟内存
所谓虚拟内存,就好比是将书桌上的比较老的文件先暂时收到抽屉里,用空出来的地方来摊开新的文件。在计算机中,体现在在内存容量不足时将不经常访问的内存空间中的数据写入硬盘,以增加“账面上”可用内存容量的手段(想想我们的内存和硬盘容量对比就知道了)。

BUT,如果在书桌和抽屉之间频繁进行文件的交换,工作效率肯定会下降。如果每次要看一份文件都要先收拾书桌再到抽屉里面拿的话,那工作根本就无法进行了。

虚拟内存也有同样的缺点:硬盘的容量比内存大,但也只是相对的,速度却非常缓慢,如果和硬盘之间的数据交换过于频繁,处理速度就会下降,表面上看起来就像卡住了一样,这种现象称为抖动(Thrushing)。相信很多人都有过计算机停止响应的经历,而造成死机的主要原因之一就是抖动。
二、GC的基本方式
2.1 标记清除方式
标记清除是最早的GC算法,其原理是:首先从根开始将可能被引用的对象用递归的方式进行标记,然后将没有标记到的对象作为垃圾进行回收。
下图直观地展示了标记清除算法的大致原理:
① 初始阶段:

② 标记阶段:

其中,红色背景白色字体的对象为已标记的对象。重复这一阶段步骤,已标记的对象会被视为“存活”的对象,而没有被标记的对象就将被进行回收。
③ 清除阶段:

将前面阶段中没有被标记的对象进行回收,这一操作被称为清除阶段。在扫描的同时,还需要将存活对象的标记清除掉,以便于下一次GC操作做好准备。标记清除算法的处理时间,是和存活对象与对象总数的总和相关的。
标记清除算法的缺点:在分配了大量对象并且其中只有一小部分存活的情况下,所消耗的时间会大大超过必要的值,这是因为在清除阶段还需要对大量死亡对象进行扫描。
2.2 复制收集方式
复制收集克服了标记清除的缺点,其基本原理是:将从根开始被引用的对象复制到另外的空间中,然后再将复制的对象所能够引用的对象用递归的方式不断复制下去。
下图直观地展示了复制手机的大致原理:
① 初始阶段:

② 复制收集阶段:

复制阶段-1

复制阶段-2
③ 清除阶段:

在清除阶段会将旧空间废弃掉,也就可以将死亡对象所占用的空间一口气全部释放出来,而没有必要再次扫描每个对象。下次GC的时候,现在的新空间也就成为了下次的旧空间。
复制收集的缺点是:和标记方式相比,将对象复制一份所需要的开销比较大,因此在“存活”对象比例较高的情况下,反而比较不利。
2.3 引用计数方式
引用计数方式是GC算法中最简单也最容易实现的一种,其基本原理是:在每个对象中保存该对象的引用计数,当引用发生增减时对计数进行更新。引用计数的增减,一般发生在变量赋值、对象内容更新、函数结束(局部变量不再被引用)等时间点,当一个对象的引用计数变为0时,则说明它将来不会再被引用,因此可以释放响应的内存空间。
下图直观地展示了引用计数方式的大致原理:
① 初始阶段:

② 引用计数阶段:

当对象引用发生变化时,引用计数也会跟着变化。在这里,由对象B到对象D的引用失效了,于是对象D的引用计数变为0。由于对象D的引用计数变为了0,因此由对象D到对象C和对象E的引用数也分别相应减少。最后,对象D和对象E引用数变为了0,所以需要被清除。
③ 清除阶段:

所有引用计数变为0的对象都将被释放,“存活”的对象则保留了下来。在整个GC处理过程中,并不需要对所有对象进行扫描。
引用计数的优点在于:易于实现(标记清除和复制收集机制实现由难度);当对象不再被引用的瞬间就会被释放(其他机制预测一个对象何时被释放很困难)。
引用计数的缺点在于:
① 无法释放循环引用的对象

② 必须在引用发生增减时对引用计数做出正确的增减:想想漏掉了对某个对象计数的增减会怎么样?
③ 引用计数管理并不适合并行处理:想想如果多个线程同时对引用计数进行增减又会怎样?
三、GC的改良方式
GC的基本算法,大体上都逃不出上述三种方式以及它们的衍生品。现在,通过对这三种方式进行融合,出现了一些更加高级的方式。
3.1 分代回收方式
由于GC和程序处理的本质是无关的,因此它所消耗的时间越短越好。分代回收的目的是为了在程序运行期间,将GC所消耗的时间尽量缩短。
分代回收的基本思路是:大部分对象都会在短时间内成为垃圾,而经过一定时间依然存活的对象往往拥有较长的寿命。如果寿命长的对象更容易存活下来,寿命短的对象则会被很快废弃。那么,对分配不久的“年轻”对象进行重点扫描,应该就可以更有效地回收大部分垃圾。
在分代回收方式中,对象会按照生成时间进行分代,刚刚生成不久的年轻对象划为新生代(Young generation),而存活了较长时间的对象划为老生代(Old generation)。对于不同的实现方式,可能还会划分更多的代,
在.NET中,CLR就将内存中的对象分为了三代,每执行N次0代的回收,才会执行一次1代的回收,而每执行N次1代的回收,才会执行一次2代的回收。当某个对象实例在GC执行时被发现仍然在被使用,它将被移动到下一个代中上,下图直观地展示了CLR对三个代的回收操作:

回想刚刚说到的几种基本回收方式,我们可以将其组合一下来为分代回收奠定实现基础。
(1)首先,从根开始一次常规扫描,找到“存活”对象。这个步骤可以采用标记清除或复制收集,不过大多数分代回收的实现都采用了复制收集算法。不过在扫描的过程中,如果遇到被划分到更高级别的代的对象则不对该对象继续进行递归扫描。这样一来,需要扫描的对象数量就大幅度减少。
(2)其次,将第一次扫描后残留下来的对象划分到更高级别的代上。具体来说,如果是用复制收集算法的话,只要将复制目标空间设置为更高级别的代就可以。而如果用标记清除算法的话,则大多采用在对象上设置某种级别标志的方式。但是,被分配到更高的级别的代上后,该对象所占用的内存空间的时间也会随之增加,如何确保及时利用和释放的平衡点也是需要考虑的。
3.2 增量回收方式
在对实时性要求很高的程序中,往往更重视缩短GC的最大中断时间(想想车辆制动控制程序因为GC而延迟响应的话后果是不堪设想的),必须能够对GC所产生的中断时间做出预测(例如将最多只能中断10ms作为附加条件)。
因此,为了维持程序的实时性,不等到GC全部完成,而是将GC操作细分成多个部分逐一执行,这种方式就被称为“增量回收”(Incremental GC)。
由于增量回收的过程是渐进式的,可以将中断时间控制在一定长度之内,另外由于由于中断操作需要消耗一定的时间,GC所消耗的总时间也会增加。
3.3 并行回收方式
在多核环境中,可以通过利用多线程发挥多CPU的性能,并行回收正是通过最大限度地利用多CPU的处理能力来进行GC操作的一种方式。
并行回收的基本原理是:在原有程序运行的同时进行GC操作。相对于在一个CPU上进行GC任务分割的增量回收来说,并行回收可以利用多CPU的性能,尽可能让这些GC任务并行(同时)进行。
不过,要让GC操作完全并行并且一点都不影响原有程序的运行是做不到的。因此,在GC操作的某些特定阶段,还是需要暂停原有程序的运行。
四、GC大一统理论
像标记清除和复制收集之类的算法是从根开始扫描以判断对象生死的算法,被称为跟踪回收(Tracing GC)。而引用计数算法则是当对象之间的引用关系发生变化时,通过对引用计数进行更新来判定对象生死。
2004年IBM研究中心发表了一篇论文,提出了一个理论:任何一种GC算法都是跟踪回收和引用计数两种方式的组合,两者的关系正如“物质”和“反物质”一样,是相互对立的。对其中一方进行改善的技术之中,必然存在对另一方进行改善的技术,而其结果只是两者的组合而已。
参考资料

(1)本文全文源自Ruby之父松本行弘的《代码的未来》一书!
(2)霍旭东,《不得不知的CLR中的GC》【好文一篇,值得阅读】
(3)cposture,《GC/垃圾回收简介》
(4)周旭龙,《.NET基础拾遗之内存管理基础》
《代码的未来》读书笔记:内存管理与GC那点事儿的更多相关文章
- 代码的未来读书笔记<二>
代码的未来读书笔记<二> 3.1语言的设计 对Ruby JavaScript Java Go 从服务端client以及静态动态这2个角度进行了对照. 这四种语言因为不同的设计方针,产生了不 ...
- 《Troubleshooting SQL Server》读书笔记-内存管理
自调整的数据库引擎(Self-tuning Database Engine) 长期以来,微软都致力于自调整(Self-Tuning)的SQL Server数据库引擎,用以降低产品的总拥有成本.从SQL ...
- delphi 精要-读书笔记(内存分配释放)
delphi 精要-读书笔记(内存分配释放) 1.内存分为三个区域:全局变量区,栈区,堆区 全局变量区:专门存放全局变量 栈区:分配在栈上的变量可被栈管理器自动释放 堆区:堆上的变量内存必须人 ...
- Linux内核笔记--内存管理之用户态进程内存分配
内核版本:linux-2.6.11 Linux在加载一个可执行程序的时候做了种种复杂的工作,内存分配是其中非常重要的一环,作为一个linux程序员必然会想要知道这个过程到底是怎么样的,内核源码会告诉你 ...
- OCP读书笔记(13) - 管理内存
SGA 1. 什么是LRULRU表示Least Recently Used,也就是指最近最少使用的buffer header链表LRU链表串联起来的buffer header都指向可用数据块 2. 什 ...
- c语言学习笔记.内存管理.
内存: 每个程序的内存是分区的:堆区.栈区.静态区.代码区. 1.代码区:放置所有的可执行代码,包括main函数. 2.静态区:存放所有的全局变量和静态变量. 3.栈区:栈(stack),先进后出.存 ...
- [转]linux内核分析笔记----内存管理
转自:http://blog.csdn.net/Baiduluckyboy/article/details/9667933 内存管理,不用多说,言简意赅.在内核里分配内存还真不是件容易的事情,根本上是 ...
- redis源码笔记-内存管理zmalloc.c
redis的内存分配主要就是对malloc和free进行了一层简单的封装.具体的实现在zmalloc.h和zmalloc.c中.本文将对redis的内存管理相关几个比较重要的函数做逐一的介绍 参考: ...
- Linux 0.11源码阅读笔记-内存管理
内存管理 Linux内核使用段页式内存管理方式. 内存池 物理页:物理空闲内存被划分为固定大小(4k)的页 内存池:所有空闲物理页组成内存池,以页为单位进行分配回收.并通过位图记录了每个物理页是否空闲 ...
随机推荐
- 浅入tomcat
前言:学习笔记,以供参考 1.什么是服务器 所谓的服务器其实就是一段别人写好的程序,服务器有两个能力. a.可以帮助我们来管理资源. b.可以将资源向外界发布以便于外界来访问这个资源. 2.资源有哪些 ...
- 使用简介<EntityFramework6.0>
序言 在这一篇中,我们将演示EnitityFramework基本的建模[建模也是EntityFramework最核心的特性]范例,例如实体的分离和继承等.我们开始了演示如何创建一个简单的概念模型的例子 ...
- Cocos2d-x 生成真正的随机数
关于随机数 cocos2d-x 定义了一个宏 CCRANDOM_0_1 生成的是 [0, 1] 之间的值 因此,要生成 [0-100] 之间的数 CCRANDOM_0_1 * 100 生成 [ ...
- RANSAC算法笔记
最近在做平面拟合,待处理的数据中有部分噪点需要去除,很多论文中提到可以使用Ransac方法来去除噪点. 之前在做图像配准时,用到了Ransac算法,但是没有去仔细研究,现在好好研究一番. 参考: ht ...
- 常用的sublime text插件(很爽哦)
个人比较懒,平时喜欢用webstorm,但是因为webstorm打开实在太慢了,并且太看设备,所以本人编辑简单的文件依然会选择使用sublime,虽然网上有很多关于此类插件的分享了,但是感觉都是片段, ...
- oracle(sql)基础篇系列(二)——多表连接查询、子查询、视图
多表连接查询 内连接(inner join) 目的:将多张表中能通过链接谓词或者链接运算符连接起来的数据查询出来. 等值连接(join...on(...=...)) --选出雇员的名字和雇员所 ...
- 获取终端ip地址
网上找的,记录下 import java.io.*; import java.net.*; import java.util.*; //import org.apache.http.conn.util ...
- 2015 ACM/ICPC EC-Final
A. Boxes and Balls 二分找到最大的不超过$n$的$\frac{x(x+1)}{2}$形式的数即可. #include <bits/stdc++.h> using name ...
- kafka的log存储解析——topic的分区partition分段segment以及索引等
转自:http://blog.csdn.net/jewes/article/details/42970799 引言 Kafka中的Message是以topic为基本单位组织的,不同的topic之间是相 ...
- Hive函数大全
一.关系运算: 1. 等值比较: = 语法:A=B 操作类型:所有基本类型 描述: 如果表达式A与表达式B相等,则为TRUE:否则为FALSE 举例: hive> select 1 from l ...