我是靠谱客的博主 迷你战斗机,最近开发中收集的这篇文章主要介绍胡说八道JVM—垃圾回收算法和垃圾回收器垃圾回收算法分代垃圾回收在虚拟机上的实现垃圾回收器(算法的实现)日志,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

垃圾回收算法

引用计数器法(Reference Counting)

可达性分析

标记清除算法(Mark-Sweep)

  • 这个算法的原理很简单,但是它却是其他算法的基础,后续的其他算法否是在这个算法的基础上,针对它的不足,进行改造。

标记阶段

  • 每一个可以从根对象访问到的对象都会被添加一个标识,于是这个对象就被标识为可到达对象——存活对象,将来垃圾回收的时候不会被回收。
在深入了解JVM最佳实践第二版中,是写的标记需要回收的对象,但是这里我认为标记的是可达的对象,因为可达性分析无法找到不可达的对象,所以没有被标记的就是我们要回收的对象。

清除阶段

  • 垃圾回收器,会对堆内存从头到尾进行线性遍历,如果发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作

优缺点

优点

  • 实现简单,适用于存活对象比较多的情况(老年代)。

缺点

两边扫描效率偏低
回收之后内存碎片化,必须使用空闲列表的方式去分配内存
  • 效率问题:标记和清除过程的效率都不高(主要体现在清除阶段需要一个一个的遍历整个堆空间的对象,让后清除掉垃圾对象);而且需要两次遍历,第一次遍历标记存活对象,第二次遍历清除死亡对象
  • 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至Stop The World。

复制算法(Coping)

  • 为了解决标记清除算法的空间问题和效率问题,复制算法将内存分为两块,每次只使用其中一块内存,当这块内存使用完了,就将这块内存上的对象,全部复制到另一块上,然后把当前这一块内存一次性回收清理,这样每次回收都是对整个半区进行操作,既解决了效率问题也解决了内存碎片
  • 效率体现在一次回收整个内存区域的一半,而不是每次一块一块的回收;空间问题体现在,一次回收之后,只少有一半的内存是规整的,可以分配给打对象;并且由于内存是规整的,分配内存的时候也更加的高效,可以采取"指针碰撞"的方法进行分配;
  • 由于是复制过去的,所以复制的时候我们也可以对复制过来的对象内存分配进行优化,使其变得规整。
当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。

复制阶段

清除阶段

优缺点

优点

  • 解决了标记清除算法的效率问题和内存碎片问题,并且只需要扫描一次(直接复制存活对象),而且回收的时候一次回收一半的内存空间,非常高效。
  • 适用于对象比较多但是存活对象比较少的情况(新生代)

缺点

  • 浪费内存,复制算法要求整块内存只能使用一半,另外一半留作备用。后面有针对这个问题的优化。
  • 在存活对象比较多(对象存活率比较高)的时候,就要进行较多的复制操作,效率也会变低。
  • 移动对象需要调整对象的引用(Java 采用的是指针的方式进行对象定位的,所以要更新对象的引用,但是这种方式比起句柄更加高效)

标记整理算法(Mark-Compact)

  • 针对复制算法,在对想存活率比较高的情况下,效率会变低的问题进行优化
  • 标记整理算法和标记清除算法的标记阶段一样,但是后续阶段不一样,不是直接清除,而是将存活的对象移动到内存区域的一端,然后从边界开始清理,清理掉剩余的内存部分。
标记-整理算法不仅可以弥补标记-清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价

优缺点

缺点

  • 标记-整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记-整理算法要低于复制算法

优点

  • 回收后的内存空间没有碎片。
  • 无内存折半
  • 应用:老年代回收

总结

垃圾回收算法内存是否规整内存分配方式优势不足适用场景
Mark-Swap(标记清除)空闲列表实现简单内存碎片,效率较低存活时间比较长
Copy(复制算法)指针碰撞效率较高、内存规整空间浪费存活时间比较短,存活对象比较少
Mark-Compact(标记压缩)指针碰撞内存规整,内存利用率高效率较低存活时间比较长

分代垃圾回收(Generational Collecting )

弱代理论

分代垃圾收集基于弱代理论。具体描述如下:

  • 大多说分配了内存的对象并不会存活太长时间,在处于年轻时代就会死掉。
  • 很少有对象会从老年代变成年轻代。
  • 新生代98%的对象都是"朝生夕死";所以并不需要按1:1比例来划分内存(解决了标记复制算法的一半的内存浪费)
弱代理论,是基于IBM公司的一项研究结果上提出的。所以在弱代理论的支持下,Hotspot虚拟机优改进了复制算法的不足——浪费一半的内存问题

意义

  • 首先就是分代回收可以对堆中对象采用不同的gc策略。在实际程序中,对象的生命周期有长有短。进行分代垃圾回收,能针对这个特点做很好的优化
  • 分代以后,gc时进行可达性分析的范围能大大降低。在分代回收中,新生代的规模比老年代小,回收频率也要高,显然新生代gc的时候不能去遍历老年代。

