深度探索JFR - 3. 各种Event详细说明与JVM调优策略(3)

简介: 深度探索JFR - 3. 各种Event详细说明与JVM调优策略(3)

3. 虚拟机相关 Event


3.3. JIT即时编译相关

JIT 即时编译可能会遇到编译后的代码缓存占满,或者因为空间有限或者代码设计问题,导致某些关键方法需要重编译导致性能问题,以及因为代码块过大导致编译失败从而性能有问题,这些问题我们可以通过 JFR 中相关的 Event 进行查询。 JFR 对于 Java 开发可以完全替换 JVM 编译日志


额外讲解:JIT 相关的知识

首先,这里简单介绍下 JIT 相关的知识(这里我推荐看 O'Rerilly 上面的 Java Performance 第二版的第四章:Working with the JIT Compiler):

首先什么是 JIT: 当 Java 被编译为字节码形式的 class 文件之后,他可以在任意的 JVM 运行。这里说的编译,主要是指前端编译器。但是 class 文件里面的字节码并不能直接运行,而是要通过后端编译器在程序运行期间,将字节码转变成机器码,这样电脑才能执行你的代码。


Java Code Cache 是啥: 如果 Java 每次都需要即时编译成机器码,再执行,效率太慢了。那么是不是对于某些热点代码,编译后的机器码,缓存起来,这样下次就不用重新即时编译了,多快乐。Java Code Cache 就是用来干这个的。但是编译后的机器码太大了,Java Code Cache 的空间是有限的,也不能将所有的代码都编译成机器码缓存起来。所以就需要一些优化与清理策略。


C1,C2 编译器与分层编译(Tiered Compilation)以及编译级别(Compilation Level): C1,C2 按照老概念来看,分别是客户端编译器和服务端编译器,随着 -server 的 Java 参数的消失,代表着 Java 程序本质上不再区分客户端服务端,所以目前所有的 Java 进程都是 C1,C2混合使用。


C1 编译器对代码做一些简单的优化并加入一些采样代码, C2 针对 C1 加入的代码的采样结果,做更多的分析(语法解析,逃逸分析,高级优化器等等),优化成为更好的代码。C1是一个简单快速的编译器,主要关注点在于局部优化,而放弃许多耗时较长的全局优化手段。C2 则是关注于耗时较长的全局优化手段。同时由于 Java Code Cache 是有限的,有些代码可能优化后被淘汰,需要重新编译,没必要对于每种代码都进行 C1 C2 的优化,那些执行次数少的代码,直接编译执行即可。


C1,C2是怎么配合的呢?从 Java 7 开始引入了分层编译的概念,目前已经是默认的即时编译方式。Java 代码一共有如下几层编译级别:

  • level 0:代码直接解释执行
  • level 1:通过 C1 编译器优化编译执行(不会加入采样代码,例如统计这段代码执行次数等等)
  • level 2:通过 C1 编译器优化编译执行(加入非常轻量的采样代码)
  • level 3:通过 C1 编译器优化编译执行(加入所有的采样代码)
  • level 4:通过 C2 编译器优化编译执行(根据上一步采样代码的信息,决定如何优化)

一般情况下,热点代码都是经过 0 -> 3 -> 4 这个路径被优化的。但是也有例外情况:

  • 当方法过于简单的时候(例如只有一行代码),C2 不能在 C1 的基础上更加优化,则走向 level 1,去掉了采样代码。
  • 如果 C2 线程正忙(C2 线程任务队列满了),则走向 level 2,因为认为 C2 一段时间内不会恢复,level 3 采集的很多数据有时效性,现在采集也没意义,所以先走向 level 2 减少采集。之后过一段时间,重新变成 level 3,等待 C2 闲时,走向 level 4
  • C1 忙,但是 C2 闲时,直接从 level 0 升级到 level 4。


Java Code Cache 分块(Segmented Code Cache): 从 Java 9 开始引入的 Code Cache 分块,主要解决之前把所有种类代码放一起,导致扫描的时候效率低下。例如之前说的有些代码经过 C1 优化,之后 C2 优化,这些代码最好分开存储。(C1 优化过得代码,C2 优化完之后,C1的要被清理掉)。


目前 Java Code Cache 分为 3 块:

  • **非方法代码堆(non-method code heap):**这些是 JIT 编译器要用到的内存区域,例如编译器要用的缓存等等。他们会永远存在于 Code Cache 内。
  • **带采样的代码堆(profiled code heap):**目前是经过 C1 编译器优化的代码,就是轻度优化,带采样代码的代码,存活时间比较短,因为 C1 优化的代码最终会被 C2 优化,或者退回 level 1 去掉采样。
  • **不带采样的代码堆(non-profiled code heap):**包含不带采样的代码,已经完全优化好,并且长期存活的代码。


默认情况下,非方法代码堆包括 3MB 的来自于虚拟机的固定空间占用,以及随着编译线程个数上涨而上涨的空间占用。剩下的空间,带采样的代码堆与不带采样的代码堆评分,可以通过如下参数修改:


  • -XX:NonProfiledCodeHeapSize: 不带采样的代码堆大小.
  • -XX:ProfiledCodeHeapSize: 带采样的代码堆大小.
  • -XX:NonNMethodCodeHeapSize: 非方法代码堆大小.
  • -XX:ReservedCodeCacheSize 以上三个加起来需要等于这个

Code Cache Sweeper(代码缓存清理器): 随着 Java Code Cache 的分块处理,目前 Code Cache Sweeper 只用扫描 带采样的代码堆 和 非方法代码堆,并且可以分开并发扫描,看那些代码缓存可以回收。


Method Inline(方法内联): 通俗来讲,就是JVM在运行时优化编译好的代码,将经常调用的方法从调用替换为方法体代码,减少调用。通常发生在 C1 编译器优化。举一个简单的例子:在编写Java的POJO object时候,经常会用到getter和setter,这是从学校里学java开始,我们就一直使用的设计模式,为了保证POJO类对象的域安全。但是我们是否考虑过,如果每次都用这个而外的方法修改,那么是否都会多一次方法栈寻址调用?这样对性能肯定是有影响的。幸好我们有Java Inline Method这个机制,参考下面的代码:


public class JitInlining {
    public static void main(String args[]) {
        Position position = new Position(25);
        for (int i = 0; i < 1000000; ++i) {
            int x = position.getX();
            position.setX(x + 1);
        }
        System.out.println(position.getX());
    }
}
class Position {
    private int x;
    public Position(int x) {
        this.x = x;
    }
    public int getX() {
        return x;
    }
    public void setX(int x) {
        this.x = x;
    }
}

加上jvm参数来看一下inline效果:-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining


@ 19   com.test.Position::getX (5 bytes)   accessor
@ 27   com.test.Position::setX (6 bytes)   accessor

这个accessor代表jit识别这些方法是某个field的accessor(即getter或者setter),accessor是会被inline的。


OSR(On Stack Replacement): 一种在运行时替换正在运行的函数/方法的栈帧的技术。JIT 优化,一般在某一段代码被执行很多次之后,例如:


for (int i = 0; i < 10000; i ++) {
    this.data++;
}

this.data++;如果被编译器优化之后,这个循环还没执行完,则需要 OSR 机制,动态替换正在栈帧中待执行的代码。这样还没完成的循环,就可以用优化后的代码执行。

通常来看,应该把 所有的栈上替换执行代码都叫做 OSR,但是 Java 中只针对于编译器优化的代码替换原有代码称为 OSR,对于去优化的替换(就是用原有代码替换优化过的代码),称为Bailout


为什么会有这种Bailout的情况呢?代码优化可以分为(这里参考大神的知乎回答):

  • 从低优化向高优化迁移:为了平衡启动性能(刚开始要启动速度快,所以要初始开销小的执行模式)和顶峰性能(代码执行一段时间,对于热点代码,想要更快需要更优化的执行模式,即便优化需要较大开销)
  • 从高优化向低优化迁移:有很多情况,这里仅举例:
  • 高优化层级做了很激进的优化:例如假设某个类不会有别的子类、某个引用一定不是null、某个引用所指向的对象一定是某个具体类型,等),而这个激进的假设假如失效了的话,就必须退回到没有做这些优化的“安全”的低优化层级去继续执行。这个非常重要,有了这种Bailout机制的支持,JIT编译器就可以对代码做非常激进的优化,性能受正确性要求的约束会得到放松,因而对常见代码模式可以生成更快的代码。
  • 高优化层级不便于对代码做调试,如果某个方法之前已经被JIT优化编译了,而后来有调试器动态决定调试该方法,则让它从高优化层级退回到便于调试的低优化层级(例如解释器或者无优化的JIT编译版本的代码)去执行。


