《深刻理解java虚拟机》学习笔记系列——垃圾收集器&内存分配策略

本文主要从GC(垃圾回收)的角度试着对jvm中的内存分配策略与相应的垃圾收集器作一个介绍。html

注:仍是老规矩,本着能画图就不BB原则,尽可能将各知识点经过思惟导图或者其余模型图的方式进行说明。文字仅记录额外的思考与心得,以及其余特殊状况java

内存分配策略

本部分的回答主要围绕 哪些内存须要回收?何时回收?以及如何回收?这三个问题来进行介绍。算法

哪些内存须要回收?

一张图总结

clipboard.png

补充说明

由上图可知,只有堆区和静态区,运行时才能知道建立的对象信息,因此垃圾收集器所须要关注的内存也就集中于这两个部分了。编程

何时回收?

堆区

回收依据

不可能再被任何途径使用(对象已死)segmentfault

对象存活断定算法

主流对象存过断定算法分为以下两种:安全

  • 引用计数算法数据结构

clipboard.png

  • 可达性分析算法并发

clipboard.png

补充说明

clipboard.png

在 java 中引用分为强软弱虚四种形式,jvm

  • 最多见的就是强引用,好比相似Object obj = new Object()这种。高并发

  • 软引用经过 “SoftReference” 来实现

  • 弱引用经过 “WeakReference” 来实现

  • 弱引用经过 “PhantomReference” 来实现

方法区

在方法区中,垃圾收集远不像堆区那么频繁和高效。咱们聚焦于两部份内容,废弃常量和无用的类。

clipboard.png

补充介绍

  • 针对是否对类进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制

  • 针对类加载和卸载信息,可使用 -verbose:class 以及 -XX:+TraceClassLoading-XX:TraceClassUnLoading

注:-verbose:class 以及 -XX:+TraceClassLoading 能够用在Product版的虚拟机中。-XX:+TraceClassUnLoading 参数须要 FastDebug 版的虚拟机支持。

如何回收?

其实如何回收也是具体的垃圾收集器该干的的事。可是各个平台的虚拟机操做内存的方法又各不相同。因此这部分先站在一个略宏观的角度讨论下关于垃圾回收的几种常见算法。

标记-清除算法

示意图

图片描述

一张图总结

clipboard.png

复制收集算法

示意图:

图片描述

一张图总结

clipboard.png

拓展说明

传统的复制算法因为将内存划分为了两半,致使同一时间内存的可用率只有50%,这显然是难以接受的。
因此也早就有了机智的前辈对此方法进行了改进,接下来就来介绍下 HotSpot 虚拟机中是如何改进的~

clipboard.png

标记-整理算法

示意图

图片描述

一张图总结

clipboard.png

拓展说明

复制收集算法在对象存活率较高的时候,就要进行较多的复制操做,效率将会变低。更关键的是,若是不想浪费50%的控件,就须要有额外的空间进行分配担保,以应对被使用的内存中全部对象都100%存活的极端状况。

因此针对老年代的特色,通常更倾向使用相似“标记-整理”而非“复制收集”这样的算法。

分代收集算法

一张图总结

clipboard.png

HotSpot 的算法实现难点

前面从理论上介绍了对象存活的断定方法和垃圾收集算法的思想,可是具体实现的过程当中,也才会发现一些在理论思考时不会注意的点。

枚举根节点

难点

clipboard.png

解决方案

经过一组称为 OopMap 的数据结构来达到目的:

  • 在类加载完成的时候,HotSpot 将对象内数据类型及其偏移量记录下来

  • JIT 编译过程当中也在特定的位置记录下栈和寄存器中哪些位置使引用

经过这种事前约定记录位置的方法,实现快速遍历根节点引用

安全点

概念由来

安全点的由来自己也是为了解决一个难题而产生的:

clipboard.png

位置选定的要点

clipboard.png

如何进入安全点

clipboard.png

安全区域

clipboard.png

垃圾收集器

不一样的厂商,不一样版本的虚拟机所提供的垃圾收集器差异很大,为了方便讨论,这里以 JDK 1.7 Update 14 为基础进行讨论。

一张图总结

clipboard.png

上图展现了7种做用于不一样分代的收集器,若是两个收集器之间存在连线,就说明它们能够搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器仍是老年代收集器。

Serial 收集器

运行示意图(新生代部分)

clipboard.png

优缺点分析

clipboard.png

ParNew 收集器

运行示意图(新生代部分)

clipboard.png