Hotspot虚拟机新生代内存布局及算法

上面的复制算法中,我们看到复制算法存在连个问题,第一个问题 是内存浪费的问题,在弱代理论的支持下,在一定程度上降低了内存的浪费,
第二个问题是复制算法在,对象存活率比较高的情况下,会降低效率——大量的复制。所以复制算法仅仅用在了新生代上。
  • 新生代内存分配一块较大的Eden空间和两块较小的Survivor空间
  • 每次使用Eden和其中一块Survivor空间
  • 回收时将Eden和Survivor空间中存活的对象一次性复制到另一块Survivor空间上
  • 最后清理掉Eden和使用过的Survivor空间。
Hotspot虚拟机默认Eden和Survivor的大小比例是8:1

分配担保机制(ndle Promotion)

  • 在弱代理论的支持下,我们将复制算法用在了新生代上,在存活率比较低的情况下,通过复制少量存活对象和调节复制算法的内存分配比例,很好的解决了复制算法存在的两个问题,但是又引入了一个新的问题。
  • 如果另一块Survivor空间没有足够内存来存放上一次新生代收集下来的存活对象,那么这些对象则直接通过担保机制进入老年代——这个机制就是内存担保机制。
  • 这是因为我们改变了复制算法的内存分配比例,默认在1:1 的情况下就不存在这个问题,复制过来的对象肯定在预留的一半内存放的下,但是当我们改变了这个比例之后,我们假设98%的对象都会在垃圾回收的时候死掉,但是这个一个概率问题、并且只是适用与一般情况,我们无法保证每次回收之后都只有不少于10% 的对象存活,所以我们需要分配担保——担保这些对象可以存的下(其实这就是为什么你给固定的堆内存,jvm为什么分配给老年代比年轻代多的原因之一 )

  • 在默认情况下,年轻代与老年代的比例是1:2 这里给的总共内存是30M,年轻代10M,老年代20M,你看到年轻代是9M,是因为10M 的内存之,只有90%是可以用的,也就是 一个Eden加一个 Survivor。

分代垃圾回收算法

  • 分代垃圾回收算法其实并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块,这样就可以根据各个年代的特点采用最适当的收集算法;
  • 一般把Java堆分为新生代和老年代

新生代复制算法

  • 每次垃圾收集都有大批对象死去,只有少量存活——所以可采用复制算法,只要付出少量的复制成本就可以完成垃圾回收,但是依然得有分配担保机制的存在。

老年代整理或者清除

  • 对象存活率高,没有额外的空间可以分配担保,使用标记-清理或标记-整理算法;

对象在分代垃圾回收算法下的行为

对象年龄的增长

  • 虚拟机采用分代收集的思想来管理内存,内存回收时必须识别哪些对象放入新生代,哪些对象放入老年代。为了做到这点,虚拟机为每个对象定义了一个对象年龄计数器
  • 如果对象在Eden出生并经过一次Minor GC仍然存活,并且能被Survivor容纳,将被移动到Survivor区,并且对象年龄设置为1.对象每经过一次Minor GC后仍保持存活,年龄+1
    • 如果不能被Survivor容纳,根据分配担保机制,则直接升级到老年代,如果老年代不能容纳,则触发 full-gc
  • 当对象年龄到达一定程度(一般15岁),那么它会在下一次垃圾回收的时候会晋升到老年代,这个时候不论Survivor 区间是否足够。对象晋升的年龄限制 -XX:MaxTenuringThreshold设定

对象的晋升细节

对象年龄的动态判定

  • 为了更好的适应不同程度的内存状况,虚拟机并不是永远地要求对象的年龄必须到达MaxTenuringThreshold才能晋升进入老年代,当Survivor中相同年龄所有对象大小总和大于Survivor空间一半,年龄大于该年龄的对象直接进入老年代

空间的担保分配

  • 其实前面提到过,为什么要有空间分配担保机制,因为默认的复制算法存在50% 的内存浪费,为了解决这个问题虚拟机将内存空间不是按照对半空间进行划分的,在弱代理论的支持下,进行了8:1:1 的划分,至于为什么是8:1:1而不是 9:1 或者 8:2 是为了完成年龄的判定。
  • 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有空间总和,如果条件程离,那么Minor GC是安全的,如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行Minor GC 否则可能进行一次Full GC
  • 新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间作为轮换备份,因此当出现大量对象在Minor GC仍然存活的情况(最极端情况为内存回收后新生代所有对象都存活),就需要老年代进行担保,把Survivor无法容纳的对象存入老年代。但老年代需要足够空间,所以需要进行判断,当不足时 进行Full GC腾出老年代空间

minor gc 和 full gc

  • 不同的垃圾回收器关于这个两个概念不太一样,不一样主要体现在老年代上
  • 不论是怎样的gc,导致发生的原因就是堆空间不足,无法容纳要分配或者要移动的对象

