计算机经过两个机制,去实现内存的高效使用。java
第一种机制是虚拟内存。硬盘的容量实际上是远远大于内存的(RAM),虚拟内存会在内存不足的时候,把不常常访问的内存的数据写到硬盘里。虽说硬盘容量比较大,可是它的访问速度却很慢。若是内存和硬盘交换数据过于频繁,处理速度就会降低,计算机就会看上去像卡死了同样,这种现象被叫作抖动(Thrushing)。形成电脑蓝屏的主要缘由之一就是抖动。程序员
第二种机制就是垃圾回收(GC)。golang
虚拟内存的东西在计算机组成原理和操做系统的教科书里有相关的章节去讲。因为内容不少我就很少叙述了。主要来说一下GC的事情。面试
以前学习java以及参加java相关的面试,被问到关于相关GC的事情一直非常头疼,看了好多遍仍是记不住,脑壳里只有隐隐约约的一些关键字,什么老年代、新生代、full GC什么的。具体流程一被问就GG。算法
如今想一想,其实GC就是计算机帮助你去进行内存的回收。你用不到的数据若是一直占着内存,那么你的程序能用的内存就愈来愈少,因此须要进行内存的管理。好比你在写C、C++程序的时候,须要本身去管理内存,在堆上申请的内存最后须要本身手动释放,释放的内存会被操做系统从新利用。若是你认为某些内存空间“可能还会被用到”,或者是干脆忘记释放内存,这些没法访问的内存空间就会一直保留下来,形成内存浪费,最终致使性能降低和产生抖动。管理大量分配的内存空间,对于人来讲实际上是很困难的。编程
对于像Java、go这些语言,它们有本身的一套内存管理的机制,内存空间释放的自动化也就是GC,从某种程度上解放了程序员的双手。缓存
垃圾(Garbage)就是须要回收的对象。做为编程人员你知道对象何时不须要,可是计算机没法判断。所以,若是程序直接或者间接地引用一个对象,那么它就会被计算机标记为“存活”;相反的,没有被引用到的对象就被视为“死亡”。把这些“死亡”的对象找出来,而后做为垃圾进行回收,这就是GC的本质。并发
根(Root),就是判断对象是否可被引用的起始点。不一样语言和编译器对根有不一样规定,可是基本上是将变量和运行栈空间做为根。编程语言
标记清除原理很是简单,首先从根开始,将可能被引用的对象用递归的方式进行标记,而后将没有标记上的对象做为垃圾进行回收。函数
(2)部分GC开始执行,从根开始对可能被引用的对象打上标记。大多数状况下,这种标记是经过对象内部的标志(Flag)来实现的。被标记的对象咱们将它涂黑。
(3)中,被标记的对象所可以引用的对象也被打上标记。重复这一步骤,能够将从根开始可能被间接引用到的对象所有打上标记。到此为止的操做,称做标记阶段(Mark phase)。标记阶段完成是,被标记的对象就被看作是“存活”对象。
(4)中,将所有对象按顺序扫描一遍,将没有标记的对象进行回收。这一步操做称做清除阶段(Sweep phase)。在扫描的同时,还须要将存活对象的标记清除掉,以便下一次GC去处理。
标记清除算法的处理时间,是和存活对象数与对象总数二者的和相关的。
标记清除方式的优势:能够处理循环引用的对象。缺点:在分配了大量对象,而且其中只有一小部分存活的状况下,因为清除阶段还要对大量“死亡”的对象进行扫描,会致使消耗大量没必要要的时间。
复制收集(Copy and Collection)方式试图解决标记清除的缺点。这种算法中,会从根开始,被引用的对象复制到另外的空间中,而后再将复制的对象所可以引用的对象递归的方式不断复制下去。
(3)部分中,从已经复制的对象开始,再将能够被引用的对象复制到新空间中(串到了后面),“死亡”对象就被留存在了旧空间中。
(4)中,将旧空间废弃掉,就能够将这部分所占的空间一会儿所有释放掉,而没有必要再扫描每一个对象。下次GC的时候,如今的新空间就当作了将来的旧空间。
经过图1.2能够发现,复制收集方式,只有相似标记清除的标记阶段,而不存在须要扫描全部对象的状况。可是相比之下,复制收集将对象复制一份所须要的开销会比较大,所以在“存活”对象比例较高的状况下,反而会比较不利。
这种算法的另外一个好处就是它具备局部性(Locality)。在复制收集过程当中,会按照对象被引用的顺序将对象复制到新空间中。所以关系较近的对象被放在距离较近的内存空间中的可能性会提供,这被称为局部性。局部性高的状况,内存缓存会更容易有效运做,程序的性能也会获得提升。
引用计数的基本原理是,在每个对象中保存该对象的引用计数,当引用发生增减时对计数进行更新。这让我想到了C++里的智能指针(shared_ptr),曾经被面试手写shared_ptr的场景历历在目啊。
引用计数的增减,通常发生在变量赋值、对象内容更新、函数结束(局部变量再也不被引用)等时间点。当一个对象的引用计数变为0的时候,则说明它未来不会再被引用,所以能够释放相应的内存空间。
(2)中,当对象引用发生变化的时候,引用计数也跟着变。这里因为B到D的引用失效了,因而对象D的引用计数变为0,因为D的引用计数为0,所以由D到对象C和E的引用数也分别相应减小。结果,对象E的引用计数变为0,因而E也对应释放掉了。
(3)中,引用计数为0的对象被释放,“存活”的对象保留了下来。可以注意到,在整个GC的过程当中,并不须要对全部对象进行扫描。
这种方式最大优势,就是容易实现。它的另一个优势就是,当对象再也不被引用的瞬间就会被释放。其它的GC机制很难预测对象何时会被释放,而这种方式是当即被释放的。所以,由GC产生的中断时间(Pause time)就比较短。
固然这种方式也有缺点。最大的缺点就是,没法释放循环引用的对象。图1.4中A、B、C之间互相循环引用,它的引用计数永远不会为0,也就永远不会被释放。
它的另一个缺点就是,若是在必要的增减计数的时候遗漏掉了增减操做,或者是增减计数出错,就会产生内存错误。若是是手动管理计数就很容易产生bug。
它的最后一个缺点就是引用计数管理不适合并行处理。若是多个线程同时对引用计数进行增减,引用数值就会产生不一致的问题从而致使内存错误。所以引用计数必须采用独占的方式。若是引用计数频繁发生,每次须要使用加锁等并发控制机制的话,也会形成很大的额外开销。
GC的基本算法,大致上都是上面的三种方式或者是它们的衍生品。如今经过三种方式进行融合,出现一些其余高级的GC方式,即分代回收、增量回收和并行回收。有些状况也会对这些改良版的gc方式进行组合使用。
分代回收的目的,就是在程序运行期间,将GC所消耗的时间尽可能的缩短。
它基于这样一个通常程序的性质,即大部分对象都会在短期内成为垃圾,通过必定时间依然存活的对象每每有较长的寿命。对于刚分配不久的“年轻”对象进行重点扫描,应该能够更有效的回收大部分垃圾。
分代回收中,按照对象生成时间进行分代。刚刚生成不久的对象划分为新生代(Young generation),而存活了较长时间的对象划分为旧生代(Old generation)。不一样实现可能还会有更多划分。若是上面的对象寿命假说成立的话,只要扫描回收新生代对象,就能够回收掉废弃对象中的很大一部分。
这种只扫描新生代对象的回收操做,被称做小回收(Minor GC)。它的具体步骤以下。
首先从根开始一次常规扫描,找到“存活”对象。可使用复制收集算法或者标记清除算法,可是大部分使用了复制收集。须要注意的是,在扫描的过程当中,若是遇到属于旧生代的对象,则不对该对象进行递归扫描。这样须要扫描的对象就会大量减小。
而后,将第一次扫描后残留下来的对象划分到旧生代。具体来讲,若是使用复制收集算法,只要将复制目标空间设置为旧生代就能够了;标记清除方式的话,则采用在对象上设置某种标志的方式。
如今有一个问题,若是有从旧生代到新生代对象的引用怎么办?若是只扫描新生代,那么旧生代对新生代的引用就不会被检测到。这样一来,若是一个年轻的对象只有来自旧生代的引用,就会被误认为“死亡”。所以在分代回收中,会对对象的更新进行监视,将从旧生代对新生代的引用,记录在记录集(remembered set)的表中。在执行小回收过程当中,这个记录集也做为一个根(root)来对待。
记录引用的子程序工做方式以下。设有两个对象A、B,当对A内容进行改写,并加入对B的引用时,若是A属于旧生代,B属于新生代,则将该引用添加到记录集中。
这种检查程序须要对全部涉及修改对象内容的地方进行保护,所以也被称为写屏障(Write barrier)。写屏障也用在不少其余GC算法中,好比Go的gc算法也用到了写屏障。
虽然旧生代中的对象寿命通常比较长,可是最终也会“死亡”。随着程序运行,旧生代中“死亡”的对象不断增长。为了不这些“死亡”的旧生代对象白占内存空间,偶尔须要对包括旧生代在内的所有区域进行一次扫描回收。这种以所有区域为对象的GC操做叫Full GC
分代收集经过减小GC中扫描的对象数量进而缩短GC带来的平均中断时间,可是最终仍是须要一次Full GC,所以最大中断时间并无改善。GC的性能也会被程序行为、分代数量、Full GC的触发条件等因素大幅左右。
实时性要求很高的程序中,相比缩短GC平均中断时间,缩短GC的最大中断时间更加剧要。
GC最大的中断时间要限定在一个时间范围内,可是通常GC算法没办法保证,由于GC产生的中断时间和对象的数量和状态有关系。所以为了维持程序的实时性,不等到GC所有完成,而是将GC操做细分红多个部分逐一执行。这种方式就是增量回收(Incremental GC)
增量回收过程当中程序自己会继续运行,为了不对象之间的引用关系改变而致使GC回收出错,增量回收也使用了写屏障,来说新被引用的对象做为扫描的起始点记录下来。
因为增量回收过程式分步渐进式的,能够将中断时间控制在必定长度以内。可是相应的中断操做须要消耗必定时间,GC消耗的总时间会相应增长。
如今的计算机基本上都有多个CPU核心。而并行回收方式正是经过最大限度利用多CPU的处理能力来进行GC操做。
并行回收的基本原理是,在原有程序运行的同时进行GC操做,这点和增量回收是类似的。相对于在一个CPU上进行任务分割的增量回收方式,并行回收能够利用多CPU的性能,尽量让这些GC任务并行(同时)进行。因为程序功能运行和GC操做是同时发生的,就会遇到和前面相同的问题,因此并行回收也须要利用写屏障来对对象当前的状态信息保持更新。
可是让GC彻底并行,而不影响原有程序运行时作不到的,在GC的某些特定阶段仍是须要暂停原有程序的运行。
目前的GC算法基本上都是上述算法的变体或者是组合,例如java里的分代回收,golang中的三色标记法。有时候由于对象之间的引用状态发生了变化,结果致使了原本是“存活”对象却被回收掉,这时候须要写屏障(Write barrier)来进行记录和保护。当咱们理解了上面的事情,其余具体编程语言的GC算法就不难理解了。