目录
Parallel GC和Parallel Old GC(吞吐量优先)
总结
一些相关概念
- 吞吐量
- 程序运行的时间占总时间的比值。吞吐量=程序执行时间 / (程序执行时间 + 内存回收时间)
- 垃圾收集开销
- 吞吐量的补数。内存回收时间 / (程序执行时间 + 内存回收时间)
- 暂停时间
- STW 。执行垃圾回收的时候程序暂停的时间
- 收集频率
- 相对于应用程序的执行,收集操作发生的频率
- 内存占用
- 堆内存占用的大小
- 快速
- 一个对象从诞生到被回收所经历的时间
一个GC算法只可能针对吞吐量or暂停时间其中一个。在最大吞吐量优先的情况下,降低暂停时间。
各个垃圾回收器的组合关系
JDK8中默认的垃圾回收器是 Parallel GC 和Parallel Old GC
JDK9中默认的垃圾回收器是G1 GC
CMS垃圾收集器在JDK14中被删除
需要根据不同的场景选择合适的垃圾收集器
查看默认垃圾收集器的两种方式
方式一:-XX:+PrintCommandLineFlags
控制台输出
-XX:CompressedClassSpaceSize=96468992 -XX:+DoEscapeAnalysis-XX:+EliminateAllocations -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=268435456
-XX:MaxMetaspaceSize=104857600 -XX:MetaspaceSize=104857600
-XX:+PrintCommandLineFlags -XX:+PrintGC -XX:+UseCompressedClassPointers
-XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
方式二:jps 结合 jinfo -flag UseParallelGC [PID]查看
C:\Users\qianqian>jinfo -flag UseParallelGC 19572
-XX:+UseParallelGCC:\Users\qianqian>jinfo -flag UseParallelOldGC 19572
-XX:+UseParallelOldGCC:\Users\qianqian>jinfo -flag UseG1GC 19572
-XX:-UseG1GC
Serial GC和Serial Old(串行回收)
Serial GC作为HopSpot客户端(Client)模式下的默认新生代垃圾收集器,使用的是复制算法、串行回收、STW。
Serial Old GC是运行在Client模式下默认的老年代垃圾收集器。采用的是标记-压缩算法、串行回收和STW机制
Serial Old GC在Server模式下主要有两个作用:
- 与Parallel GC新生代垃圾收集器组合使用
- 作为老年代CMS收集器的后备垃圾收集方案
优势:简单而高效(在单线程中和其它收集器相比),对于单个cpu环境,没有线程交互的开销,可以获得更高的单线程收集效率
开启Serial GC(新生代和老年代都使用串行收集器):-XX:+UseSerialGC
ParNew(并行回收)
可以看作是Serial的多线程版本,几乎没有任何区别。新生代的垃圾收集器,采用的也是复制算法和STW机制。是很多JVM默认的新生代垃圾收集器。s
除了在单CPU的环境下,其它的环境都比Serial效率高。
-XX:+UseParNewGC:开启PerNew GC(设置新生代并行收集器,不影响老年代)
-XX:ParallelGCThreads:设置并行的垃圾回收线程数量,默认开启和CPU核心相同的线程数
Parallel GC和Parallel Old GC(吞吐量优先)
Parallel
Parallel和ParNew差不多都是使用的复制算法、STW机制。
适用于与用户交互比较少的场景(吞吐量优先,每次STW相对较长),比如批量处理、订单处理、科学计算等
那为什么还需要Parallel,还在JDK8中把Paraller和Paraller Old设置为默认的垃圾收集器呢?
Parallel和ParNew的区别
- 相对于ParNew更关注的是吞吐量
- 提供了自适应的策略。
- 在JDK1.6及之后提供了Parallel Old组合使用,在此之前只能搭配Serial Old使用
- ParNew可以搭配CMS使用。也可以搭配Serial Old使用。(注意版本变化导致组合的变化)
Parallel Old
使用的也是标记-压缩算法,为了取代Serial Old垃圾收集器在JDK1.6版本加入
相关JVM 参数设置
-XX:+UseParallelGC 开启新生代Parallel GC垃圾收集器
-XX:+UseParallelOldGC 开启老年代Prarllel Old GC垃圾收集器
需要注意的是上面两个参数是互相激活的。设置了-XX:+UseParallelGC,老年代也切换使用ParallelOld,反之一样。
-XX:ParallelGCThreads 设置新生代并行收集器的线程数,一般最好与CPU核心数相等,以避免过多的线程影响垃圾收集的性能
- 在默认情况下,当CPU核心数量小于8个时,ParallelGCThreads的值=CPU核心数量
- 当CPU核心数量大于8个时,ParallelGCThreads的值=3 + (5 * cpu_count) / 8
-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW时间),单位毫秒
查看默认值
jinfo -flag MaxGCPauseMillis 5592
-XX:MaxGCPauseMillis=18446744073709551615
- 为了尽可能的把时间控制在MaxGCPauseMillis以内,收集器在工作的时候会调整Java堆大小或者一些其它的参数
- 对于用户来说停顿的时间越短越好,但在服务器端注重的是高并发,整体的吞吐量。所以服务端适合Parallel。
- 注意:该参数如果设置的过小,收集器为了控制时间,可能会导致调小堆空间。但是堆空间小了也会频繁的触发GC,导致整体的STW时间变长。所以该参数谨慎设置
-XX:GCTimeRatio 垃圾收集时间占总时间的比例(= l / (N + l))。用于衡量吞吐量大小
- N的取值范围( 0 , 100)。默认值是99,也就是垃圾回收时间不超过1%。
- 与-XX:MaxGCPauseMillis有一定的矛盾。暂停时间越长越容易超过比例
-XX:+UseAdaptiveSizePolicy 设置Parallel收集器具有自适应调节的策略
- 在这种模式下,新生代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到堆大小、吞吐量和停顿时间中间的平衡点。
- 在手动调节比较困难的时候,可以直接使用这种自适应的方式,仅针对堆最大大小、目标吞吐量和停顿时间,人虚拟机自己完成调优工作。
CMS(低延迟)
追求的是较短的延迟时间。通常应用在比较关注服务的响应速度,希望停顿的时间尽可能的短。比如互联网网站、基于浏览器的B/S系统的服务端。
老年代垃圾收集器,采用的是标记-清除算法,也会STW。可以搭配新生代ParNew垃圾收集器使用,不建议搭配新生代Serial使用,一是因为Serial是串行的,二是该组合在JDK1.9被删除。
因为是并发的垃圾收集器,为了保证在CMS的过程中用户线程有足够的内存可用,所以不会等到老年代空间不足的时候才进行垃圾回收,而是当堆内存使用率达到一定的阈值时便开始回收。确保在回收的时候用户线程有足够的空间可用。如果在并发的过程空间不足,就会出现一次"Concurrent Mode Failure"失败,这时虚拟机启用后备方案:临时使用Serial Old收集器重新进行老年代的垃圾收集,停顿时间就非常长了。
四个阶段
- 初始标记
- 所有的工作线程会因为STW出现暂停,这个阶段的主要任务是标记出GC Roots能直接关联到的对象,由于直接关联对象比较小,所有这个阶段的速度非常快。
- 并发标记
- 从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,用户线程和垃圾收集线程并发的执行。
- 重新标记
- 会STW,为了修正并发标记期间因用户线程执行而导致标记变动的那一部分对象的标记记录(增量更新),这个阶段通常比初始标记的时间稍长。但是也远比并发标记阶段时间短的多。
- 并发清除
- 清理删除标记阶段判断为已死亡的对象,因为不需要移动对象,所以可以用户线程并发的执行。
缺点
- 使用的是标记-清除算法,会产生内存碎片。之所以使用标记-清除算法,也是为了追求清除阶段GC线程可以和用户线程并发执行。因为标记-清除算法不需要移动对象,而标记-压缩算法需要移动对象,而移动对象用户线程就必须停下来
- 对处理器资源非常敏感,在并发阶段虽然不会导致用户线程暂停,但是因为GC线程占用了用户线程,而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4。当cpu核心数少于4个的时候对用户程序的影响就可能变得很大。
- 无法处理“浮动垃圾”,有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。因为在并发标记和并发清除的阶段,用户线程是持续运行的,那程序自然会有新的垃圾产生,但这一部分垃圾对象是出现在标记阶段之后,所以CMS无法在当次集中清理它们,只能留到下一次垃圾收集清理,这一部分垃圾就叫“浮动垃圾”。在JDK5的时候老年代默认的的阈值是68%,达到这个阈值就会激活CMS进行垃圾收集。到JDK6的时候这个阈值被调整到92%。过低可能导致频繁GC,过高可能出现“并发失败”而启动后备方案。该阈值可以通过“-XX:CMSInitiatingOccu-pancyFraction”设置。
CMS参数设置
-XX:+UseConcMarkSweepGC 指定使用CMS收集器执行内存回收任务
开启该参数会自动将-XX:+UseParNewGC打开
-XX:CMSlnitiatingOccupanyFraction 设置堆内存阈值,达到便开始回收
JDK5默认68,JDK6及以后默认92,视程序老年代内存增长速度决定设置什么值
-XX:+UseCmsCompactAtFullCollection 指定在执行完full GC后对内存空间进行压缩整理,避免内存碎片的产生。但是压缩整理的过程无法与用户线程并发执行。所以会导致停顿时间变长
-XX:CMSFullGCsBeforeCompaction 设置在执行多少次full GC后对内存空间进行压缩整理
-XX:ParallelCMSThreads 设置CMS的线程数量,默认是(处理器核心数量+3)/ 4
设置:(ParallelCMSThreads + 3) / 4
Garbage First(G1) 区域分代
G1把堆内存分割成很多不相关的区域(Region)(物理上不连续)。使用不同的Region来表示Eden、幸存者0区、幸存者1区、老年代等
优点
- 并行与并发
- 并行:回收期间可以多个GC线程同时工作
- 并发:部分工作可以与用户线程同时执行
- 分代收集
- 依旧会分新生代、老年代。新生代依旧有Eden和Survivor区。但从机构上看,不要求整个新生代、老年代是连续的,也不坚持固定大小和固定数量。
- 将堆空间划分成若干个区域(Region),这些区域逻辑上包含了新生代和老年代
- 同时对新生代、老年代进行收集
- 空间整合
- 内存的回收以Region为基本单位,Region之间是复制算法,但整体上实际可以看作是标记-压缩算法。都避免了内存碎片
- 可预测的停顿时间模型(软实时)
- 能明确指定在一个M的时间段之内,消耗在垃圾收集上的时间不超过N毫秒
- 因为分区,所以G1可以只选部分区域进行垃圾回收,缩小了回收的范围,控制停顿时间
- G1会跟踪Region里面的垃圾堆积的价值大小(合适所得到的内存大小和所需要的时间),维护一个优先列表,保证在有限的时间进行高效的收集。
- 在小内存上CMS的效率会优先于G1,平衡点在6-8G之间,在大内存G1效率高于CMS
缺点:G1无论是为了垃圾收集产生的内存占用,还是程序运行时的额外执行负载都比CMS高
G1相关部分参数的设置
-XX:+UseG1GC 指定开启G1垃圾回收,在JDK9中默认的垃圾收集是G1
-XX:G1HeapRegionSize 设置每个Region的大小,值是2的幂。范围是1M-32M中间。
-XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间(不保证达到)。默认值是200ms
-XX:ParallelGCThreads 设置STW时GC线程的数量,默认是8
-XX:ConcGCThreads 设置并发标记的时候GC线程的数量。一般设置为ParallelGCThread的1/4。
-XX:InitiatingHeapOccupancyPercent 设置触发G1的堆占用阈值(并发标记),默认是45
一般使用G1只需要以下步骤
- -XX:+UseG1GC 开启G1垃圾收集
- -Xmx 设置堆的最大内存
- -XX:MaxGCPauseMillis 设置期望停顿时间
其它的交给虚拟机就行。G1提供了3种垃圾回收模式:Young GC、Mixed GC和Full GC,在不同的条件下触发
适用场景
- 高性能服务器。比如大内存、多核cpu
- 需要低延迟。如在堆空间大于6GB时,可预测的暂停时间低于0.5s
- 满足以下情况,G1可能比CMS更好,可以考虑替换CMS
- 大堆内存
- 超过50%的堆内存被占用
- 对象分配频率或年代提升频率变化大
- GC停顿时间过长(大于0.5s)
另外其它的垃圾收集器使用的都是内置的JVM线程执行GC多线程操作,优先级比较低。而G1可以采用应用线程承担后台GC操作,即当JVM的GC线程处理速度慢时,系统会调用应用线程帮助加速垃圾回收的过程
分区Region:化整为零
把堆内存划分成一个个Region(约2048个),每个Region的大小工具堆大小而定。整体被控制在1MB-32MB之间,且为2的次幂(1、2、4、8、16、32),可以通过参数-XX:G1HeapRegionSize设置,所有的Region大小相同,且在Jvm的生命周期大小不会改变
如果一个对象的大小超过了一个Region大小的50%,那么它将分配到H区。
为了解决大对象(短期)直接放人老年代,导致内存浪费的问题,而设置了H区。G1大多数的行为把H区当作老年代的一部分来看待。
Region内部:使用的是指针碰撞,使用一个指针记录Region内部内存的分配。需要分配内存的时候移动指针。
记忆集(Remembered set)、卡表和写屏障
为了解决跨代引用引入了记忆集的概念,卡表可以看作是记忆集的实现。为了维护卡表状态又引入了写屏障。
跨代引用:比如需要回收新生代的对象,是否需要扫描所有的老年代看是否被引用。一个Region里的对象也可能被其他Region所引用,那回收一个Region是否需要扫描所有的Region呢?
JVM是使用记忆集(Remembered set)来避免全局扫描
每个Region都有一个记忆集,每次引用类型写数据时,都会产生一个写屏障暂时中断操作,判断引用了该对象的的指向和该对象是否在不同的Region,如果不同,通过卡表把相关的引用信息记录被引用对象所在的Region中。
当进行垃圾收集的时候,在GC根节点枚举的时候把卡表也加入进去,就可以保证不进行全局扫描
G1垃圾回收的过程
年轻代GC是一个并行、独占的收集器,会暂停所有的用户线程,启用多线程进行年轻代的垃圾回收。当堆内存超过45%(默认)时,开始老年代并发标记、标记完成马上开始老年代的垃圾回收。G1老年代的垃圾回收只是扫描/回收一部分价值高的老年代Region以控制停顿时间。同时这个老年代的Region是和年轻代一起回收的。
具体过程
年轻代GC
并发标记
混合回收
Full GC
总结
垃圾收集器选择
单CPU选串行;多CPU对吞吐量有要求选Parallel/Parallel Old;对延迟时间有要求选择并发收集器,一般推荐选G1。