上篇介绍了一些经典的垃圾收集算法和它们的优缺点 垃圾回收算法看这一篇就够了 今天跟着顾南的脚步,咱们一块儿看一下HotSpot虚拟机中为了实现垃圾收集作了哪些事情,而且了解几个经典垃圾收集器的原理和适用场景,最后咱们学会看gc日志,以及如何编写高质量的代码来优化垃圾收集器行为,话很少说咱们开始;java
垃圾收集器要决定三件事web
因为引用技术的循环引用等一些问题,jvm中都是使用的追踪式的垃圾收集器,那它们是如何判断一个对象是否要回收的呢,答案是依靠根结点枚举和可达性分析,在一些低延迟的垃圾回收器中(好比cms),可达性分析能够与用户线程并发进行,而不用停顿。算法
并发和并行的区别数组
并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线 程在协同工做,一般默认此时用户线程是处于等待状态。 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾 收集器线程与用户线程都在运行。因为用户线程并未被冻结,因此程序仍然能响应服务请求,但因为 垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到必定影响。浏览器
可达性分析算法是从离散数学中的图论引入的,程序把全部的引用关系看做一张图,经过一系列的名为 “GC Roots” 的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连(用图论的话来讲就是从 GC Roots 到这个对象不可达)时,则证实此对象是不可用的,以下图所示。在Java中,可做为 GC Root 的对象包括如下几种:安全
这些根结点可能会随着程序的运行不断的变化,那收集器的根结点枚举这一步骤时都是必须暂停用户线程的,会面临stop th world的问题,在大多数web系统中,终端全部用户线程会给用户带来很差的体验,即便是号称不会停顿的cms,g1,zgc等收集器,在根结点枚举这一步骤,也是须要停顿的。 在HotSpot虚拟机中,使用一组称为OopMap的数据结构来解决这一问题的,在程序运行到全局安全点时,虚拟机能够能够在OopMap的协助下快速的实现先根结点枚举服务器
安全点位置的选取是以是否让程序长时间执行的特征为标准选定的; 例如:方法调用,循环跳转,异常跳转等,只有具备这些功能的指令才会产生安全点;数据结构
如何在垃圾收集时让全部的线程都在最近的安全点停顿下来,有两种方案可选,抢先式终端、主动式中断 抢先式中断不须要线程主动配合,在垃圾收集时,系统吧用户线程所有中断,若是发现用户线程不在安全点上,那么恢复这个线程,让它一会再中断,直到它跑到安全点上。 主动式中断的思想是当垃圾收集须要中断线程的时候,不直接对线程操做,仅仅简单地设置一 个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就本身在最 近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上全部建立对象和其余 须要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。多线程
另外一种中断位置叫作安全区域,若是用户线程处于sleep或者blocked状态,线程没法响应虚拟机中断请求,就不能到安全点挂起本身,这种状况就须要安全区域,在某一段代码片断中,引用关系不会发生变化,所以这这个区域中任何地方开始垃圾收集都是安全的。并发
在年轻代垃圾收集中,有些对象是直接和gcroot关联的,而有些对象是经过老年代间接的和gcroot关联 若是要在一次young gc中也扫描关联了gcroot的老年代才能进行,那么会增大回收器扫描的效率,以下图所示
标记阶段是全部追踪式垃圾收集算法的共同特征,若是能较少这部分的停顿时间,那么收益将会是惠及每一个追踪式垃圾收集器; 引入三色标记来做为辅助,把遍历对象图过程当中遇到的对象,按照“是否访问过”这个条件标记成如下三种颜色:
白色:表示对象还没有被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,全部的对象都是 白色的,若在分析结束的阶段,仍然是白色的对象,即表明不可达。
黑色:表示对象已经被垃圾收集器访问过,且这个对象的全部引用都已经扫描过。黑色的对象代 表已经扫描过,它是安全存活的,若是有其余对象引用指向了黑色对象,无须从新扫描一遍。黑色对 象不可能直接(不通过灰色对象)指向某个白色对象。
灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用尚未被扫描过。
把扫描对象图想象为一股从灰色为波纹从黑色向白色推动的过程,那若是用户线程与收集器并发工做,收集器在对象图上标记颜色,用户线程在运行时修改关系,就会对对象的引用关系形成影响,使得原本要被清除的对象得以存活,这只是形成了“浮动垃圾”,而若是把原本存活的对象清除,那就会形成程序错误了; 所以解决并发扫描时,存活对象被清扫一般使用两种方法;
对象消失主要有两个操做引发
1 赋值器插入了一条或多条从黑色对象到白色对象的新引用; 2 赋值器删除了所有从灰色对象到该白色对象的直接或间接引用。
增量更新是破坏第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新 插入的引用记录下来,等并发扫描结束以后,再将这些记录过的引用关系中的黑色对象为根,从新扫 描一次。这能够简化理解为,黑色对象一旦新插入了指向白色对象的引用以后,它就变回灰色对象 了。
原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删 除的引用记录下来,在并发扫描结束以后,再将这些记录过的引用关系中的灰色对象为根,从新扫描 一次。这也能够简化理解为,不管引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来 进行搜索。
以上的引用关系记录是经过写屏障实现的,cms使用的是增量更新,g1使用原始快照;
查看本身服务器垃圾收集器
java -XX:+PrintCommandLineFlags -version
-XX:+UseParallelGC 也是java8 默认的垃圾收集器 UseParallelGC 即 Parallel Scavenge + Parallel Old
那就先熟悉下这两个垃圾收集器吧
Parallel Scavenge收集器也是一款新生代收集器,它一样是基于标记-复制算法实现的收集器; 它的目标是达到一个可控制的吞吐量,所谓吞吐量就是处理器用户运行用户代码的时间与处理器消耗时间的比值,与cms这种尽量缩短停顿时间的收集器不一样,parallel scavenge在吞吐量的控制上下了更多功夫;
有同窗问我,停顿时间越小,那程序运行时间越长,那吞吐量不就越高吗,那是否是说的是一回事, 其实不是,好比 程序运行2s 垃圾回收停顿1s,另外一种运行10s 停顿2s,那么第一种停顿时间是1s小于第二种,但吞吐量要比第二种小的多
-XX:M axGCPauseM illis参数容许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的 时间不超过用户设定值。不过你们不要异想天开地认为若是把这个参数的值设置得更小一点就能使得 系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的: 系统把新生代调得小一些,收集300MB新生代确定比收集500MB快,但这也直接致使垃圾收集发生得 更频繁,原来10秒收集一次、每次停顿100毫秒,如今变成5秒收集一次、每次停顿70毫秒。停顿时间 的确在降低,但吞吐量也降下来了。
-XX:GCTimeRat io参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的 比率,至关于吞吐量的倒数。譬如把此参数设置为19,那容许的最大垃圾收集时间就占总时间的5% (即1/(1+19)),默认值为99,即容许最大1%(即1/(1+99))的垃圾收集时间。
除上述两个参数以外,ParallelScavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得咱们关注。这是一个开关参数,当这个参数被激活以后,就不须要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GCErgonomics)
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法,
咱们服务器使用以上两种吞吐量优先的服务器看出咱们更看重服务的吞吐量,容许程序的更长时间停顿,顺便说一句,若是程序的请求量更高,那么长时间的停顿可能会在没有自适应负载均衡系统的服务中形成高停顿,若是没作好限流的话可能会形成整个微服务的服务雪崩;
下面介绍一个以低延迟低停顿为主的垃圾收集器cms
大名鼎鼎的CMS(Concurrent Mark Sweep),是一种以获取最短挺短期为牧鞭的的收集器,大部分java应用集中在基于浏览器的B/S系统的服务器上,这类应用一般会较为关注服务响应速度,但愿停顿时间更短,以给用户带来良好的体验,cms基于标记清除的老年代回收器,主要分为四个步骤:
1)初始标记(CMS initial mark)
2)并发标记(CM S concurrent mark)
3)从新标记(CM S remark)
4)并发清除(CM S concurrent sweep)
其中须要停顿的是第一步初始标记和第三部从新标记,初始标记是标记gc roots能直接关联到的对象,速度很快 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对 象图的过程,这个过程耗时较长可是不须要停顿用户线程,能够与垃圾收集线程一块儿并发运行; 而从新标记阶段则是为了修正并发标记期间,因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,前边有讲过cms是采用增量更新的方式处理的,这段的停顿时间稍长;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的 对象,因为不须要移动存活对象,因此这个阶段也是能够与用户线程同时并发的。
面向并发设计的程序都对处理器资源比较敏 感。在并发阶段,它虽然不会致使用户线程停顿,但却会由于占用了一部分线程(或者说处理器的计 算能力)而致使应用程序变慢,下降总吞吐量;
CMS没法处理浮动垃圾,有可能出现并发失败形成一次 stop the world的full fc,由于标记清理过程当中用户线程和垃圾回收线程并发运行,程序运行会产生新的垃圾对象,但这部分对象是出如今标记过程以后的,CMS没法在本次收集中清理掉,就要留到下一次收集,这一部分就是浮动垃圾,因此CMS不能等到老年代满了才开始收集,必须预留一部分空间给程序运行使用,java5默认是68%,java6的默认数值提高到了92%,那若是预留的空间仍是没法知足程序运行时新对象分配,那么会出现一次并发失败,虚拟机会冻结用户线程,临时启用 serial old收集器(一个单线程的老年代垃圾收集器)来收集垃圾,这样的停顿时间就更长了,因此参数 -XX:CMSInitiatingOccupancyFraction设置得过高将会很容易致使 大量的并发失败产生,性能反而下降,用户应在生产环境中根据实际应用状况来权衡设置。
还有一个重要的关注点,CMS是一款基于标记-清除算法实现的垃圾收集器,这种算法会产生不少空间碎片,将会给大对象分配带来麻烦,会出现老年代还有不少空间,可是没法找到连续的足够大空间来分配对象不得不进行一次full gc,CMS收集器提供了一个参数,+UseCMS-CompactAtFullCollection开关参数,用于在CMS收集器不得不进行FullGC时开启内存碎片的合并整理过程,因为这个内存整理必须移动存活对象,是没法并发的。这样空间碎片问题是解决了,但停顿时间又会变长,所以虚拟机设计者们还提供了另一个参数-XX:CMSFullGCsBefore-Compaction(此参数从JDK9开始废弃),这个参数的做用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的FullGC以后,下一次进入FullGC前会先进行碎片整理(默认值为0,表示每次进入FullGC时都进行碎片整理)。
G1是一款主要面向服务端应用的垃圾收集器。HotSpot开发团队最初赋予它的指望是(在比较长 期的)将来能够替换掉JDK 5中发布的CMS收集器。如今这个指望目标已经实现过半了,JDK 9发布之 日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则 沦落至被声明为不推荐使用(Deprecate)的收集器[1]。若是对JDK 9及以上版本的HotSpot虚拟机使用 参数-XX:+UseConcMarkSweep GC来开启CM S收集器的话,用户会收到一个警告信息,提示CM S未 来将会被废弃
G1,能够面向全堆的任何组成部分回收,衡量标准再也不是对象属于哪一个分代,而是哪块内存中存放的垃圾数量最多,回收效益最大,虽然它也是遵循分代收集理论,可是堆内存布局与其余收集器有很是明显的差别,G1再也不坚持固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region能够根据须要扮演新生代的Eden空间、Survivor空间,或者老年代空间,G1根据扮演不一样角色的Region采用不一样的策略去处理; Humongous区域是一种特殊的Region,专用来存储大对象,只要超过一个Region容量的一半就断定为大对象。
G1会去跟踪每一个Region里边垃圾回收的价值大小,价值就是回收所得到的空间大小以及回收所须要时间的经验值,会在后台维护一个优先级列表,根据用户设定容许的收集停顿时间,优先处理收益最大的Region,使用记忆集来避免跨Region的扫描,G1维护一个哈希表做为记忆集,key是别的Region其实地址,value是个集合,存储元素是卡表的索引号,因为Region的数量比传统收集器的分代数量明显要多得多,所以G1收集器要比其余的传统垃 圾收集器有着更高的内存占用负担。
初始标记(Initial M arking):仅仅只是标记一下GC Roots能直接关联到的对象,而且修改TAM S 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段须要 停顿线程,但耗时很短,并且是借用进行Minor GC的时候同步完成的,因此G1收集器在这个阶段实际 并无额外的停顿。
并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要从新处理SAT B记录下的在并发时有引用变更的对象。
最终标记(Final M arking):对用户线程作另外一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少许的SAT B记录。
筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所指望的停顿时间来制定回收计划,能够自由选择任意多个Region 构成回收集,而后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的所有空间。这里的操做涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。
目前在小内存应用上CMS的表现大几率仍然要会优于G1,而在大内存应用上G1则大多能发挥其 优点,这个优劣势的Java堆容量平衡点一般在6GB至8GB之间,固然,以上这些也仅是经验之谈,不 同应用须要量体裁衣地实际测试才能得出最合适的结论,随着HotSpot的开发者对G1的不断优化,也 会让对比结果继续向G1倾斜。
ZGC目标是在尽量对吞吐量影响不太大的前提下,实如今任意堆内存大小下均可以把垃圾收集的停顿时间限制在十毫秒之内的低延迟; ZGC收集器是一款基于Region内存布局的,(暂时) 不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低 延迟为首要目标的一款垃圾收集器; 染色指针是一种直接将少许额外的信息存储在指针上的技术,染色指针可使得一旦某个Region的存活对象被移走以后,这个Region当即就可以被释放和重用掉,而没必要等待整个堆中全部指向该Region的引用都被修正后才能清理;
ZGC使用了多重映射(Multi-Mapping)将多个不一样的虚拟内存地址映射到同一 个物理内存地址上,这是一种多对一映射,意味着ZGC在虚拟内存中看到的地址空间要比实际的堆内 存容量来得更大。把染色指针中的标志位看做是地址的分段符,那只要将这些不一样的地址段都映射到 同一个物理内存空间,通过多重映射转换后,就可使用染色指针正常进行寻址了
并发标记(ConcurrentMark):与G一、Shenandoah同样,并发标记是遍历对象图作可达性分析的阶段,先后也要通过相似于G一、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的短暂停顿,并且这些停顿阶段所作的事情在目标上也是相相似的。与G一、Shenandoah不一样的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked0、Marked1标志位。
并发预备重分配(ConcurrentPrepareforRelocate):这个阶段须要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(RelocationSet)。重分配集与G1收集器的回收集(CollectionSet)仍是有区别的,ZGC划分Region的目的并不是为了像G1那样作收益优先的增量回收。相反,ZGC每次回收都会扫描全部的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。所以,ZGC的重分配集只是决定了里面的存活对象会被从新复制到其余的Region中,里面的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,由于标记过程是针对全堆的。此外,在JDK12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。
并发重分配(ConcurrentRelocate):重分配是ZGC执行过程当中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每一个Region维护一个转发表(ForwardTable),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,若是用户线程此时并发访问了位于重分配集中的对象,此次访问将会被预置的内存屏障所截获,而后当即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。这样作的好处是只有第一次访问旧对象会陷入转发,也就是只慢一次,对比Shenandoah的Brooks转发指针,那是每次对象访问都必须付出的固定开销,简单地说就是每次都慢,所以ZGC对用户程序的运行时负载要比Shenandoah来得更低一些。还有另一个直接的好处是因为染色指针的存在,一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就能够当即释放用于新对象的分配(可是转发表还得留着不能释放掉),哪怕堆中还有不少指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用,它们都是能够自愈的。
并发重映射(ConcurrentRemap):重映射所作的就是修正整个堆中指向重分配集中旧对象的全部引用,这一点从目标角度看是与Shenandoah并发引用更新阶段同样的,可是ZGC的并发重映射并非一个必需要“迫切”去完成的任务,由于前面说过,即便是旧引用,它也是能够自愈的,最多只是第一次使用时多一次转发和修正操做。重映射清理这些旧引用的主要目的是为了避免变慢(还有清理结束后能够释放转发表这样的附带收益),因此说这并非很“迫切”。所以,ZGC很巧妙地把并发重映射阶段要作的工做,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所 有对象的,这样合并就节省了一次遍历对象图[9]的开销。一旦全部指针都被修正以后,原来记录新旧对象关系的转发表就能够释放掉了。
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的先后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径
尝试打出的gc日志
[GC (System.gc()) [PSYoungGen: 5022K->1152K(38400K)] 5022K->1160K(125952K), 0.0019083 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 1152K->0K(38400K)] [ParOldGen: 8K->1034K(87552K)] 1160K->1034K(125952K), [Metaspace: 3139K->3139K(1056768K)], 0.0045528 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap PSYoungGen total 38400K, used 333K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000) eden space 33280K, 1% used [0x0000000795580000,0x00000007955d34a8,0x0000000797600000) from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000) to space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000) ParOldGen total 87552K, used 1034K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000) object space 87552K, 1% used [0x0000000740000000,0x0000000740102bb0,0x0000000745580000) Metaspace used 3147K, capacity 4564K, committed 4864K, reserved 1056768K class space used 340K, capacity 388K, committed 512K, reserved 1048576K
![]()
个人服务器20秒左右一次young gc,没有full gc,说明运行情况良好 若是频繁full gc,会引发频繁停顿,如下状况会致使fullgc
一、System.gc()方法的调用 二、老年代空间不足 三、永生区空间不足 四、CMS GC时出现concurrent mode failure 五、堆中分配很大的对象
jps查处进程id为5280
jmap -dump:format=b,file=temp.dump 5280 把文件dump出来,再经过jvisualvm分析对象的引用链的方式来定位具体频繁建立对象的地方。
今天学习了追踪式垃圾收集的过程,包括根结点枚举,并发可达性分析,三色标记,跨代(Region)引用的卡表,写屏障,以后又熟悉了几个java中经典的垃圾收集器,分析了各个经典垃圾收集器的优缺点和使用场景,今天,大多数互联网公司都在用java8,等下一个java11的时代来临,咱们能够大范围的使用java11的zgc,相信这一天就快到来了,最后咱们学习了查看gc日志的方式,以及简单讲解了如何排查频繁full gc,那今天就到这了,喜欢的朋友一键三连把,手动狗头。
预先善其事必先利其器,下期聊下jmap相似的java自带的排查问题工具