minor gc

  • serial、parallel、cms、G1 因为年轻代内存空间不足引起的垃圾回收行为叫做minor gc
minor gc 执行的时机
  • 当JVM 无法为一个新生的对象分配内存的时候,就会触发minor gc ,所以分配率越高,内存越少,执行minor gc 就越频繁

major gc

  • 清理老年代,当eden 区内存不足的时候触发
触发时机

full gc

  • serial、parallel 因为老年代内存不足就叫full gc
  • cms 老年代垃圾并发回收失败之后,退化为串行gc 之后叫做full gc否则就是并发收集(观察日志看到full gc 相关信息)
  • g1 老年代中
触发时机
  • 老年代内存不足的时候

卡表(Card Table)

  • 一个比特位集合,每一个比特位表示老年代的某一区域中的对象是否持有新生代的引用。新生代GC时,根据卡表扫描老年代对象,而避免扫描所有老年代对象。下图,每一位表示老年代4KB的空间。

分区算法(Region)

  • 将堆划分成连续的不同小区间,每个小区间都独立使用、独立回收
  • 可控制每次回收的小区间个数,避免回收整个堆,减少GC停顿时间。

分代垃圾回收在虚拟机上的实现

  • JVM进行垃圾回收是一个非常复杂的过程,如何进行垃圾标记、什么时候进行垃圾、如何进行垃圾回收等等都非常复杂,当前主流测JVM在垃圾回收时都会进行STW(stop the world),即使宣称非常快的CMS垃圾回收期早期也会STW标记垃圾状态。

枚举 GC ROORTS

  • 枚举出这个GC Roots我们需要考虑到这个分析过程所产生结果的准确性及枚举效率

STW

  • 可达性分析的过程,需要在一个能保证一致性的快照中进行,这里的一致性是指在整个可达性分析的过程中这个系统像是被冻结了,引用和对象的关系不在发生变化,如果不能满足该条件,就不能保证分析结果的正确性,导致有引用的对象被垃圾回收。这一点也是GC进行时必须停顿所有执行线程的一个重要原因
  • 其实这个就像是,你妈妈在打扫房间的时候,你还在向地上扔垃圾一样,这种情况下,房间就永远打扫不完了。
  • 多线程下的漏报和误报是导致STW的根本原因(还有一个原因就是考虑到效率)

关于STW

  • GC 停顿会拖慢应用程序,在外界看来,它就像冻住了一样。在 GC 停顿期间发给服务器的请求会更晚收到响应,根据停顿时间的不同(传统的 GC 停顿有可能达到几十秒),客户端有可能会出现超时。如果客户端进行重试,服务器端就会有更多待处理的请求,这个时候需要使用断路器。
  • 长时间的 GC 停顿也可能造成服务的健康检测失效,并导致服务被重启。而在一个服务重启期间,其他服务需要承担更多的负载,它们所经历的停顿会更长,这就像是一个恶性循环。
  • 不可预测的 GC 停顿给系统带来的影响远远超过了应用程序本身。客户端出现回压,请求队列溢出,监控控制台满是各种超时异常,运维人员忙得团团转。对于一个可以应对各种情况的系统来说,需要在 CPU 时间、队列长度、可接受的响应时间方面具备缓冲能力。

多线程下的漏报和误报

  • 如果没有STW 提供的一致性快照环境——对象和引用的关系不再发生变化,将会出现误报和漏报
  • 我们知道可达性分析标记的是对象的引用(详情可见上一章),如果被标记则说明被标记引用的对象是有用的,不能被回收,否则则是垃圾,应该被回收。
  • 标记的漏报和误报针对的是标记出来的引用而言,所以误报就是将本该被回收的对象标记了下来,漏报就是将不该被回收的对象没有标记。
误报
  • 在标记的时候,由于需要标记的对象比较多,需要一定的时间,假设这个时候已经标记了一半了,还在标记剩余的一半,这个时候有一个线程将自己的一个引用置为null了,并且这个引用是在已经标记完成的那一半中的,这个时候就会出现误报——本该是垃圾被回收的,现在标记成了存活。

漏报
  • 其实就是标记已经结束了,这个时候其他线程创建了对象,但是这个对象的引用没有被标记,然后在垃圾回收的时候,这个对象就被回收掉了。

准确式GC

  • Java虚拟机是利用可达性算法判断对象是否需要回收的,由于在GC进行时,必须暂停所有的Java执行线程(Sun称之为“Stop The World”),所以,虚拟机必须尽量的优化GC过程的效率,减少暂停的时间。
  • GC ROOTS 主要存在全局性引用和执行上下文,现在很多应用程序的方法区都很大动辄几百兆,当执行系统停顿下来后,我们不需要一个不漏地检查完所有执行上下文和全局的引用位置,因为在这种情况下枚举完整个GC ROOTS 势必浪费很多时间,并且在枚举的过程中还要求STW