优缺点分析

clipboard.png

补充说明

ParNew 默认开启的垃圾收集器线程数就是CPU数量,可经过-XX:parallelGCThreads参数来限制收集器线程数

另:
从 ParNew 收集器开始,后续还有几款并发和并行收集器。这里解释一下这两个名词:并发和并行。这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们能够解释以下:

  • 并行(Parallel):指多条垃圾收集线程并行工做,但此时用户线程仍处于等待状态。

  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不必定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另外一个CPU上。

Parallel Scavenge 收集器

运行示意图(新生代部分)

clipboard.png

优缺点分析

clipboard.png

补充说明

提供了两个参数来精确控制吞吐量:

  1. 最大垃圾收集器停顿时间(-XX:MaxGCPauseMillis 大于0的毫秒数,停顿时间小了就要牺牲相应的吞吐量和新生代空间),

  2. 设置吞吐量大小(-XX:GCTimeRatio 大于0小于100的整数,默认99,也就是容许最大1%的垃圾回收时间)。
      

还有一个参数表示自适应调节策略(GC Ergonomics)(-XX:UseAdaptiveSizePolicy)。就不用手动设置新生代大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)晋升老年代对象大小(-XX:PretenureSizeThreshold),会根据当前系统的运行状况手机监控信息,动态调整停顿时间和吞吐量大小。也是其与PreNew收集器的一个重要区别,也是其没法与CMS收集器搭配使用的缘由(CMS收集器尽量地缩短垃圾收集时用户线程的停顿时间,以提高交互体验)。

另:所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。

虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那么吞吐量就是99%。

Serial Old 收集器

运行示意图(老年代部分)

clipboard.png

优缺点分析

clipboard.png

Parallel Old 收集器

运行示意图(老年代部分)

clipboard.png

(图画错了,老年代应该是并行收集才对)

优缺点分析

clipboard.png

CMS 收集器

运行示意图

clipboard.png

优缺点分析

clipboard.png

补充说明

CMS收集器是基于“标记-清除”算法实现的,整个收集过程大体分为4个步骤:
①.初始标记(CMS initial mark)
②.并发标记(CMS concurrenr mark)
③.从新标记(CMS remark)
④.并发清除(CMS concurrent sweep)

其中初始标记、从新标记这两个步骤任然须要停顿其余用户线程(Stop The World)。
初始标记仅仅只是标记出 GC ROOTS 能直接关联到的对象,速度很快,并发标记阶段是进行 GC ROOTS 根搜索算法阶段,会断定对象是否存活。而从新标记阶段则是为了修正并发标记期间,因用户程序继续运行而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。

因为整个过程当中耗时最长的并发标记和并发清除过程当中,收集器线程均可以与用户线程一块儿工做,因此总体来讲,CMS收集器的内存回收过程是与用户线程一块儿并发执行的。

关于CMS的三个缺点,这里有更详细的解释说明:

  1. CMS收集器对CPU资源很是敏感。在并发(并发标记、并发清除)阶段,虽然不会致使用户线程停顿,可是会占用CPU资源而致使应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是:(CPU数量+3) / 4。收集器线程所占用的CPU数量为:(CPU+3)/4=0.25+3/(4*CPU)。所以这时垃圾收集器始终不会占用少于25%的CPU,所以当进行并发阶段时,虽然用户线程能够跑,可是很缓慢,特别是双核CPU的时候,已经占用了5/8的CPU,吞吐量会很低。为了解决这种状况,产生了“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)。就是采用抢占方式来模拟多任务机制,就是在并发(并发标记、并发清除)阶段,让GC线程、用户线程交替执行,尽可能减小GC线程独占CPU,这样垃圾收集过程更长,可是对用户程序影响小一些。实际上i-CMS效果很通常,目前已经被声明为“deprecated”。

  2. CMS收集器没法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而致使另外一次Full GC的产生。因为CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出如今标记过程以后,CMS没法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是因为在垃圾收集阶段用户线程还须要运行,即须要预留足够的内存空间给用户线程使用,所以CMS收集器不能像其余收集器那样等到老年代几乎彻底被填满了再进行收集,须要预留一部份内存空间提供并发收集时的程序运做使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也能够经过参数-XX:CMSInitiatingOccupancyFraction的值来提升触发百分比,以下降内存回收次数提升性能。JDK1.6中,CMS收集器的启动阈值已经提高到92%。要是CMS运行期间预留的内存没法知足程序其余线程须要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来从新进行老年代的垃圾收集,这样停顿时间就很长了。因此说参数-XX:CMSInitiatingOccupancyFraction设置的太高将会很容易致使“Concurrent Mode Failure”失败,性能反而下降。

  3. 最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来不少麻烦。好比说大对象,内存空间找不到连续的空间来分配不得不提早触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC以后增长一个内存碎片的合并整理过程,可是内存整理过程是没法并发的,所以解决了空间碎片问题,却使停顿时间变长。还可经过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC以后,跟着来一次碎片整理过程(默认值是0,表示每次进入Full GC时都进行碎片整理)。

