深度探索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

相关文章
|
1月前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
44 0
|
1月前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
1月前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
2月前
|
监控 算法 Java
Java虚拟机垃圾回收机制深度剖析与优化策略####
【10月更文挑战第21天】 本文旨在深入探讨Java虚拟机(JVM)中的垃圾回收机制,揭示其工作原理、常见算法及参数调优技巧。通过案例分析,展示如何根据应用特性调整GC策略,以提升Java应用的性能和稳定性,为开发者提供实战中的优化指南。 ####
47 5
|
2月前
|
监控 Java 编译器
Java虚拟机调优指南####
本文深入探讨了Java虚拟机(JVM)调优的精髓,从内存管理、垃圾回收到性能监控等多个维度出发,为开发者提供了一系列实用的调优策略。通过优化配置与参数调整,旨在帮助读者提升Java应用的运行效率和稳定性,确保其在高并发、大数据量场景下依然能够保持高效运作。 ####
39 1
|
2月前
|
存储 算法 Java
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
|
2月前
|
监控 Java 测试技术
Elasticsearch集群JVM调优垃圾回收器的选择
Elasticsearch集群JVM调优垃圾回收器的选择
67 1
|
2月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
2月前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
2月前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。