垃圾收集器在虚拟机规范中并没有过多规定,可以由不同厂商、不同版本的jvm来实现,由于jdk的不断迭代,已经衍生出了众多的GC版本
按线程数分为:串行、并行
按工作模式分为:并发式和独占式
按碎片处理方式:压缩式和非压缩式
这几个指标中,内存占用、吞吐量和暂停时间,三者不可能同时满足,而随着硬件性能的提升,内存占用问题越来越能被容忍,而内存空间越大,吞吐量就越大,但是暂停时间就会更长
所以吞吐量和暂停时间,是矛盾的点
运行用户代码的时间占总运行时间的比例(总运行时间=内存回收时间+程序运行时间)
在这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应(暂停时间)是不必考虑的
注重吞吐量,一次垃圾回收的时间会更长,但是总的垃圾回收的时间会短一些
暂停时间就是stw停止应用程序线程的时间,暂停时间优先,意味着尽可能的让单次的stw时间最短,这样回收的频率会更高,总的垃圾回收时间会更长一些,对于需要频繁交互的程序暂停时间短(低延迟)体验更好
高吞吐量和低延迟是竞争关系,无法同时满足,所以一个垃圾回收器只能针对于其中一个优先来设计,或者两者采取折中方案
垃圾回收器是java的招牌能力,极大的提高了开发效率
7款经典的垃圾回收器:
串行回收器:Serial、 Serial old (垃圾回收时,只有一个线程进行垃圾回收)
并行回收器:ParNew 、Parallel Scavenge 、 Parallel old (垃圾回收时,有多个垃圾回收线程)
并发回收器:CMS、G1 (垃圾回收线程和工作线程交替执行)
不同的jdk版本,垃圾收集器的组合方式
jdk8中默认的垃圾收集器组合为:Parallel Scavenge 和 Parallel old ,也使用 ParNew 和 CMS ,或者Serial 和 Serial old
jdk9默认的是G1
Serial old 是 CMS 的保底机制
因为java的使用场景很多,所以针对不同的场景,会有不同的垃圾回收器,所以没有最好的垃圾回收器,只有最适合的
Serial 是最基本、历史最悠久的垃圾回收器,是jdk1.3之前回收新生代的唯一选择,也是hotspot在client模式下默认的新生代垃圾回收器
Serial 收集器采用复制算法,串行回收和STW机制的方式执行内存回收
Serial old 同样采用串行收集器和STW机制,但是内存回收算法采用的是标记-压缩算法
Serial old是client模式下,默认的老年代垃圾回收器,在server模式下Serial old的主要用途是:与新生代的Parallel Scavenge 配合使用,以及作为老年代CMS的后备垃圾收集方案
这两个收集器都是单线程的,单线程的意义不仅仅在于只会有一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾回收时,必须暂停其他所有工作线程,直到它收集结束
Serial的优点在于,简单高效,这里的高效是针对其他单线程收集器来说的,对于单个cpu的环境来说,Serial由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率,在client模式下是不错的选择
在内存不大的场景下,可以在较短时间完成垃圾回收,只要不是频繁发生,串行垃圾回收器是可以接受的
在hotspot中,使用 -XX:+UseSerialGc 可以指定年轻代和老年代都使用串行的垃圾收集器(Serial 和 Serial old)
现在基本已经不用串行垃圾回收器了,因为很少会有单核cpu了,而且对于交互性较强的应用,这种垃圾收集器是不可接受的
ParNew除了采用并行回收以外,和Serial基本没有区别,都是复制算法和STW机制,可以理解为Serial的多线程版本
Par是Parallel的缩写,New代表只能处理新生代
ParNew是jvm在Server模式下新生代的默认垃圾收集器
ParNew 一般和Serial Old 或者 CMS一起使用,对于新生代,回收次数频繁,使用并行的方式要更高效,对于老年代,回收的次数少,使用串行的方式节省资源(因为不涉及线程的资源)
基本已经不在使用了
Parallel Scavenge 同样采用复制算法、并行回收和STW机制
和ParNew 不同,Parallel Scavenge 的目标是达到一个可控的吞吐量,也被称为吞吐量优先的垃圾收集器
Parallel Scavenge 的自适应策略也是与Parnew 一个重要区别
高吞吐量可以高效率地利用CPU时间,尽快完成运算任务,主要适合在后台运算而不需要太多交互的任务,常用与服务器
Parallel 收集器在1.6时提供了用于执行老年代垃圾收集的Parallel old 收集器,用来代替 Serial old
Parallel old 采用标记-压缩算法,同样基于并行回收和STW机制
jdk8默认就是Parallel 与Parallel old
-XX:+UseParallelGC 手动指定年轻代使用Parallel垃圾收集器
-XX:+UseParalleloldGC 手动指定老年代使用Parallel Old垃圾收集器
-XX:ParallelGCTheards 设置年轻代并行收集器的线程数
-XX:MaxGCPauseMillis 设置垃圾收集器的最大停顿时间也就是stw的时间,单位为毫秒,使用这个参数要慎重
-XX:GCtimeRation 垃圾收集器占总时间的比例,用于衡量吞吐量的大小
-XX:+UseAdaptiveSizePolicy 设置收集器具有自适应调节策略,默认是开启的,能够动态的调整年轻代、老年代的比例,年轻代内伊甸园区、幸存者区的比例,以及晋升老年代的对象年龄等参数
对客户来说,暂停时间更重要,对于服务器来说,吞吐量更重要
jdk 1.5 CMS(Concurrent-Mark-Sweep)收集器,这是hotspot虚拟机第一款真正意义上的并发收集器,实现了让垃圾收集线程和用户线程同时工作
CMS 的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越低,与用户交互体验就越好
CMS 使用的算法是标记-清除算法,一样会有stw
初始标记:工作线程会因为STW机制出现短暂的暂停,这个阶段的目的是标记处GC Roots 能直接关联到对象,由于直接关联的对象少,所以比较块
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
重新标记:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,会有一些错误标记,为了修正并发标记期间,因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,但是比并发标记阶段的时间短
并发清除:清理删除标记阶段判断已近死亡的对象,释放内存空间,因为不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发
目前并没有垃圾收集器可以完全不需要stw,CMS在初始化标记、重新标记这两个阶段仍然需要执行STW机制,但是暂停的时间不会很长,而最耗费时间的并发标记、并发清除阶段都不需要暂停工作线程,所以整体的回收是低停顿的
因为在执行垃圾回收过程中,用户线程并没有中断,所以还需要确保用户线程有足够的内存可用,因此,CMS不能像其他收集器一样等到老年代几乎完全被填满了在收集,而是当堆内存使用率达到某一阈值时,就开始回收,如果预留的内存不足以支持工作线程运行,就会临时启用Serial old 收集器来进行老年代的垃圾回收,所以CMS有一个预备方案Serial old,但是这个预备方案的回收时间会很长
CMS的优点是:并发收集和低延迟,但是
-XX:+UseConcMarkSweepGC 手动指定使用CMS收集器执行内存回收任务,设置这个参数后,会自动开启ParNew+CMS+Serial old
-XX:CMSInitiatingOccupanyFraction 设置堆内存使用率的阈值,jdk5默认为68%,jdk6及以上版本默认是92%,这个值越大,越不容易触发CMS,可以减少老年代的回收次数,但是发生FUll GC 的概率也越大
-XX:+UseCMSCompactAtFullCollection 指定在执行完Full GC之后,对内存进行压缩整理,可以避免内存碎片问题,但是停顿时间会变得更长
-XX:CMSFullGCsBeforeCompaction 和上一个参数一起使用,设置在执行了多少次Full GC之后对内存空间进行整理
-XX:ParallelCMSThreads 设置CMS的线程数量,默认的线程数量是:(并行的线程数量+3)/4
CMS在jdk9中已经声明后续会移除了,在jdk14中被彻底删除,想要强行使用的话,也不会报错,会以默认的GC方式启动
GC的选择建议:
最小化使用内存和并行的开销,使用Serial GC
最大化吞吐量:Parallel GC
最小化GC停顿时间:CMS GC
随着硬件性能的提升,和业务场景越来越复杂、庞大,前面几款垃圾收集器逐渐不能满足使用要求
G1是jdk4引入的垃圾收集器,在jdk9开始是默认的垃圾回收器,官方希望G1可以在延迟可控的情况下尽可能的提高吞吐量,所以担当起了全功能的垃圾回收器(同时可以应用于新生代和老年代)
G1(Garbage First)
G1是一个并行的回收器,它把堆内存分割为很多不相关的区域,使用不同的region来表示伊甸园区、幸存者区、老年代等
G1会避免在整个java堆空间中进行全区域的垃圾回收,它会根据每个region里面的垃圾堆积的价值大小,也就是回收所获得的空间大小和回收需要的时间,维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region
因为这种方式的侧重点在于回收垃圾最大价值的区间(region),所以取名为Garbage First,也就是垃圾优先的意思
G1是面向服务端应用的垃圾收集器,主要针对配备多核CPU以及大容量内存的机器,以极高的概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征
G1是jdk4引入的垃圾收集器,jdk7正式启用,在jdk9开始是默认的垃圾回收器,取代了Parallel +Paeallel old 的组合,被称为全能的垃圾收集器
jdk 8中,还不是默认的垃圾收集器,可以使用:-XX:+UseG1GC 来启用
但是G1并不能全方位的强于CMS,比如G1为了垃圾收集所产生的占用和额外执行负载都高于CMS,通常来说,在内存小的应用,CMS的表现要优于G1,G1在大内存应用能发挥出优势,平衡点在6-8G之间
对于G1的调优,可以简单的分为三部,首先开启G1,然后设置堆的最大内存,在设置最大停顿时间,剩下的都可以交给垃圾回收器自主完成
G1将堆空间划分成了2048个大小相同的独立region,region的大小根据堆空间的实际大小而定,整体被控制在1到32MB之间,所有的region大小相同,且在JVM生命周期内不会被改变
虽然保留了新生代、老年代的概念,但是新生代、老年代不在是物理隔离的了,他们都是一部分region的集合,而且不要求连续
一个region有可能属于 Eden、Survivor或者老年代的区域,角色之间可以转换,但是一个region只能属于一个角色
G1 中还增加了一种新的内存区域,叫做 Humongous 内存区域,用于存储大对象,一个对象如果超过一个region的百分之50就会被放到H区
图中空白处表示未使用的内存
G1 GC主要包含三个环节
如果需要还会执行单线程、独占式、高强度的Full GC ,这是一种保护机制
应用程序分配内存时,发现Eden区用尽时开始年轻代的回收过程,年轻代的收集阶段是一个并行的独占式的收集器,会暂停所有的程序线程,启动多线程执行年轻代回收,然后存活对象会被移动到幸存者区间或者老年区间
当堆内存使用率达到一定值时,默认是45%,就开始老年代并发标记
标记完成就开始混合回收过程,对于一个混合回收期,会从老年区间移动存活对象到原本空闲的区间,这些原本空闲的区间也就变成了老年区间,和年轻代不同,老年代的回收器不需要整个老年代被回收,一次只需要扫描回收一部分老年代的region即可,这里老年代和年轻代是一起被回收的
一个对象有可能被不同区域对象引用,例如老年代区的对象可能引用伊甸园区的对象,而想要回收伊甸园区的对象,如果还需要遍历老年代区会十分耗费时间,这个问题在其他的分代垃圾回收器中都存在,只是G1更突出,所以引入记忆集的概念(不止是G1有),jvm就是使用Remembered Set,来避免全局扫描
一个region不可能是孤立的,其中的对象可能被其他任意region中对象引用,而如果扫描整个堆区,会非常浪费时间,所以给每个region都维护了一个记忆集 Rset (Remembered Set)
Jvm启动的时候,G1就会准备好Eden区,程序在运行过程中会不断的创建对象到伊甸园区,当伊甸园区耗尽,就会启动年轻代的垃圾回收,幸存者区耗尽并不会触发Young GC
Young GC在回收时只会回收伊甸园区和幸存者区,YGC时,G1会停止应用程序的执行,创建回收集(Collection Set),会收集是指需要被回收的内存分段的集合,年轻代回收过程中的回收集包含伊甸园区和幸存者区的所有内存分段
YGC具体大概可以分为:
当越来越多的对象晋升到old region时,为了避免内存被耗尽,会触发混合回收,也就是Mixed GC,会回收整个Young Region 和一部分的Old region
Full GC
Full GC是单线程的,且会触发STW,性能很差,G1的初衷是要避免Full GC的
导致Full GC的原因有:
避免显式的设置年轻代的大小,固定年轻代大小会覆盖暂停时间目标
暂停时间不要太苛刻,G1的目标是90%的时间为应用程序时间,10%为垃圾回收时间,暂停时间太短,会导致可回收的region变少,反而会加速Full Gc 的到来
GC的选择推荐:
没有最好的垃圾收集器,只有最适合的场景
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出详细的GC日志
-XX:+PrintGCTimeStamps 输出GC的时间戳,以基准时间的形式
-XX:+PrintGCDateStamps 输出GC的时间戳,以日期形式
-XX:+PrintHeapAtGC 在GC的前后打印出堆的信息
-Xloggc:…/logs/gc.log 日志文件的输出路径
-verbose:gc 打开GC日志,只会显示总的GC堆变化
Full GC:
日志分析工具:GCeasy 官网:
https://gceasy.io/
GC仍然处于告诉发展阶段,目前默认的G1 也在不断地改动,例如串行的full GC在jdk 10以后,已经是并行运行
Serial GC 虽然比较古老,但是简单的设计和实现未必过时,因为开销小,随着云计算的兴起,反而有了新的舞台
而CMS 因为算法的理论缺陷,虽然依然有很大的用户群体,但是jdk9就已经废弃,jdk14就移除掉了
(以上都是指的hotsopt)
open jdk12引入了 Shenandoah GC 主打低停顿时间 ,是第一款不是oracle开发的垃圾回收器,由RedHat(红帽)开发,Oracle拒绝把其加入到Oracle jdk ,所以目前只存在于Open jdk ,号称垃圾回收的暂停时间和堆的大小无关,但是在高负荷的情况下吞吐量下降比较明显
jdk11新引入了 ZGC : 可伸缩、低延迟的垃圾回收器 ,JDK15已经可以投入使用,目标和 Shenandoah 高度相似,都是想尽可能对吞吐量影响不大的前提下,实现任意堆大小都可以把垃圾收集的停顿时间限制在十毫秒以内 ,任然是基于region的内存布局
阿里巴巴基于G1算法推出了AliGC ,是一个面向大堆的GC