准确式GC的定义

  • HotSpot采用了准确式GC以提升GC roots的枚举速度。所谓准确式GC,就是让JVM知道内存中某位置数据的类型什么。比如当前内存位置中的数据究竟是一个整型变量还是一个引用类型。这样JVM可以很快确定所有引用类型的位置,从而更有针对性的进行GC roots枚举。
  • 在HotSpot的实现中,是使用一组称为OOPMap的数据结构来达到这个目的的,当类加载完成后,HotSpot就将对象内存布局之中什么偏移量上数值是一个什么样的类型的数据这些信息存放到 OopMap中;在 HotSpot 的 JIT 编译过程中,同样会插入相关指令来标明哪些位置存放的是对象引用等
  • 这样,GC在扫描的时候,就可以根据OOPMap上记录的信息准确定位到哪个区域中有对象的引用,这样大大减少了通过逐个遍历来找出对象引用的时间消耗,从而快速的完成 GC Roots 的枚举。

安全点

  • 在Oopmap的帮助下,HotSpot虚拟机可以准确快速的完成GCROORTS的枚举,但是一个很现实的问题随之而来,随着程序的运行与垃圾回收和内存分配,引用关系在发生变化,要想准确的知道引用和对象类型就必须为每一条指令都生成一个OopMap,如果为每条指令都插入oopmap,那么将会产生大量的内存损耗。

  • 为了准确安全地回收内存,JVM是在SafePoint点时才进行回收,所谓SafePoint就是Java线程执行到某个位置这时候JVM能够安全、可控的回收对象,这样就不会导致上所说的回收正在使用的对象。

  • JVM使用的是主动性主动到达安全点,那么应该在什么地方设置全局变量呢?显然不能随意设置全局变量,进入安全点有个默认策略那就是:“避免程序长时间运行而不进入Safe Point”

安全点的选取原则

  • 程序在执行时,并非在所有的地方都能停下来开始GC,只有到达这个“安全点“时才能停顿下来。安全点的选区既不能太少以至于让GC等待时间过长,也不能过于频繁以至于过分增大运行时的负荷。
  • 安全点“的选择基本上是以程序”是否具有让程序长时间执行的特征“为标准来选定的。因为每条执行指令执行的时间都非常地短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,而程序”长时间的运行“实际上就是指令序列的一个复用。例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生”安全点“

安全点的位置

从线程角度看,safepoint可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停,
比如发生GC时,需要暂停所有活动线程,但是该线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,然后才开始GC,该线程等待GC结束。
  • 循环的末尾
  • 方法返回前
  • 调用方法的call之后
  • 抛出异常的位置

安全区域

加入垃圾回收队列

直接回收

对象自救

  • 重写了finalize()方法的对象,如果finalize()没有被执行,在加入了垃圾队列后执行该方法将不会被回收

垃圾回收器(算法的实现)

  • serial收集器、parnew收集器、parallel scavenge收集器、serial old 收集器、parallel old收集器、cms收集器、g1收集器
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1

相关概念

  • 并行收集(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
  • 并发收集(concurrent):指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
  • 吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

垃圾回收器

  • 如果说收集算法是内存回收的方法论,那么垃圾收集器就是方法论的实现
  • 虚拟机规范并没对垃圾回收器的实现做任何规定,因此不同的厂商的实现、不同版本之间的垃圾回收器可能存在很大差别。
  • 不同的垃圾回收器有不同的针对场景,没有一个放之四海而皆准的的收集器的存在,所以我们要根据应用的特点选一个最合适的组合(其实也不一定是组合)。
  • 垃圾收集器的发展,都是朝着增大吞吐量和减少停顿时间的方向发展。

Serial

  • 它是一个单线程垃圾回收器,也会要求STW
  • 这里的单线程的意义
    • 只会使用一个线程去完成GC(后面会减少使用多个线程区进行垃圾回收的垃圾回收器)
    • 在它工作时,必须暂停其他所有的工作线程
  • 该收集器也存在的原因是因为简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,没有线程交互的开销,专心做GC,自然可以获得最高的单线程效率。
  • Serial收集器对于运行在client模式下的应用是一个很好的选择(到目前为止,它依然是虚拟机运行在client模式下的默认新生代收集器)
  • 在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的

ParNew

  • ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
  • 它是许多运行在Server模式下的虚拟机的首要选择,主要是因为除了Serial收集器外,目前只有它能与CMS收集器配合工作。这就是为什么有了Parallel Scavenge,还要有 ParNew 的原因
CMS收集器是一个被认为具有划时代意义的并发的老年代垃圾收集器,因此如果有一个垃圾收集器能和它一起搭配使用让其更加完美,那这个收集器必然也是一个不可或缺的部分了。
ParNew不能与CMS收集器配合因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;

Parallel Scavenge

  • Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
  • Parallel Scavenge收集器关注点是吞吐量(如何高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
  • 所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。(吞吐量:CPU用于用户代码的时间/CPU总消耗时间的比值,即=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)。比如,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。)
  • Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,不进行手工优化,可以选择把内存管理优化交给虚拟机去完成,用户只要提供一个优化目标,让虚拟机自己去朝这个目标进行优化。

适用场景

  • 高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;
  • 当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;
  • 例如,那些执行批量处理、订单处理(对账等)、工资支付、科学计算的应用程序;

参数设置

  • -XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间
  • 设置垃圾收集时间占总时间的比率 -XX:GCTimeRatio
  • GC自适应的调节策略 -XX:+UseAdptiveSizePolicy

适用推荐

开启这个参数后,就不用手工指定一些细节参数,如:

  • 新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;
  • JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics);
