性能测试中除了需要做好性能测试外, 我们还需要做性能测试后的, 性能调优, 需要发现性能问题, 也需要做性能调优, 在做性能调优中, 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 讲》专栏
G1 的很多开销都是源自 Remembered Set, 例如, 它通常约占用 Heap 大小的 20% 或更高, 这可是非常可观的比例. 并且, 我们进行对象复制的时候, 因为需要扫描和更改 Card Table 的信息, 这个速度影响了复制的速度, 进而影响暂停时间.
在 G1 算法中记录了老年代 region 间对象引用, Humongous 对象数量有限, 所以能够快速的知道是否有老年代对象引用它. 如果没有, 能够阻止它被回收的唯一可能, 就是新生代是否有对象引用了它, 但这个信息是可以在 Young GC 时就知道的, 所以完全可以在 Young GC 中就进行 Humongous 对象的回收, 不用像其他老年代对象那样, 等待并发标记结束.
8u20 以后字符串排重的特性, 在垃圾收集过程中, G1 会把新创建的字符串对象放入队列中, 然后在 Young GC 之后, 并发地 (不会 STW) 将内部数据 (char 数组, JDK 9 以后是 byte 数组) 一致的字符串进行排重, 也就是将其引用同一个数组. 你可以使用下面参数激活:
-XX:+UseStringDeduplication
这种排重虽然可以节省不少内存空间, 但这种并发操作会占用一些 CPU 资源, 也会导致 Young GC 稍微变慢.
通过
-XX:+TraceClassUnloading
可以查看到 G1 算法的类型卸载方式, 8u40 以后的 jdk 版本中, G1 增加并默认开启下面的选项:
-XX:+ClassUnloadingWithConcurrentMark
在并发标记阶段结束后, JVM 即进行类型卸载, 并不会在发生了 full GC 才进行类型卸载.
在之前的 jdk 版本中, 老年代对象回收, 基本要等待并发标记结束. 这意味着, 如果并发标记结束不及时, 导致堆已满, 但老年代空间还没完成回收, 就会触发 Full GC, 所以触发并发标记的时机很重要. 早期的 G1 调优中, 通常会设置下面参数, 但是很难给出一个普适的数值, 往往要根据实际运行结果调整.
-XX:InitiatingHeapOccupancyPercent
在 JDK 9 之后的 G1 实现中, 这种调整需求会少很多, 因为 JVM 只会将该参数作为初始值, 会在运行时进行采样, 获取统计数据, 然后据此动态调整并发标记启动时机. 对应的 JVM 参数如下, 默认已经开启:
-XX:+G1UseAdaptiveIHOP
在新的 jdk 中, full gc 已经从单线程串行 GC 变更为了并行进行了, 在通用场景中的表现还优于 Parallel GC 的 Full GC 实现.
性能 GC 调优一些建议如下:(摘选自: Java GC 调优怎么做? 杨晓峰 出处 | 极客时间《Java 核心技术 36 讲》)
1, 首先, 建议尽量升级到较新的 JDK 版本, 从上面介绍的改进就可以看到, 很多人们常常讨论的问题, 其实升级 JDK 就可以解决了.
2, 掌握 GC 调优信息收集途径. 掌握尽量全面, 详细, 准确的信息, 是各种调优的基础, 不仅仅是 GC 调优. 我们来看看打开 GC 日志, 这似乎是很简单的事情, 可是你确定真的掌握了吗?
除了常用的两个选项,
- -XX:+PrintGCDetails
- -XX:+PrintGCDateStamps
还有一些非常有用的日志选项, 很多特定问题的诊断都是要依赖这些选项:
-XX:+PrintAdaptiveSizePolicy // 打印 G1 Ergonomics 相关信息
我们知道 GC 内部一些行为是适应性的触发的, 利用 PrintAdaptiveSizePolicy, 我们就可以知道为什么 JVM 做出了一些可能我们不希望发生的动作. 例如, G1 调优的一个基本建议就是避免进行大量的 Humongous 对象分配, 如果 Ergonomics 信息说明发生了这一点, 那么就可以考虑要么增大堆的大小, 要么直接将 region 大小提高.
如果是怀疑出现引用清理不及时的情况, 则可以打开下面选项, 掌握到底是哪里出现了堆积.
-XX:+PrintReferenceGC
另外, 建议开启选项下面的选项进行并行引用处理.
-XX:+ParallelRefProcEnabled
需要注意的一点是, JDK 9 中 JVM 和 GC 日志机构进行了重构, 其实我前面提到的
PrintGCDetails 已经被标记为废弃, 而 PrintGCDateStamps 已经被移除, 指定它会导致 JVM 无法启动. 可以使用下面的命令查询新的配置参数.
java -Xlog:help
最后, 来看一些通用实践, 理解了我前面介绍的内部结构和机制, 很多结论就一目了然了, 例如
如果发现 Young GC 非常耗时, 这很可能就是因为新生代太大了, 我们可以考虑减小新生代的最小比例.
-XX:G1NewSizePercent
降低其最大值同样对降低 Young GC 延迟有帮助.
-XX:G1MaxNewSizePercent
如果我们直接为 G1 设置较小的延迟目标值, 也会起到减小新生代的效果, 虽然会影响吞吐量.
如果是 Mixed GC 延迟较长, 我们应该怎么做呢?
还记得前面说的, 部分 Old region 会被包含进 Mixed GC, 减少一次处理的 region 个数, 就是个直接的选择之一.
我在上面已经介绍了 G1OldCSetRegionThresholdPercent 控制其最大值, 还可以利用下面参数提高 Mixed GC 的个数, 当前默认值是 8,Mixed GC 数量增多, 意味着每次被包含的 region 减少.
-XX:G1MixedGCCountTarget
需要注意的是, 要避免过度调优, G1 对大堆非常友好, 其运行机制也需要浪费一定的空间, 有时候稍微多给堆一些空间, 比进行苛刻的调优更加实用.
来源: https://www.cnblogs.com/laoqing/p/9738872.html