Java 与 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的高墙 ---《深刻理解Java虚拟机》
咱们知道手动管理内存意味着自由、精细化地掌控,可是却极度依赖于开发人员的水平和细心程度。html
若是使用完了忘记释放内存空间就会发生内存泄露,再如释放错了内存空间或者使用了悬垂指针则会发生没法预知的问题。java
这时候 Java 带着 GC 来了(GC,Garbage Collection 垃圾收集,早于 Java 提出),将内存的管理交给 GC 来作,减轻了程序员编程的负担,提高了开发效率。python
因此并非用 Java 就不须要内存管理了,只是由于 GC 在替咱们负重前行。程序员
可是 GC 并非那么万能的,不一样场景适用不一样的 GC 算法,须要设置不一样的参数,因此咱们不能就这样撒手无论了,只有深刻地理解它才能用好它。算法
关于 GC 内容相信不少人都有所了解。我最先得知有关 GC 的知识是来自《深刻理解Java虚拟机》,可是有关 GC 的内容单看这本书是不够的。编程
当时我觉得我懂不少了,后来通过了一番教育以后才知道啥叫无知者无畏。数组
并且过了一段时间不少有关 GC 的内容都说不上来了,其实也有不少同窗反映有些知识学了就忘,有些内容当时是理解的,过一段时间啥都不记得了。缓存
大部分状况是由于这块内容在脑海中没有造成体系,没有搞懂来龙去脉,没有把一些知识串起来。安全
近期我整理了下 GC 相关的知识点,想由点及面展开有关 GC 的内容,顺带理一理本身的思路,因此输出了这篇文章,但愿对你有所帮助。微信
有关 GC 的内容其实有不少,可是对于咱们这种通常开发而言是不须要太深刻的,因此我就挑选了一些我认为重要的整理出来,原本还有一些源码的我也删了,感受不必,重要的是在概念上理清。
原本还打算分析有关 JVM 的各垃圾回收器,可是文章太长了,因此分两篇写,下篇再发。
本篇整理的 GC 内容不限于 JVM 但大致上仍是偏 JVM,若是讲具体的实现默认指的是 HotSpot。
首先咱们知道根据 「Java虚拟机规范」,Java 虚拟机运行时数据区分为程序计数器、虚拟机栈、本地方法栈、堆、方法区。
而程序计数器、虚拟机栈、本地方法栈这 3 个区域是线程私有的,会随线程消亡而自动回收,因此不须要管理。
所以垃圾收集只须要关注堆和方法区。
而方法区的回收,每每性价比较低,由于判断能够回收的条件比较苛刻。
好比类的卸载须要此类的全部实例都已经被回收,包括子类。而后须要加载的类加载器也被回收,对应的类对象没有被引用这才容许被回收。
就类加载器这一条来讲,除非像特地设计过的 OSGI 等能够替换类加载器的场景,否则基本上回收不了。
而垃圾收集回报率高的是堆中内存的回收,所以咱们重点关注堆的垃圾收集。
既然是垃圾收集,咱们得先判断哪些对象是垃圾,而后再看看什么时候清理,如何清理。
常见的垃圾回收策略分为两种:一种是直接回收,即引用计数;另外一种是间接回收,即追踪式回收(可达性分析)。
你们也都知道引用计数有个致命的缺陷-循环引用,因此 Java 用了可达性分析。
那为何有明显缺陷的计数引用仍是有不少语言采用了呢?
好比 CPython ,由此看来引用计数仍是有点用的,因此我们就先来盘一下引用计数。
引用计数其实就是为每个内存单元设置一个计数器,当被引用的时候计数器加一,当计数器减小为 0 的时候就意味着这个单元再也没法被引用了,因此能够当即释放内存。
如上图所示,云朵表明引用,此时对象 A 有 1 个引用,所以计数器的值为 1。
对象 B 有两个外部引用,因此计数器的值为 2,而对象 C 没有被引用,因此说明这个对象是垃圾,所以能够当即释放内存。
由此能够知晓引用计数须要占据额外的存储空间,若是自己的内存单元较小则计数器占用的空间就会变得明显。
其次引用计数的内存释放等于把这个开销平摊到应用的平常运行中,由于在计数为 0 的那一刻,就是释放的内存的时刻,这其实对于内存敏感的场景很适用。
若是是可达性分析的回收,那些成为垃圾的对象不会立马清除,须要等待下一次 GC 才会被清除。
引用计数相对而言概念比较简单,不过缺陷就是上面提到的循环引用。
首先咱们知道像整型、字符串内部是不会引用其余对象的,因此不存在循环引用的问题,所以使用引用计数并无问题。
那像 List、dictionaries、instances 这类容器对象就有可能产生循环依赖的问题,所以 Python 在引用计数的基础之上又引入了标记-清除来作备份处理。
可是具体的作法又和传统的标记-清除不同,它采起的是找不可达的对象,而不是可达的对象。
Python 使用双向链表来连接容器对象,当一个容器对象被建立时,它被插入到这个链表中,当它被删除时则移除。
而后在容器对象上还会添加一个字段 gc_refs,如今我们再来看看是如何处理循环引用的:
具体以下图示例,A 和 B 对象循环引用, C 对象引用了 D 对象。
为了让图片更加清晰,我把步骤分开截图了,上图是 1-2 步骤,下图是 3-4 步骤。
最终循环引用的 A 和 B 都能被清理,可是天下没有免费的午饭,最大的开销之一是每一个容器对象须要额外字段。
还有维护容器链表的开销。根据 pybench,这个开销占了大约 4% 的减速。
至此咱们知晓了引用计数的优势就是实现简单,而且内存清理及时,缺点就是没法处理循环引用,不过能够结合标记-清除等方案来兜底,保证垃圾回收的完整性。
因此 Python 没有解决引用计数的循环引用问题,只是结合了非传统的标记-清除方案来兜底,算是曲线救国。
其实极端状况下引用计数也不会那么及时,你想假如如今有一个对象引用了另外一个对象,而另外一个对象又引用了另外一个,依次引用下去。
那么当第一个对象要被回收的时候,就会引起连锁回收反应,对象不少的话这个延时就凸显出来了。
可达性分析其实就是利用标记-清除(mark-sweep),就是标记可达对象,清除不可达对象。至于用什么方式清,清了以后要不要整理这都是后话。
标记-清除具体的作法是按期或者内存不足时进行垃圾回收,从根引用(GC Roots)开始遍历扫描,将全部扫描到的对象标记为可达,而后将全部不可达的对象回收了。
所谓的根引用包括全局变量、栈上引用、寄存器上的等。
看到这里你们不知道是否有点感受,咱们会在内存不足的时候进行 GC,而内存不足时也是对象最多时,对象最多所以须要扫描标记的时间也长。
因此标记-清除等于把垃圾积累起来,而后再一次性清除,这样就会在垃圾回收时消耗大量资源,影响应用的正常运行。
因此才会有分代式垃圾回收和仅先标记根节点直达的对象再并发 tracing 的手段。
但这也只能减轻没法根除。
我认为这是标记-清除和引用计数的思想上最大的差异,一个攒着处理,一个把这种消耗平摊在应用的平常运行中。
而不论标记-清楚仍是引用计数,其实都只关心引用类型,像一些整型啥的就不须要管。
因此 JVM 还须要判断栈上的数据是什么类型,这里又能够分为保守式 GC、半保守式 GC、和准确式 GC。
保守式 GC 指的是 JVM 不会记录数据的类型,也就是没法区份内存上的某个位置的数据究竟是引用类型仍是非引用类型。
所以只能靠一些条件来猜想是否有指针指向。好比在栈上扫描的时候根据所在地址是否在 GC 堆的上下界以内,是否字节对齐等手段来判断这个是否是指向 GC 堆中的指针。
之因此称之为保守式 GC 是由于不符合猜想条件的确定不是指向 GC 堆中的指针,所以那块内存没有被引用,而符合的却不必定是指针,因此是保守的猜想。
我再画一张图来解释一下,看了图以后应该就很清晰了。
前面咱们知道能够根据指针指向地址来判断,好比是否字节对齐,是否在堆的范围以内,可是就有可能出现刚好有数值的值就是地址的值。
这就混乱了,因此就不能肯定这是指针,只能保守认为就是指针。
所以确定不会有误杀对象的状况。只会有对象已经死了,可是有疑似指针的存在指向它,误觉得它还活着而放过了它的状况发生。
因此保守式 GC 会有放过一些“垃圾”,对内存不太友好。
而且由于疑似指针的状况,致使咱们没法确认它是不是真的指针,因此也就没法移动对象,由于移动对象就须要改指针。
有一个方法就是加个中间层,也就是句柄层,引用会先指到句柄,而后再从句柄表找到实际对象。
因此直接引用不须要改变,若是要移动对象只须要修改句柄表便可。不过这样访问就多了一层,效率就变低了。
半保守式GC,在对象上会记录类型信息而其余地方仍是没有记录,所以从根扫描的话仍是同样,得靠猜想。
可是获得堆内对象了以后,就能准确知晓对象所包含的信息了,所以以后 tracing 都是准确的,因此称为半保守式 GC。
如今能够得知半保守式 GC 只有根直接扫描的对象没法移动,从直接对象再追溯出去的对象能够移动,因此半保守式 GC 可使用移动部分对象的算法,也可使用标记-清除这种不移动对象的算法。
而保守式 GC 只能使用标记-清除算法。
相信你们看下来已经知道准确意味 JVM 须要清晰的知晓对象的类型,包括在栈上的引用也能得知类型等。
能想到的能够在指针上打标记,来代表类型,或者在外部记录类型信息造成一张映射表。
HotSpot 用的就是映射表,这个表叫 OopMap。
在 HotSpot 中,对象的类型信息里会记录本身的 OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据,而在解释器中执行的方法能够经过解释器里的功能自动生成出 OopMap 出来给 GC 用。
被 JIT 编译过的方法,也会在特定的位置生成 OopMap,记录了执行到该方法的某条指令时栈上和寄存器里哪些位置是引用。
这些特定的位置主要在:
这些位置就叫做安全点(safepoint)。
那为何要选择这些位置插入呢?由于若是对每条指令都记录一个 OopMap 的话空间开销就过大了,所以就选择这些个关键位置来记录便可。
因此在 HotSpot 中 GC 不是在任何位置都能进入的,只能在安全点进入。
至此咱们知晓了能够在类加载时计算获得对象类型中的 OopMap,解释器生成的 OopMap 和 JIT 生成的 OopMap ,因此 GC 的时候已经有充足的条件来准确判断对象类型。
所以称为准确式 GC。
其实还有个 JNI 调用,它们既不在解释器执行,也不会通过 JIT 编译生成,因此会缺乏 OopMap。
在 HotSpot 是经过句柄包装来解决准确性问题的,像 JNI 的入参和返回值引用都经过句柄包装起来,也就是经过句柄再访问真正的对象。
这样在 GC 的时候就不用扫描 JNI 的栈帧,直接扫描句柄表就知道 JNI 引用了 GC 堆中哪些对象了。
咱们已经提到了安全点,安全点固然不是只给记录 OopMap 用的,由于 GC 须要一个一致性快照,因此应用线程须要暂停,而暂停点的选择就是安全点。
咱们来捋一遍思路。首先给个 GC 名词,在垃圾收集场景下将应用程序称为 mutator 。
一个能被 mutator 访问的对象就是活着的,也就是说 mutator 的上下文包含了能够访问存活对象的数据。
这个上下文其实指的就是栈、寄存器等上面的数据,对于 GC 而言它只关心栈上、寄存器等哪一个位置是引用,由于它只须要关注引用。
可是上下文在 mutator 运行过程当中是一直在变化的,因此 GC 须要获取一个一致性上下文快照来枚举全部的根对象。
而快照的获取须要中止 mutator 全部线程,否则就得不到一致的数据,致使一些活着对象丢失,这里说的一致性其实就像事务的一致性。
而 mutator 全部线程中这些有机会成为暂停位置的点就叫 safepoint 即安全点。
openjdk 官网对安全点的定义是:
A point during program execution at which all GC roots are known and all heap object contents are consistent. From a global point of view, all threads must block at a safepoint before the GC can run.
不过 safepoint 不只仅只有 GC 有用,好比 deoptimization、Class redefinition 都有,只是 GC safepoint 比较知名。
咱们再来想一下能够在哪些位置放置这个安全点。
对于解释器来讲其实每一个字节码边界均可以成为一个安全点,对于 JIT 编译的代码也能在不少位置插入安全点,可是实现上只会在一些特定的位置插入安全点。
由于安全点是须要 check 的,而 check 须要开销,若是安全点过多那么开销就大了,等于每执行几步就须要检查一下是否须要进入安全点。
其次也就是咱们上面提到的会记录 OopMap ,因此有额外的空间开销。
那 mutator 是如何得知此时须要在安全点暂停呢?
其实上面已经提到了是 check,再具体一些还分解释执行和编译执行时不一样的 check。
在解释执行的时候的 check 就是在安全点 polling 一个标志位,若是此时要进入 GC 就会设置这个标志位。
而编译执行是 polling page 不可读,在须要进入 safepoint 时就把这个内存页设为不可访问,而后编译代码访问就会发生异常,而后捕获这个异常挂起即暂停。
这里可能会有同窗问,那此时阻塞住的线程咋办?它到不了安全点啊,总不能等着它吧?
这里就要引入安全区域的概念,在这种引用关系不会发生变化的代码段中的区域称为安全区域。
在这个区域内的任意地方开始 GC 都是安全的,这些执行到安全区域的线程也会标识本身进入了安全区域,
因此会 GC 就不用等着了,而且这些线程若是要出安全区域的时候也会查看此时是否在 GC ,若是在就阻塞等着,若是 GC 结束了那就继续执行。
可能有些同窗对counted 循环有点疑问,像for (int i...)
这种就是 counted 循环,这里不会埋安全点。
因此说假设你有一个 counted loop 而后里面作了一些很慢的操做,因此颇有可能其余线程都进入安全点阻塞就等这个 loop 的线程完毕,这就卡顿了。
前面咱们提到标记-清除方式的 GC 其实就是攒着垃圾收,这样集中式回收会给应用的正常运行带来影响,因此就采起了分代收集的思想。
由于研究发现有些对象基本上不会消亡,存在的时间很长,而有些对象出来没多久就会被咔嚓了。这其实就是弱分代假说和强分代假说。
因此将堆分为新生代和老年代,这样对不一样的区域能够根据不一样的回收策略来处理,提高回收效率。
好比新生代的对象有朝生夕死的特性,所以垃圾收集的回报率很高,须要追溯标记的存活对象也不多,所以收集的也快,能够将垃圾收集安排地频繁一些。
新生代每次垃圾收集存活的对象不多的话,若是用标记-清除算法每次须要清除的对象不少,所以能够采用标记-复制算法,每次将存活的对象复制到一个区域,剩下了直接所有清除便可。
可是朴素的标记-复制算法是将堆对半分,可是这样内存利用率过低了,只有 50%。
因此 HotSpot 虚拟机分了一个 Eden 区和两个Survivor,默认大小比例是8∶1:1,这样利用率有 90%。
每次回收就将存活的对象拷贝至一个 Survivor 区,而后清空其余区域便可,若是 Survivor 区放不下就放到
老年代去,这就是分配担保机制。
而老年代的对象基本上都不是垃圾,因此追溯标记的时间比较长,收集的回报率也比较低,因此收集频率安排的低一些。
这个区域因为每次清除的对象不多,所以能够用标记-清除算法,可是单单清除不移动对象的话会有不少内存碎片的产生,因此还有一种叫标记-整理的算法,等于每次清除了以后须要将内存规整规整,须要移动对象,比较耗时。
因此能够利用标记-清除和标记-整理二者结合起来收集老年代,好比平日都用标记-清除,当察觉内存碎片实在太多了就用标记-整理来配合使用。
可能还有不少同窗对的标记-清除,标记-整理,标记-复制算法不太清晰,没事,我们来盘一下。
分为两个阶段:
标记阶段:tracing 阶段,从根(栈、寄存器、全局变量等)开始遍历对象图,标记所遇到的每一个对象。
清除阶段:扫描堆中的对象,将为标记的对象做为垃圾回收。
基本上就是下图所示这个过程:
清除不会移动和整理内存空间,通常都是经过空闲链表(双向链表)来标记哪一块内存空闲可用,所以会致使一个状况:空间碎片。
这会使得明明总的内存是够的,可是申请内存就是不足。
并且在申请内存的时候也有点麻烦,须要遍历链表查找合适的内存块,会比较耗时。
因此会有多个空闲链表的实现,也就是根据内存分块大小组成不一样的链表,好比分为大分块链表和小分块链表,这样根据申请的内存分块大小遍历不一样的链表,加快申请的效率。
固然还能够分更多个链表。
还有标记,标记的话通常咱们会以为应该是标记在对象身上,好比标记位放在对象头中,可是这对写时复制不兼容。
等于每一次 GC 都须要修改对象,假设是 fork 出来的,实际上是共享一块内存,那修改必然致使复制。
因此有一种位图标记法,其实就是将堆的内存某个块用一个位来标记。就像咱们的内存是一页一页的,堆中的内存能够分红一块一块,而对象就是在一块,或者多块内存上。
根据对象所在的地址和堆的起始地址就能够算出对象是在第几块上,而后用一个位图中的第几位在置为 1 ,代表这块地址上的对象被标记了。
并且用位图表格法不只能够利用写时复制,清除也更加高效,若是标记在对象头上,那么须要遍历整个堆来扫描对象,如今有了位图,能够快速遍历清除对象。
可是不管是标记对象头仍是利用位图,标记-清除的碎片问题仍是处理不了。
所以就引出了标记-复制和标记-整理。
首先这个算法会把堆分为两块,一块是 From、一块是 To。
对象只会在 From 上生成,发生 GC 以后会找到全部存活对象,而后将其复制到 To 区,以后总体回收 From 区。
再将 To 区和 From 区身份对调,即 To 变成 From , From 变成 To,我再用图来解释一波。
能够看到内存的分配是紧凑的,不会有内存碎片的产生。
不须要空闲链表的存在,直接移动指针分配内存,效率很高。
对 CPU缓存亲和性高,由于从根开始遍历一个节点,是深度优先遍历,把关联的对象都找到,而后内存分配在相近的地方。
这样根据局部性原理,一个对象被加载了那它所引用的对象也同时被加载,所以访问缓存直接命中。、
固然它也是有缺点的,由于对象的分配只能在 From 区,而 From 区只有堆一半大小,所以内存的利用率是 50%。
其次若是存活的对象不少,那么复制的压力仍是很大的,会比较慢。
而后因为须要移动对象,所以不适用于上文提到的保守式 GC。
固然我上面描述的是深度优先就是递归调用,有栈溢出风险,还有一种 Cheney 的 GC 复制算法,是采用迭代的广度优先遍历,具体不作分析了,有兴趣自行搜索。
标记-整理其实和标记-复制差很少,区别在于复制算法是分为两个区来回复制,而整理不分区,直接整理。
算法思路仍是很清晰的,将存活的对象往边界整理,也没有内存碎片,也不须要复制算法那样腾出一半的空间,因此内存利用率也高。
缺点就是须要对堆进行屡次搜索,毕竟是在一个空间内又标记,又移动的,因此总体而言花费的时间较多,并且若是堆很大的状况,那么消耗的时间将更加突出。
至此相信你对标记-清除、标记-复制和标记-整理都清晰了,让咱们再回到刚才提到的分代收集。
咱们已经根据对象存活的特性进行了分代,提升了垃圾收集的效率,可是像在回收新生代的时候,有可能有老年代的对象引用了新生代对象,因此老年代也须要做为根,可是若是扫描整个老年代的话效率就又下降了。
因此就搞了个叫记忆集(Remembered Set)的东西,来记录跨代之间的引用而避免扫描总体非收集区域。
所以记忆集就是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。根据记录的精度分为
最多见的是用卡精度来实现记忆集,称之为卡表。
我来解释下什么叫卡。
拿对象精度来距离,假设新生代对象 A 被老年代对象 D 引用了,那么就须要记录老年代 D 所在的地址引用了新生代对象。
那卡的意思就是将内存空间分红不少卡片。假设新生代对象 A 被老年代 D 引用了,那么就须要记录老年代 D 所在的那一块内存片有引用新生代对象。
也就是说堆被卡切割了,假设卡的大小是 2,堆是 20,那么堆一共能够划分红 10 个卡。
由于卡的范围大,若是此时 D 旁边在同一个卡内的对象也有引用新生代对象的话,那么就只须要一条记录。
通常会用字节数组来实现卡表,卡的范围也是设为 2 的 N 次幂大小。来看一下图就很清晰了。
假设地址从 0x0000 开始,那么字节数组的 0号元素表明 0x0000~0x01FF,1 号表明0x0200~0x03FF,依次类推便可。
而后到时候回收新生代的时候,只须要扫描卡表,把标识为 1 的脏表所在内存块加入到 GC Roots 中扫描,这样就不须要扫描整个老年代了。
用了卡表的话占用内存比较少,可是相对字长、对象来讲精度不许,须要扫描一片。因此也是一种取舍,到底要多大的卡。
还有一种多卡表,简单的说就是有多张卡表,这里我画两张卡表示意一下。
上面的卡表表示的地址范围更大,这样能够先扫描范围大的表,发现中间一块脏了,而后再经过下标计算直接获得更具体的地址范围。
这种多卡表在堆内存比较大,且跨代引用较少的时候,扫描效率较高。
而卡表通常都是经过写屏障来维护的,写屏障其实就至关于一个 AOP,在对象引用字段赋值的时候加入更新卡表的代码。
这其实很好理解,说白了就是当引用字段赋值的时候判断下当前对象是老年代对象,所引用对象是新生代对象,因而就在老年代对象所对应的卡表位置置为 1,表示脏,待会须要加入根扫描。
不过这种将老年代做为根来扫描会有浮动垃圾的状况,由于老年代的对象可能已经成为垃圾,因此拿垃圾来做为根扫描出来的新生代对象也颇有多是垃圾。
不过这是分代收集必须作出的牺牲。
所谓的增量式 GC 其实就是在应用线程执行中,穿插着一点一点的完成 GC,来看个图就很清晰了
这样看起来 GC 的时间跨度变大了,可是 mutator 暂停的时间变短了。
对于增量式 GC ,Dijkstra 等人抽象除了三色标记算法,来表示 GC 中对象三种不一样情况。
白色:表示还未搜索到的对象。
灰色:表示正在搜索还未搜索完的对象。
黑色:表示搜索完成的对象。
下面这图从维基百科搞得,虽然说颜色没对上,可是意思是对的(black 画成了蓝色,grey画成了黄色)。
我再用文字概述一下三色的转换。
GC 开始前全部对象都是白色,GC 一开始全部根可以直达的对象被压到栈中,待搜索,此时颜色是灰色。
而后灰色对象依次从栈中取出搜索子对象,子对象也会被涂为灰色,入栈。当其全部的子对象都涂为灰色以后该对象被涂为黑色。
当 GC 结束以后灰色对象将所有没了,剩下黑色的为存活对象,白色的为垃圾。
通常增量式标记-清除会分为三个阶段:
这里解释下 GC 中两个名词的含义。
并发:应用线程和 GC 线程一块儿执行。
并行:多个 GC 线程一块儿执行。
看起来好像三色标记没啥问题?来看看下图。
第一个阶段搜索到 A 的子对象 B了,所以 A 被染成了黑色,B 为灰色。此时须要搜索 B。
可是在 B 开始搜索时,A 的引用被 mutator 换给了 C,而后此时 B 到 C 的引用也被删了。
接着开始搜索 B ,此时 B 没有引用所以搜索结束,这时候 C 就被当垃圾了,所以 A 已经黑色了,因此不会再搜索到 C 了。
这就是出现漏标的状况,把还在使用的对象当成垃圾清除了,很是严重,这是 GC 不容许的,宁愿放过,不能杀错。
还有一种状况多标,好比 A 变成黑色以后,根引用被 mutator 删除了,那其实 A 就属于垃圾,可是已经被标记为黑色了,那就得等下次 GC 清除了。
这其实就是标记过程当中没有暂停 mutator 而致使的,但这也是为了让 GC 减小对应用程序运行的影响。
多标其实还能接受,漏标的话就必须处理了,咱们能够总结一下为何会发生漏标:
只要打破这两个条件任意一个就不会发生漏标的状况。
这时候能够经过如下手段来打破两个条件:
利用写屏障在黑色引用白色对象时候,将白色对象置为灰色,这叫增量更新。
利用写屏障在灰色对象删除对白色对象的引用时,将白色对象置为灰,其实就是保存旧的引用关系。这叫STAB(snapshot-at-the-beginning)。
至此有关垃圾回收的关键点和思路都差很少了,具体有关 JVM 的垃圾回收器等我下篇再做分析。
如今咱们再来总结一下。
关于垃圾回收首先得找出垃圾,而找出垃圾分为两个流派,一个是引用计数,一个是可达性分析。
引用计数垃圾回收的及时,对内存较友好,可是循环引用没法处理。
可达性分析基本上是现代垃圾回收的核心选择,可是因为须要统一回收比较耗时,容易影响应用的正常运行。
因此可达性分析的研究方向就是往如何减小对应用程序运行的影响即减小 STW(stop the world) 的时间。
所以根据对象分代假说研究出了分代收集,根据对象的特性划分了新生代和老年代,采起不一样的收集算法,提高回收的效率。
千方百计的拆解 GC 的步骤使得能够与应用线程并发,而且采起并行收集,加快收集速度。
还有往评估的方向的延迟回收或者说回收部分垃圾来减小 STW 的时间。
总的而言垃圾回收仍是很复杂的,由于有不少细节,我这篇就是浅显的纸上谈兵,不要被个人标题骗了哈哈哈哈。
这篇文章写了挺久的,主要是内容不少如何编排有点难,我也选择性的剪了不少了,但仍是近 1W 字。
期间也查阅了不少资料,不过我的能力有限,若是有纰漏的地方请抓紧联系我。
http://arctrix.com/nas/python...
https://openjdk.java.net/grou...
《The Garbage Collection Handbook 》
https://www.iteye.com/blog/us... R大的博客
https://www.jianshu.com/u/90a... 占小狼的博客
微信搜索【yes的练级攻略】,关注 yes,从一点点到亿点点,咱们下篇见。