(1)、只需设置好内存数据大小(如"-Xmx"设置最大堆);
(2)、然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标;
(3)、那些具体细节参数的调节就由JVM自适应完成;;

Serial Old

  • Serial收集器的老年代版本,它同样是一个单线程收集器。
  • 它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案
  • 采用"标记-整理-压缩"算法(Mark-Sweep-Compact);

应用场景

  1. 主要用于Client模式;
  2. 而在Server模式有两大用途:
    • 在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配Parallel Scavenge收集器);
    • 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用;

Parallel Old收集器

  • Parallel Scavenge收集器的老年代版本。
  • 使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
    在JDK1.6才有的。

应用场景

  • JDK1.6及之后用来代替老年代的Serial Old收集器;
  • 特别是在Server模式,多CPU的情况下;这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge(新生代)加Parallel Old(老年代)收集器的"给力"应用组合;

CMS

  • 跨时代的产品,特别长的STW,现在只有初始标记的STW
  • 适用于在线服务场景的情况
  • 在cms 之后的垃圾回收器主要在并发标记阶段发力,缩短这个阶段的运行时间
  • 扩展阅读,三色标记算法
  • cms 存在的问题比较多,所以不是虚拟机的默认垃圾回收器,需要手动指定
  • 在1.7 或者 1.8 的时候G1 还不是很完善

标记阶段

初始标记
  • 直接标记GC Roots 直接可达的存活对象
并发标记
  • 从初始标记存活的对象,进行并发标记,这个阶段是最费时间的(这个时候程序正在运行,就有对象和引用的变化就是前面说的漏报和误报,这个主要是漏报,就是垃圾变成非垃圾)
重新标记
  • 由于并发标记运行阶段的对象和引用关系的变化,所以重新标记阶段就是对并发标记阶段的变化进行处理
  • 这个阶段是需要一个比较准确的标记结果的,所以是STW 的
并发清理
  • 并发清理阶段产生的垃圾叫做浮动垃圾

缺点

  • 1.cms堆cpu特别敏感,cms运行线程和应用程序并发执行需要多核cpu,如果cpu核数多的话可以发挥它并发执行的优势,但是cms默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候比如说为为2核,如果这个时候cpu运算压力比较大,还要分一半给cms运作,这可能会很大程度的影响到计算机性能。
  • cms无法处理浮动垃圾,可能导致Concurrent Mode Failure(并发模式故障)而触发full GC
  • .由于cms是采用"标记-清除“算法,因此就会存在垃圾碎片的问题,为了解决这个问题cms提供了
浮动垃圾:由于cms支持运行的时候用户线程也在运行,程序运行的时候会产生新的垃圾,这里产生的垃圾就是浮动垃圾,cms无法当次处理,得等下次才可以。
  • -XX:+UseCMSCompactAtFullCollection选项,这个选项相当于一个开关【默认开启】,用于CMS顶不住要进行full GC时开启内存碎片合并,内存整理的过程是无法并发的,且开启这个选项会影响性能(比如停顿时间变长)

G1

  • G1 成为 Java 9 的默认垃圾回收器
  • G1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。

G1 的特性

  • 并行性:G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力。
  • 并发性:G1拥有与应用程序交替执行的能力,因此一般来说,不会在整个回收期间完全阻塞应用程序。
  • 分代GC:与之前回收器不同,其他回收器,它们要么工作在年轻代要么工作在老年代。G1可以同时兼顾年轻代与老年代。
  • 空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS,只是简单的标记清除,在若干次GC后CMS必须进行一次碎片整理,G1在每次回收时都会有效的复制对象,减少空间碎片

G1 垃圾回收的阶段

初始标记

  • 标记从根节点直接可达的对象。这个阶段会伴随一次新生代GC,它是会产生全局停顿的,应用程序在这个阶段必须停止执行。

根区域扫描

  • 由于初始标记必然会伴随一次新生代GC,所以在初始化标记后,eden被清空,并且存活对象被移到survivor区。在这个阶段,将扫描由survivor区直接可达的老年代区域,并标记这些直接可达的对象。
  • 这个过程是可以和应用程序并发执行的。但是根区域扫描不能和新生代GC同时发生(因为根区域扫描依赖survivor区的对象,而新生代GC会修改这个区域),故如果恰巧此时需要新生代GC,GC就需要等待根区域扫描结束后才能进行,如果发生这种情况,这次新生代GC的时间就会延长

