深度探索JFR – JFR详细介绍与生产问题定位落地 – 3. 各种Event详细说明与JVM调优策略(3)

深度探索JFR – JFR详细介绍与生产问题定位落地 – 3. 各种Event详细说明与JVM调优策略(3)本文基于OpenJDK113.虚拟机相关Event3.3.JIT即时编译相关JIT即时编译可能会遇到编译后的代码缓存占满,或者因为空间有限或者代码设计问题,导致某些关键方法需要重编译导致性能问题,以及因为代码块过大导致编译失败从而性能有问题,这些问题我们可以通过JFR中相关的Event进行查询。JFR对于Java开发可以完全替换JVM编译日志。额外讲解:JIT相关的知识首先,这里简单介绍下JIT相关的知识(这里我推荐看O’Rerilly上面的Java.

大家好,又见面了,我是你们的朋友全栈君。

本文基于 OpenJDK 11

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 列表

事件名称 简介 default.jfc中的配置 profile.jfc中的配置
CompilerConfiguration 即时编译器配置采集 是否启用: 是;周期: 每写入一个新的 chunk 的开头采集一次 是否启用: 是;周期: 每写入一个新的 chunk 的开头采集一次
CompilerStatistics 即时编译器统计数据采集 是否启用: 是;周期: 每写入一个新的 chunk 的开头采集一次 是否启用: 是;周期: 1000ms一次
Compilation JIT编译事件收集,编译时间大于某一时间限制则被收集 是否启用: 是;时间阈值: 1s 是否启用: 是;时间阈值: 100ms
CompilierPhase 编译阶段,较为底层的事件,一般 Java 开发不关心这个时间 是否启用: 是;时间阈值: 60s 是否启用: 是;时间阈值: 10s
CompilationFailure 编译失败事件,default没有开启,可以考虑开启,但是里面的信息都比较底层,所以一般只看 Compilation 事件定位问题 是否启用: 否 是否启用: 是
CompilerInlining 编译内联事件,这个事件线上不要开启,很影响性能,测试阶段可以开启看看内联失败的原因 是否启用: 否 是否启用: 否
CodeCacheConfiguration 代码缓存配置采集 是否启用: 是;周期: 每写入一个新的 chunk 的开头采集一次 是否启用: 是;周期: 每写入一个新的 chunk 的开头采集一次
CodeCacheStatistics 代码缓存统计数据采集 是否启用: 是;周期: 每一个 chunk 采集一次 是否启用: 是;周期: 每一个 chunk 采集一次
CodeCacheFull 代码缓存空间已满事件 是否启用: 是 是否启用: 是
CodeSweeperConfiguration 代码缓存清理配置采集 是否启用: 是;周期: 每写入一个新的 chunk 的开头采集一次 是否启用: 是;周期: 每写入一个新的 chunk 的开头采集一次
CodeSweeperStatistics 代码缓存清理统计数据采集,这个我们一般不看的 是否启用: 是;周期: 每一个 chunk 采集一次 是否启用: 是;周期: 每一个 chunk 采集一次
SweepCodeCache 代码缓存清理事件采集 是否启用: 是;时间阈值: 100ms 是否启用: 是;时间阈值: 100ms

CompilerConfiguration 事件结构

字段 描述 举例
开始时间 事件开始时间 1594215268138540326 epochns
Thread Count 线程个数 2
Tiered Compilation 是否启用了分层编译(Tiered Compilation) true

CompilerStatistics 事件结构

字段 描述 举例
开始时间 事件开始时间 1594215268138540326 epochns
Bailouts 放弃编译好的机器码使用原有字节码执行的总次数(去优化,对应上面提到的 OSR 中的 Bailout 情况),这个如果经常发生,则需要看一下发生的事件对应的其他 Compiler 相关的事件是否能定位到对应的方法 2
Compiled Methods 编译的方法总数 41329
Invalidated Compilations 无效的编译 0
Compilation Resulting Code Size 程序启动以来编译后的代码大小 85502368 B
Compilation Resulting Size 程序启动以来编译占用的总大小 128496488 B
OSR Bytes Compiled On Stack Replacement(栈上替换,这里是向上优化) 编译的代码字节大小,这个一般为采集不到,我们也不从这里看大小,一般为0,不用关注 0 B
OSR Compilations 程序启动以来 On Stack Replacement(栈上替换,这里是向上优化) 编译总次数 609
Peak Time 程序启动以来最长编译时间 961 ms
Standard Bytes Compiled 标准编译代码大小,这个一般为采集不到,我们也不从这里看大小,一般为0,不用关注 0 B
Standard Compilations 程序启动以来标准编译次数 40108
Total time JIT 编译总占用时间 206547 ms

Compilation 事件结构

字段 描述 举例
开始时间 事件开始时间 1594215268138540326 epochns
持续时间 事件持续时间 55607927 ticks
结束时间 事件结束时间 2869587482704980480 epochticks
Event Thread 编译线程,可能是 C1 CompilerThreadN 或者是 C2 CompilerThreadN,分别对应 C1,C2线程 C1 CompilerThread0
Compiled Code Size 编译后的代码大小 11048 B
Compilation Identifier 本次编译对应的全局唯一id,通过这个 id 确认哪些事件对应的是同一次编译。 5224
Compilation Level 编译级别 1,2,3,4 (0不会出现的,因为就是没编译) 1
Inlined Code Size 内联代码大小 1328 B
On Stack Replacement 是否为栈上替换 false
Java Method 对应的 Java 方法 String java.io.WinNTFileSystem.resolve(File)
Java Method Succeeded 是否编译成功,如果没有成功,看看是否代码块太大导致的 true

CompilierPhase 事件我们并不关心,也不会打开,跳过。CompilationInlining 我们也是一般不看并且不会打开,这里也跳过。

CompilerFailure 事件结构

字段 描述 举例
开始时间 事件开始时间 1594215268138540326 epochns
Event Thread 编译线程,可能是 C1 CompilerThreadN 或者是 C2 CompilerThreadN,分别对应 C1,C2线程 C1 CompilerThread0
Compilation Identifier 本次编译对应的全局唯一id,通过这个 id 确认哪些事件对应的是同一次编译。 6012
Failure Message 失败信息 Jvmti state change invalidated dependencies(JVM正在关闭会报这个)

CodeCacheConfiguration 事件结构

字段 描述 举例
开始时间 事件开始时间 1594215268138540326 epochns
Expansion size 每次扩展大小 65536 B
Initial Size 初始大小 2555904 B
Minimum Block Length 内存分配最小块大小 4 B
Non-nmethod Size 对应上面说的 非方法代码堆大小 5825164 B
Non-profiled Size 对应上面说的 不带采集代码堆大小 122916538 B
Profiled Size 对应上面说的 带采集代码堆大小 122916538 B
Reserved Size 总大小 251658240 B
Reserved Top 代码缓存内存堆地址 140692249706496
Start Address 代码缓存内存堆基地址 140691998048256

CodeCacheFull 事件结构

字段 描述 举例
开始时间 事件开始时间 1594215268138540326 epochns
Event Thread 编译线程,可能是 C1 CompilerThreadN 或者是 C2 CompilerThreadN,分别对应 C1,C2线程 C1 CompilerThread0
Adaptors 非方法堆里面元素数量 578
Code Heap 事件指的是哪个代码堆,看 CacheCache目前处于什么阶段,如果是运行阶段,则可能是:CodeHeap ‘non-profiled nmethods’,CodeHeap ‘profiled nmethods’,CodeHeap ‘non-nmethods’,如果程序刚启动,JVM原生的占用non-nmethod就满了),可能就是 Unused,如果还没拆分出来,则是笼统的 CodeCache Unused
Commited Top 代码缓存内存堆目前请求提交地址 140692249706496
Entries 元素个数 91512
Full Count 目前为止,CodeCacheFull次数 1
Methods 编译方法数量 854
Reserved Top 代码缓存内存堆地址 140692249706496
Start Address 代码缓存内存堆开始地址 140692249706496
Unallocated 未分配内存大小 2637824 B

