JVM-JIT

  • 解释执行:Java程序在运行的时候,主要就是执行字节码指令,一般这些指令会按照顺序解释执行,这种
    就是解释执行
  • JIT (即时编译器 Just In Time Compiler):那些被频繁调用的代码(热点代码)如果按照解释执行,效率非常低
    • 为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化

      • C1编译器(几乎不会对代码进行优化):是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序
      • C2编译器:是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序
        • 但是C2代码已超级复杂,无人能维护!所以才会开发Java编写的Graal编译器取代C2 (JDK10开始)
      • 分层编译:综合了 C1 的启动性能优势和 C2 的峰值性能优势
        • 使用 “-Xint” 参数强制虚拟机运行于只有解释器的编译模式
        • 使用 “-Xcomp” 强制虚拟机运行于只有 JIT 的编译模式下
  • JVM 的执行状态分为了 5 个层次
    • 第 0 层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第二层编译
    • 第 1 层:可称为 C1 编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling
    • 第 2 层:也称为 C1 编译,开启 Profiling,仅执行带方法调用次数和循环回边执行次数 profiling 的 C1 编译
    • 第 3 层:也称为 C1 编译,执行所有带 Profiling 的 C1 编译
    • 第 4 层:可称为 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化
  • java -XX:+PrintFlagsFinal –version 查询JVM参数值
  • 热点代码:那些被频繁调用的代码
    • 这些再次编译后的机器码会被缓存起来,以备下次使用
    • -XX:ReservedCodeCacheSize JIT 编译后的代码都会放在 CodeCache 里,如果这个空间不足,JIT 就无法继续编译,编译执行会变成解释执行,性能会降低一个数量级,同时,JIT 编译器会一直尝试去优化代码,从而造成了 CPU 占用上升
  • 热点探测(基于计数器):方法调用计数器(Invocation Counter)和回边计数器 (Back Edge Counter)
    • 为每个方法建立计数器统计方法的执行次数,如果执行次数超过一定的阈值就认为它是 “热点方法”
    • 这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译
    • 方法调用计数器:用于统计方法被调用的次数,方法调用计数器的默认阈值在客户端模式下是 1500 次,在服务端模式下是 10000 次(java -verion 查询模式,用-XX:CompileThreshold指定次数)
    • 回边计数器:在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)
      • 该值用于计算是否触发 C1 编译的阈值,在不开启分层编译的情况下,在服务端模式下是10700
      • 回边计数器阈值 =方法调用计数器阈值(CompileThreshold)×(OSR比率(OnStackReplacePercentage)-解释器监控比率(InterpreterProfilePercentage))/100
        • 回边计数器阈值 =10000 ×(140-33)=10700
        • 通过 -XX:CompileThreshold 来设置热点方法的阈值
  • 编译优化技术:通过一些例行检查优化,可以智能地编译出运行时的最优性能代码
    • 方法内联:把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用,JVM 会自动识别热点方法,并对它们使用方法内联进行优化
      • 方法体太大了:JVM 将不执行内联操作
      • 经常执行的方法:方法体大小小于 325 字节的都会进行内联,通过 -XX:FreqInlineSize=N 来设置大小值
      • 不是经常执行的方法:方法大小小于 35 字节才会进行内联,通过 -XX:MaxInlineSize=N 来重置大小值
      • 可以通过以下几种方式来提高方法内联
        • 通过设置 JVM 参数来减小热点阈值或增加方法体阈值,但这种方法意味着需要占用更多地内存
        • 避免在一个方法中写大量代码,习惯使用小方法体
        • 尽量使用 final、private、static 关键字修饰方法,编码方法因为继承,会需要额外的类型检查
    • 锁消除:-XX:+EliminateLocks
      • 在局部方法中创建的对象只能被当前线程访问,无法被其它线程访问,这个变量的读写肯定不会有竞争,这个时候 JIT 编译会对这个对象的方法锁进行锁消除
    • 标量替换:前提是需要开启逃逸分析 -XX:+EliminateAllocations
      • 逃逸分析证明一个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执行的时候可能不创建这个对象,而直接创建它的成员变量来代替
      • 将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了
    • 逃逸分析:-XX:+DoEscapeAnalysis 分析对象动态作用域,当一个对象在方法中定义后,它可能被外部方法引用
      • 从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度

      • 如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高JVM的效率

      • 必须要符合热点代码,JIT才会优化

      • 如果逃逸分析出来的对象可以在栈上分配,那么该对象的生命周期就跟随线程了,就不需要垃圾回收

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值