- 解释执行: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才会优化
- 如果逃逸分析出来的对象可以在栈上分配,那么该对象的生命周期就跟随线程了,就不需要垃圾回收
- 方法内联:把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用,JVM 会自动识别热点方法,并对它们使用方法内联进行优化
上一篇
RocketMQ-常见问题
2024-09-09
下一篇
JVM-GC-ZGC
2024-09-09