《代码的未来》读书笔记:内存管理与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)的页 内存池:所有空闲物理页组成内存池,以页为单位进行分配回收.并通过位图记录了每个物理页是否空闲 ...
随机推荐
- Java源码之 java.util.concurrent 学习笔记01
准备花点时间看看 java.util.concurrent这个包的源代码,来提高自己对Java的认识,努力~~~ 参阅了@梧留柒的博客!边看源码,边通过前辈的博客学习! 包下的代码结构分类: 1.ja ...
- Python-基础数据类型
数据类型 计算机顾名思义就是可以做数学计算的机器,因此,计算机程序理所当然地可以处理各种数值.但是,计算机能处理的远不止数值,还可以处理文本.图形.音频.视频.网页等各种各样的数据,不同的数据,需要定 ...
- 组合模式/composite模式/对象结构型模式
组合模式/composite模式/对象结构型 意图 将对象组合成树形结构以表示"整体--部分"的层次结构.Composite使得用户对单个对象和组合对象的使用具有一致性. 动机 C ...
- UILAbel 设置了attributedText 后省略号不显示
今天遇见个大坑呀,UILabel我设置了 attributedText ,并且设置了 lineBreakMode = NSLineBreakByTruncatingTail 就是想让多余的内容显示成省 ...
- css3选择器
原网站 cnblogs.com/tianshang/p/5982513.html通配符选择器 通配选择器的作用就是对页面上所有的元素都生效, 页面上的所有标签都会展示出通配符选择器设定的样式. 这样的 ...
- SilverLight CheckBox 控件 DataContext属性与DataContextChanged事件
当CheckBox对象创建时,会触发一次DataContextChanged事件,默认值待定,销毁时不会触发,代码修改DataContext时也会触发
- 判断是否安装APP
var time; $('#open').on('click',function(){ window.location="协议";//打开某手机上的某个app应用 time = s ...
- BZOJ2342 Manacher + set
题一:别人介绍的一道题,题意是给出一个序列,我们要求出一段最常的连续子序列,满足:该子序列能够被平分为三段,第一段和第二段形成回文串,第二段和第三段形成回文串. 题二:BZOJ2342和这题非常的相似 ...
- IntelliJ IDEA 绝对好用快捷键
最近根据自己的使用习惯整理了一下在windows下常用的一些快捷键,有些确实非常实用. 常用快捷键 键 作用 备注 Ctrl+F12 显示当前类的所有方法 F2 定位下一个错误位置 Alt ...
- 树链剖分+线段树 HDOJ 4897 Little Devil I(小恶魔)
题目链接 题意: 给定一棵树,每条边有黑白两种颜色,初始都是白色,现在有三种操作: 1 u v:u到v路径(最短)上的边都取成相反的颜色 2 u v:u到v路径上相邻的边都取成相反的颜色(相邻即仅有一 ...