CodeCacheStatistics 事件结构

字段 描述 举例
开始时间 事件开始时间 1594215268138540326 epochns
Adaptors 非方法堆里面元素数量 578
Code Heap 事件指的是哪个代码堆,看 CacheCache目前处于什么阶段,如果是运行阶段,则可能是:CodeHeap ‘non-profiled nmethods’,CodeHeap ‘profiled nmethods’,CodeHeap ‘non-nmethods’,如果程序刚启动,JVM原生的占用non-nmethod就满了),可能就是 Unused,如果还没拆分出来,则是笼统的 CodeCache Unused
Entries 元素个数 91512
Full Count 目前为止,CodeCacheFull次数 1
Methods 编译方法数量 854
Reserved Top 代码缓存内存堆地址 140692249706496
Start Address 代码缓存内存堆开始地址 140692249706496
Unallocated 未分配内存大小 2637824 B

CodeSweeperConfiguration 事件结构

字段 描述 举例
开始时间 事件开始时间 1594215268138540326 epochns
Code Cache Flushing Enabled 是否启用了Code Cache Flushing,默认也都是启用的 true
Code Sweeper Enabled 是否启用了CodeSweeper,默认都是启用的 true

CodeSweeperStatistics 我们不关心

SweepCodeCache 事件结构