并发标记

  • 和CMS类似,并发标记将会扫描并查找整个堆的存活对象,并做好标记。这是一个并发过程,并且这个过程可以被一次新生代GC打断。

重新标记

  • 和CMS一样,重新标记也是会使应用程序停顿,由于在并发标记过程中,应用程序依然运行,因此标记结果可能需要修正,所以在此阶段对上一次标记进行补充。
  • 在G1中,这个过程使用SATB(Snapshot-At-The-Begining)算法完成,即G1会在标记之初为存活对象创建一个快照,这个快照有助于加速重新标记的速度。
全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过RootTracing得到的,作用是维持并发GC的正确性。
  • G1 Remark阶段 Stop The World 与 CMS了的remark有一个本质上的区别,那就是这个暂停只需要扫描有 write barrier 所追中对象为根的对象, 而 CMS 的remark 需要重新扫描整个根集合,因而CMS remark有可能会非常慢

独占清理

  • 这个阶段会引起停顿。它将计算各个区域的存活对象和GC回收比例并进行排序,识别可供混合回收的区域。在这个阶段,还会更新记忆集。该阶段给出了需要被混合回收的区域并进行了标记,在混合回收阶段,需要这些信息。

并发清理阶段

  • 识别并清理完全空闲的区域。它是并发的清理,不会引起停顿

ZGC

Shenandoah

  • Shenandoah GC 是最新的 JVM 垃圾回收器,由 Red Hat 的一个团队负责开发。垃圾回收器的并发性是指在应用程序运行的同时进行垃圾回收,而这就是 Shenandoah 的目标——最小化垃圾回收对用户代码造成的停顿。Shenandoah 的另一个设计目标是可以处理大堆和小堆。

经典GC

  • 经典 GC(也叫作 STW,Stop-The-World)会在没有可用内存时暂停应用程序线程,回收垃圾,并压缩存活的对象,然后让应用程序继续执行。这种停顿有可能长达几十秒,而且会随着堆的增大而延长。

Shenandoah GC

  • Shenandoah GC 也会造成 STW 停顿,但通常都很短暂,因为它是在应用程序运行的同时执行大量的 GC。这种停顿不会随着堆的增大而延长。
  • Shenandoah GC 没有分代的概念,所以它需要在每次回收周期里对存活对象进行标记(分代 GC 不需要这个操作)。不过反过来,Shenandoah 也避免了分代 GC 的一些额外的工作负载
  • Shenandoah GC 的并发性是以降低应用程序的吞吐量为代价。
  • Shenandoah是一款concurrent及parallel的垃圾收集器
  • 跟ZGC一样也是面向low-pause-time的垃圾收集器,不过ZGC是基于colored pointers来实现,而Shenandoah GC是基于brooks pointers来实现
  • 与G1 GC相比,G1的evacuation是parallel的但不是concurrent,而Shenandoah的evacuation是concurrent,因而能更好地减少pause time
吞吐量的降低是可预测的,而且很容易做出应对计划。例如,如果你发现应用程序的运行速度慢了 10%,那就增加 10% 的服务器。而 GC 停顿发生得非常迅速,你无法针对它们进行“自动伸缩”,你能做的是为它们分配额外的资源,这些资源在大部分时间是闲置的,造成了金钱的浪费。

垃圾回收器总结

目标垃圾回收器说明
串性垃圾回收器Serial适用于客户端或者client 模式运行的虚拟机
高吞吐垃圾回收器Parallel注重吞吐量的垃圾回收器
低延迟垃圾回收器CMS注重低延迟的垃圾回收器

PS+PO => 加内存换垃圾回收器 => PN+CMS+SerialOld (因为Parallel 的STW 的停留时间是在是太长了),但是当升级失败之后,SerialOld 就很恐怖了,在大内存的情况下,单线程可能要回收几个小时甚至几天。

G1 几十个G 的堆内存
ZGC 上T 的内存

  • 在单线程的环境下,Serial 最强
  • 在吞吐量优先的情况下Parrallel 最强
  • 其他垃圾回收器都是针对响应时间的

垃圾回收器的指定与默认组合

  • 首先 java -version 看一下你的jvm 是不是以server 模式运行,大多数都是的,所以下面的都是基于server 模式运行的jvm 而言的
(base) kingcall:~ XXX$ java -version
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
  • 基本代码
public class EdenAllocation {
    public static void main(String[] args) {
        edenAllocation();

    }
    public static void edenAllocation(){
        byte[] a1, a2, a3, a4;
        a1 = new byte[2 * _MB];
        a2 = new byte[2 * _MB];
        a3 = new byte[2 * _MB];
        a4 = new byte[4 * _MB];
    }
}

  • 下面涉及各个垃圾回收器的日志,格式基本上差不多,主要是更具日志里面的信息识别垃圾回收器 ,如果不认识就得记住,不过可以从名字上看个七七八八