JIT即时编译相关的 Event 列表

微信图片_20220624184755.jpg


微信图片_20220624184759.jpg


微信图片_20220624184803.jpg


微信图片_20220624184807.jpg


微信图片_20220624184810.jpg


微信图片_20220624184813.jpg


微信图片_20220624184817.jpg

相关文章
|
7天前
|
监控 Java 调度
探秘Java虚拟机(JVM)性能调优:技术要点与实战策略
【6月更文挑战第30天】**探索JVM性能调优:**关注堆内存配置(Xms, Xmx, XX:NewRatio, XX:SurvivorRatio),选择适合的垃圾收集器(如Parallel, CMS, G1),利用jstat, jmap等工具诊断,解决Full GC问题,实战中结合MAT分析内存泄露。调优是平衡内存占用、延迟和吞吐量的艺术,借助VisualVM等工具提升系统在高负载下的稳定性与效率。
24 1
|
9天前
|
监控 Java 测试技术
Java中的JVM调优技巧
Java中的JVM调优技巧
|
3天前
|
监控 算法 Java
JVM调优---堆溢出,栈溢出的出现场景以及解决方案
【7月更文挑战第3天】堆溢出(Heap Overflow)和栈溢出(Stack Overflow)是两种常见的内存溢出问题,通常发生在内存管理不当或设计不合理的情况下
10 3
|
7天前
|
监控 负载均衡 Java
Java虚拟机调优技巧及性能监控
Java虚拟机调优技巧及性能监控
|
25天前
|
算法 安全 Java
JVM系列4-垃圾收集器与内存分配策略(二)
JVM系列4-垃圾收集器与内存分配策略(二)
27 0
JVM系列4-垃圾收集器与内存分配策略(二)
|
26天前
|
运维 Java Shell
手工触发Full GC:JVM调优实战指南
本文是关于Java应用性能调优的指南,重点介绍了如何使用`jmap`工具手动触发Full GC。Full GC是对堆内存全面清理的过程,通常在资源紧张时进行以缓解内存压力。文章详细阐述了Full GC的概念,并提供了两种使用`jmap`触发Full GC的方法:通过`-histo:live`选项获取存活对象统计信息,或使用`-dump`选项生成堆转储文件以分析内存状态。同时,文中也提醒注意手动Full GC可能带来的性能开销,建议在生产环境中谨慎操作。
|
5天前
|
监控 负载均衡 Java
Java虚拟机调优技巧及性能监控
Java虚拟机调优技巧及性能监控
|
6天前
|
监控 Java 调度
探索JVM性能调优,调优不仅是技术挑战,更是成长过程。
【7月更文挑战第1天】探索JVM性能调优:** 本文深入JVM内存模型,关注堆内存与方法区、栈的优化,通过调整-Xms, -Xmx及垃圾收集器参数减少GC频率。探讨了Serial到G1等垃圾收集器的选择策略,利用jstat、jmap等工具诊断性能瓶颈。实战案例中,通过问题定位、内存分析解决Full GC问题,强调开发者需理解JVM原理,运用工具在复杂场景下实现高效调优。调优不仅是技术挑战,更是成长过程。
10 0
|
9天前
|
存储 监控 算法
深入理解Java虚拟机(JVM)原理与调优技巧
深入理解Java虚拟机(JVM)原理与调优技巧
|
16天前
|
监控 算法 Java
JVM调优-简介(一)
JVM调优-简介(一)
14 0