字段 描述 举例
开始时间 事件开始时间 1594215268138540326 epochns
持续时间 事件持续时间 55607927 ticks
结束时间 事件结束时间 2869587482704980480 epochticks
Event Thread 清理线程 Sweeper thread
Methods Flushed 现在为止刷新的方法数量 5
Sweep Identifier 全局唯一清理ID 737
Methods Swept 现在为止清理的方法数量 26700
Methods Zombified 现在为止僵尸化的方法数量 2
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/130622.html原文链接:https://javaforall.net

赞 (0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 建立数据库的方法有哪些_数据库应用原则

    建立数据库的方法有哪些_数据库应用原则1:需求分析好后,找实体,不要有所落下2:分析实体里所要涉及到的属性(比如学生,在这个数据库里我需要他的学号,但不需要他的籍贯)3:分析属性的类型,长度。要想的长远点,比如学生名字的长度,汉族一般几位就够了,可万一来个维族,那几位长度就不够用了。4:实体间的关系,要满足需求和现实5:写出关系模式6:优化关系模式(越满足后面的范式越好)7:建数据库。属性,约束等最好字母+单词(正确的),

    2022年9月1日
  • 数据结构课程设计哈希表的设计与实现课程设计(数据结构哈希表查找姓名设计)

    我负责小组里处理冲突。用RN【30】做随即数列。在冲突的时候使用作为随即增量。为防止重复,在赋值时做适当处理。这是处理前的代码:#include#include#include#include#include#includeusingnamespacestd;#defineMAX_NUM26typedefstructPreson//定义数

    2022年4月16日
  • 微信开放平台—-微信扫码登录

    微信开放平台—-微信扫码登录告知:所有操作是基于域名已备案,并且具有企业级微信公众号!1.准备工作   1.1.注册微信开放平台帐号       https://open.weixin.qq.com   1.2.创建应用(网站应用),填写资料https://open.weixin.qq.com/cgi-bin/appcreate?t=manage/createWeb&amp;type=…

    2022年6月11日
  • Shell循环执行某命令n次

    Shell循环执行某命令n次如下脚本是打印’helloword!‘500次#!/bin/bashi=0times=500while[$i-le$times]doecho’helloword!’done

    2022年7月24日
  • win10配置java环境变量,解决javac不是内部或外部命令等问题「建议收藏」

    win10配置java环境变量,解决javac不是内部或外部命令等问题「建议收藏」昨天重装了win10系统,发现以前配好的java环境变量和tomcat环境变量全都清空了,在重新配置的时候总是出现问题,即在cmd命令窗口下,输入java,显示正常,输入java-version也是显示正常,唯独输入javac,显示“javac不是内部或外部命令,布拉布拉一些的问题”,然后我就开始各种百度各种谷歌,终于在大半夜把这个问题搞定了。。。来看看我的新博客下面来说说这个问题的解…

    2022年5月8日
  • C语言指针的初始化和赋值

    1、指针的初始化指针初始化时,“=”的右操作数必须为内存中数据的地址,不可以是变量,也不可以直接用整型地址值(但是int*p=0;除外,该语句表示指针为空)。此时,*p只是表示定义的是个指针变量,并没有间接取值的意思。例如:inta=25;int*ptr=&a;intb[10];int*point=b;  int*p=&b[0];

    2022年4月18日

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号