性能测试中除了需要做好性能测试外,我们还需要做性能测试后的,性能调优,需要发现性能问题,也需要做性能调优,在做性能调优中,jvm的性能调优是经常遇到的一个。
随着jdk版本的迅速变化,jdk里面的GC算法也是发生了很多变化,新版的jdk中,G1的已经成了jdk的默认算法了,性能测试中,我们经常关注的比较多的就是tps,吞吐率,内存占用,CPU占用,响应时间,其中GC
的回收对响应时间有非常大的影响,早期的GC回收,基本都会造成很长时间的Stop-The-World 的暂停,新GC算法很多都是围绕降低Stop-The-World 的暂停时间,使得平均响应时间尽量变短,TPS提升的更高。
从内存区域的角度,G1 同样存在着年代的概念,但是与我前面介绍的内存结构很不一样,其内部是类似棋盘状的一个个 region 组成,请参考下面的示意图。
备注:摘选自:Java GC调优怎么做?杨晓峰 出处 | 极客时间《Java 核心技术 36 讲》专栏
region 的大小是一致的,数值是在 1M 到 32M 字节之间的一个 2 的幂值数,JVM 会尽量划分 2048 个左右、同等大小的 region,这点可以从源码 heapRegionBounds.hpp 中看到。当然这个数字既可以手动调整,G1 也会根据堆大小自动进行调整。
在 G1 实现中,年代是个逻辑概念,具体体现在,一部分 region 是作为 Eden,一部分作为 Survivor,除了意料之中的 Old region,G1 会将超过 region 50% 大小的对象(在应用中,通常是 byte 或 char 数组)归类为 Humongous 对象,并放置在相应的 region 中。逻辑上,Humongous region 算是老年代的一部分,因为复制这样的大对象是很昂贵的操作,并不适合新生代 GC 的复制算法。
region 设计本身可能存储在的不足:
region 大小和大对象很难保证一致,这会导致空间的浪费。不知道你有没有注意到,我的示意图中有的区域是 Humongous 颜色,但没有用名称标记,这是为了表示,特别大的对象是可能占用超过一个 region 的。并且,region 太小不合适,会令你在分配大对象时更难找到连续空间,这是一个长久存在的情况,请参考 OpenJDK 社区的讨论。这本质也可以看作是 JVM 的 bug,尽管解决办法也非常简单,直接设置较大的 region 大小,参数如下:
-XX:G1HeapRegionSize=<N, 例如 16>M从 GC 算法的角度,G1 选择的是复合算法,可以简化理解为:
在新生代,G1 采用的仍然是并行的复制算法,所以同样会发生 Stop-The-World 的暂停。
在老年代,大部分情况下都是并发标记,而整理(Compact)则是和新生代 GC 时捎带进行,并且不是整体性的整理,而是增量进行的。
在过去,我们一般将年轻代(新生代)的GC称为Minor GC,老年代 GC 叫作 Major GC,全局整体性的GC叫做full GC,但是新版jdk版本中,已经和过去有了很大的不同了,对于我们讲的G1算法来说:
Minor GC 仍然存在,虽然具体过程会有区别,会涉及 Remembered Set 等相关处理。
老年代回收,则是依靠 Mixed GC。并发标记结束后,JVM 就有足够的信息进行垃圾收集,Mixed GC 不仅同时会清理 Eden、Survivor 区域,而且还会清理部分 Old 区域。可以通过设置下面的参数,指定触发阈值,并且设定最多被包含在一次 Mixed GC 中的 region 比例。
–XX:G1MixedGCLiveThresholdPercent –XX:G1OldCSetRegionThresholdPercent从 G1 内部运行的角度,下面的示意图描述了 G1 正常运行时的状态流转变化,当然,在发生逃逸失败等情况下,就会触发 Full GC。
在G1中出现了很多的新概念,比如Remembered Set,用于记录和维护 region 之间对象的引用关系。为什么需要这么做呢?试想,新生代 GC 是复制算法,也就是说,类似对象从 Eden 或者 Survivor 到 to 区域的“移动”,其实是“复制”,本质上是一个新的对象。在这个过程中,需要必须保证老年代到新生代的跨区引用仍然有效。下面的示意图说明了相关设计。
备注:摘选自:Java GC调优怎么做?杨晓峰 出处 | 极客时间《Java 核心技术 36 讲》专栏