G1 收集器

运行示意图

clipboard.png

优缺点分析

clipboard.png

补充说明

G1收集器之因此能创建可预测的停顿时间模型,是由于它能够有计划地避免在整个Java堆中进行全区域的垃圾收集。

在G1以前的其余收集器进行收集的范围都是整个新生代或者老年代,而G1再也不是这样。使用G1收集器时,Java堆的内存布局与就与其余收集器有很大差异,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分Region(不须要连续)的集合。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据容许的收集时间,优先回价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划份内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内获能够获取尽量高的收集效率。

可是,G1把内存“化整为零”的思路,理解起来彷佛很容易理解,其中的实现细节却远远没有现象中简单,不然也不会从04年Sun实验室发表第一篇G1的论文拖至今将近8年时间都尚未开发出G1的商用版。笔者举个一个细节为例:把Java堆分为多个Region后,垃圾收集是否就真的能以Region为单位进行了?听起来瓜熟蒂落,再仔细想一想就很容易发现问题所在:Region不多是孤立的。一个对象分配在某个Region中,它并不是只能被本Region中的其余对象引用,而是能够与整个Java堆任意的对象发生引用关系。那在作可达性断定肯定对象是否存活的时候,岂不是还得扫描整个Java堆才能保障准确性?这个问题其实并不是在G1中才有,只是在G1中更加突出了而已。在之前的分代收集中,新生代的规模通常都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象也面临过相同的问题,若是回收新生代时也不得不一样时扫描老年代的话,Minor GC的效率可能降低很多。。

在G1收集器中Region之间的对象引用以及其余收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每一个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操做时,会产生一个Write Barrier暂时中断写操做,检查Reference引用的对象是否处于不一样的Region之中(在分代的例子中就是检查引是否老年代中的对象引用了新生代中的对象),若是是,便经过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,GC根节点的枚举范围中加入Remembered Set便可保证不对全堆扫描也不会有遗漏。

具体实现思路

忽略Remembered Set的维护,G1的运行步骤可简单描述为:

①.初始标记(Initial Marking)
②.并发标记(Concurrenr Marking)
③.最终标记(Final Marking)
④.筛选回收(Live Data Counting And Evacution)

1.初始标记:初始标记仅仅标记GC Roots能直接关联到的对象,而且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中建立新的对象。这阶段须要停顿线程,不可并行执行,可是时间很短。
2.并发标记:此阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,此阶段时间较长可与用户程序并发执行。
3.最终标记:此阶段是为了修正在并发标记期间由于用户线程继续运行而致使标记产生变更的那一份标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段须要把Remembered Set Logs的数据合并到Remembered Set中,这段时间须要停顿线程,可是可并行执行。
4.筛选回收:对各个Region的回收价值和成本进行排序,根据用户指望的GC停顿时间来制定回收计划。

垃圾收集器参数总结

-XX:+<option> 启用选项
-XX:-<option> 不启用选项
-XX:<option>=<number>
-XX:<option>=<string>

clipboard.png

Client、Server模式默认GC

clipboard.png

Sun/Oracle JDK GC组合方式

clipboard.png

总结

表面上看,Java 和 C 比起来,因为内存的动态分配与内存回收技术已经相对成熟,平常的代码中也不怎么须要关注内存的申请与释放。为何咱们还要关注这些问题呢?

笔者认为,一方面越是日常不会关注的东西,在关键的时候越珍贵,由于存在排查各类内存溢出、内存泄漏问题、又或者当垃圾收集称为系统达到更高并发量瓶颈时,对这些“自动化”功能细节的了解,为咱们提供了更广阔的思路。另外一方面,不一样业务场景总有类似的一面,今天借鉴到的实现思想的细节,一直积累下去,或许将来的某天忽然就豁然开朗了。

参考文章

联系做者

zhihu.com
segmentfault.com
oschina.net

相关文章
相关标签/搜索