默认组合

[GC (Allocation Failure) [PSYoungGen: 8192K->640K(9216K)] 12288K->4744K(19456K), 0.0012260 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 804K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 2% used [0x00000007bf600000,0x00000007bf6290e8,0x00000007bfe00000)
  from space 1024K, 62% used [0x00000007bfe00000,0x00000007bfea0000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 4104K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 40% used [0x00000007bec00000,0x00000007bf002010,0x00000007bf600000)
 Metaspace       used 3055K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 334K, capacity 388K, committed 512K, reserved 1048576K
  • 可以看出默认情况下使用的实吞吐量优先的垃圾回收器
  • ParOldGen ——> Parallel Old; PSYoungGen——> Parallel Scavenge

指定UseSerialGC

  • -XX:+UseSerialGC 真正的参数应该是这样的,但是注意的实这个仅仅指定了年轻代的垃圾回收,可以看出当我们指定了年轻代的垃圾回收之后,老年代就给我们了一个默认组合,其实这个也是非常常见且合理的组合,因为年轻代指定了单线程的垃圾回收器,证明我们的应用场景是这样的,所以老年代也给了一个这样的垃圾回收器。
[GC (Allocation Failure) [DefNew: 8192K->514K(9216K), 0.0037798 secs] 8192K->6658K(19456K), 0.0037984 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 4693K->4693K(9216K), 0.0000069 secs][Tenured: 6144K->6144K(10240K), 0.0027005 secs] 10838K->10676K(19456K), [Metaspace: 3020K->3020K(1056768K)], 0.0027397 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [Tenured: 6144K->6144K(10240K), 0.0011879 secs] 10676K->10658K(19456K), [Metaspace: 3020K->3020K(1056768K)], 0.0011981 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4973K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  60% used [0x00000007bec00000, 0x00000007bf0db4a8, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 tenured generation   total 10240K, used 6144K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  60% used [0x00000007bf600000, 0x00000007bfc00030, 0x00000007bfc00200, 0x00000007c0000000)
 Metaspace       used 3080K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 337K, capacity 388K, committed 512K, reserved 1048576K
  • DefNew(default new generation)–>UseSerialGC,Tenured–> Serial Old

指定 UseSerialGC

  • -XX:+UseParNewGC 指定用ParNew 垃圾收集器,但是这个时候因为没有指定老年代的垃圾回收器,所以默认给了Serial old 垃圾回收器回收老年代,但是这个时候出现了一个警告
Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release
  • 因为我们年轻代选的的是一个多线程的垃圾回收器,所以这个时候我我们应该给老年代也选择一个多线程的,否则我们就不应该给年轻代选择多线程的垃圾回收器,并且还有一个原因,就是这个垃圾回收器之所以还存在就是为了搭配CMS,否则我们就使用ParallelGC 了。
Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release
[GC (Allocation Failure) [ParNew: 7966K->536K(9216K), 0.0054598 secs] 7966K->6680K(19456K), 0.0054949 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 4714K->4714K(9216K), 0.0000597 secs][Tenured: 6144K->6144K(10240K), 0.0024158 secs] 10858K->10670K(19456K), [Metaspace: 2950K->2950K(1056768K)], 0.0025505 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [Tenured: 6144K->6144K(10240K), 0.0023953 secs] 10670K->10652K(19456K), [Metaspace: 2950K->2950K(1056768K)], 0.0024328 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 par new generation   total 9216K, used 5216K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  63% used [0x00000007bec00000, 0x00000007bf118058, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 tenured generation   total 10240K, used 6144K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  60% used [0x00000007bf600000, 0x00000007bfc00030, 0x00000007bfc00200, 0x00000007c0000000)
 Metaspace       used 3000K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 331K, capacity 388K, committed 512K, reserved 1048576K

指定 UseParallelGC

  • -XX:+UseParallelGC 指定使用吞吐量有限的垃圾收集器,其实就是默认的组合,老年代也给了对应的Parallel Old 垃圾回收器
[GC (Allocation Failure) --[PSYoungGen: 8192K->8192K(9216K)] 12288K->16392K(19456K), 0.0025128 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 8192K->2563K(9216K)] [ParOldGen: 8200K->8193K(10240K)] 16392K->10757K(19456K), [Metaspace: 3048K->3048K(1056768K)], 0.0038758 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) --[PSYoungGen: 2563K->2563K(9216K)] 10757K->10757K(19456K), 0.0010899 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 2563K->2545K(9216K)] [ParOldGen: 8193K->8193K(10240K)] 10757K->10738K(19456K), [Metaspace: 3048K->3048K(1056768K)], 0.0032565 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 2875K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 35% used [0x00000007bf600000,0x00000007bf8cecc0,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
  to   space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
 ParOldGen       total 10240K, used 8193K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 80% used [0x00000007bec00000,0x00000007bf400608,0x00000007bf600000)
 Metaspace       used 3100K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 339K, capacity 388K, committed 512K, reserved 1048576K

指定 UseParallelOldGC

  • -XX:+UseParallelOldGC 指定使用老年代使用 Parallel Old 垃圾收集器,年轻代给了对应的Parallel Scavenge 垃圾收集器。
[GC (Allocation Failure) [PSYoungGen: 6248K->608K(9216K)] 6248K->4712K(19456K), 0.0023993 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) --[PSYoungGen: 6991K->6991K(9216K)] 11095K->13151K(19456K), 0.0013657 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 6991K->4607K(9216K)] [ParOldGen: 6160K->6145K(10240K)] 13151K->10753K(19456K), [Metaspace: 3048K->3048K(1056768K)], 0.0041210 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 4607K->4589K(9216K)] [ParOldGen: 6145K->6145K(10240K)] 10753K->10735K(19456K), [Metaspace: 3048K->3048K(1056768K)], 0.0036353 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 5011K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 61% used [0x00000007bf600000,0x00000007bfae4d58,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 6145K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 60% used [0x00000007bec00000,0x00000007bf200690,0x00000007bf600000)
 Metaspace       used 3099K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 339K, capacity 388K, committed 512K, reserved 1048576K

关于自定义组合垃圾收集器

异常错误提示

Conflicting collector combinations in option list; please refer to the release notes for the combinations allowed
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

总结

垃圾回收器的默认组合表

垃圾回收器的选择到底影响了什么

  • 内存的分配方式

日志

日志格式解读

日志配置

日志文件设置

  • -Xloggc:…/logs/gc.log
可以给GC日志的文件后缀加上时间戳,当JVM重启以后,会生成新的日志文件,新的日志也不会覆盖老的日志,只需要在日志文件名中添加%t的后缀即可
%t会给文件名添加时间戳后缀,格式是YYYY-MM-DD_HH-MM-SS。这样就非常简单了克服了UseGCLogFileRotation存在的所有的问题!
  • -XX:+UseGCLogFileRotation
  • -XX:NumberOfGCLogFiles=5
  • -XX:GCLogFileSize=20M
JVM的一个日志文件达到了20M以后,就会写入另一个新的文件,最多会有5个日志文件,他们的名字分别是:gc.log.0、gc.log.1、gc.log.2、gc.log.3、gc.log.4.

日志内容设置

  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps //打印距jvm启动时间间距差值时间
  • -XX:+PrintGCDateStamps //打印当前系统时间
  • -XX:+PrintGCCause
  • -XX:+PrintGCApplicationConcurrentTime // 在日志中输出每次垃圾回收前,应用未中断的执行时间
  • -XX:+PrintGCApplicationStoppedTime // 在日志中输出垃圾回收期间应用STW的暂停时间
  • -XX:+PrintHeapAtGC // 在日志里输出堆中各代的内存大小分布
  • -XX:+PrintTLAB // 在日志里输出打印TLAB相关信息
  • -XX:+PrintReferenceGC //日志里输出Reference相关内容
  • -XX:+PrintTenuringDistribution // 在日志中输出对象年龄分布

其他

滚动日志文件到底要不要

日志文件丢失

  • 如果你配置的日志文件个数是5个,一段时间过后就会产生出来5个日志文件,假如最老的是gc.log.0,最近的是gc.log.4,当gc.log.4到达20M以后,日志会重新写入到gc.log.0,gc.log.0之前的内容会被清空掉!

日志文件混乱

  • 假如现在还是有5个日志文件:gc.log.0到gc.log.4,现在JVM重启了,此时GC的日志会重新从gc.log.0开始写入,但是gc.log.1、gc.log.2、gc.log.3、gc.log.4这里面的日志却还是之前旧的日志!新旧日志就掺杂在了一起!要解决这个问题,在重启服务器之前你需要把老的日志全部迁移到其他地方。

问题

  • 为什么要让长期存活的对象进入老年代
  • 如果

常见JVM 命令

java -XX:+PrintFlagsFinal -version
java -XX:+PrintCommandLineFlags -version

打印所有参数初始化默认值
-XX:+PrintFlagsInitial
打印所有参数赋值后的值
-XX:+PrintFlagsFinal
打印前两参数的差值
-XX:+PrintCommandLineFlags

    • 开头的叫做标准参数
  • X 开头非标准参数,每个JVM的实现不同
  • XX 开头不稳定参数,

最后

以上就是迷你战斗机为你收集整理的胡说八道JVM—垃圾回收算法和垃圾回收器垃圾回收算法分代垃圾回收在虚拟机上的实现垃圾回收器(算法的实现)日志的全部内容,希望文章能够帮你解决胡说八道JVM—垃圾回收算法和垃圾回收器垃圾回收算法分代垃圾回收在虚拟机上的实现垃圾回收器(算法的实现)日志所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(61)

评论列表共有 0 条评论

立即
投稿
返回
顶部