深入浅出JVM(七)之执行引擎的解释执行与编译执行

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 深入浅出JVM(七)之执行引擎的解释执行与编译执行

本篇文章围绕执行引擎,深入浅出的解析执行引擎中解释器与编译器的解释执行和编译执行、执行引擎的执行方式、逃逸分析带来的栈上分配、锁消除、标量替换等优化以及即时编译器编译对热点代码的探测

执行引擎

hotspot执行引擎结构图

执行引擎分为解释器、JIT即时编译器以及垃圾收集器

执行引擎通过解释器/即时编译器将字节码指令解释/编译为对应OS上的的机器指令

image.png

本篇文章主要围绕解释器与即时编译器,垃圾收集器将在后续文章解析

解释执行与编译执行

Java虚拟机执行引擎在执行Java代码时,会有两种选择:解释执行和编译执行

解释执行:通过字节码解释器把字节码解析为机器语言执行

编译执行:通过即时编译器产生本地代码执行

Class文件中的代码到底是解释执行还是编译执行只有Java虚拟机自己才能判断准确

编译过程

image.png  

编译流程在前一篇文章深入浅出JVM之前端编译过程与语法糖原理已经说明,在本篇文章中不再概述

经典编译原理: 1.对源码进行词法,语法分析处理 2.把源码转换为抽象语法树

javac编译器完成了对源码进行词法,语法分析处理为抽象语法树,再遍历抽象语法树生成线性字节码指令流的过程

剩下的指令流有两种方式执行

  1. 由虚拟机内部的字节码解释器去将字节码指令进行逐行解释 (解释执行)
  2. 或优化器(即时编译器)优化代码最后生成目标代码 (编译执行)

执行引擎流程图

image.png

解释器与编译器

解释器

作用: 对字节码指令逐行解释

优点: 程序启动,解释器立即解释执行

缺点: 低效

即时编译器 (just in time compiler)

Java中的"编译期"不确定

  • 可能说的是执行javac指令时的前端编译器 (.java->.class)
  • 也可能是后端编译器JIT (字节指令->机器指令)
  • 还可能是AOT编译器(静态提前编译器) (.java->机器指令)

作用: 将方法编译成机器码缓存到方法区,每次调用该方法执行编译后的机器码

优点: 即时编译器把代码编译成本地机器码,执行效率高,高效

缺点: 程序启动时,需要先编译再执行

执行引擎执行方式

执行引擎执行方式大致分为3种

-Xint: 完全采用解释器执行

-Xcomp: 优先采用即时编译器执行,解释器是后备选择

-Xmixed: 采用解释器 + 即时编译器

image.png

hotspot中有两种JIT即时编译器

Client模式下的C1编译器:简单优化,耗时短(C1优化策略:方法内联,去虚拟化,冗余消除)

Server模式下的C2编译器:深度优化,耗时长 (C2主要是逃逸分析的优化:标量替换,锁消除,栈上分配)

分层编译策略:程序解释执行(不开启逃逸分析)可以触发C1编译,开启逃逸分析可以触发C2编译

解释器,C1,C2编译器同时工作,热点代码可能被编译多次

解释器在程序刚刚开始的时候解释执行,不需要承担监控的开销

C1有着更快的编译速度,能为C2编译优化争取更多时间

C2用高复杂度算法,编译优化程度很高的代码

逃逸分析带来的优化

当对象作用域只在某个方法时,不会被外界调用到,那么这个对象就不会发生逃逸

开启逃逸分析后,会分析对象是否发生逃逸,当不能发生逃逸时会进行栈上分配、锁消除、标量替换等优化

栈上分配内存

 //-Xms1G -Xmx1G -XX:+PrintGCDetails 
 public class StackMemory {
     public static void main(String[] args) {
         long start = System.currentTimeMillis();
 ​
         for (int i = 0; i < 10000000; i++) {
             memory();
         }
 ​
         System.out.println("花费时间:"+(System.currentTimeMillis()-start)+"ms");
 ​
         try {
             TimeUnit.SECONDS.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }
 ​
     private static void memory(){
         StackMemory memory = new StackMemory();
     }
 }

-XX:-DoEscapeAnalysis 花费时间:63ms (未开启逃逸分析)

-XX:+DoEscapeAnalysis 花费时间:4ms (开启逃逸分析)

默认开启逃逸分析

锁消除

同步加锁会带来开销

锁消除:当加锁对象只作用某个方法时,JIT编译器借助逃逸分析判断使用的锁对象是不是只能被一个线程访问,如果是这种情况下就不需要同步,可以取消这部分代码的同步,提高并发性能

标量替换

标量: 无法再分解的数据 (基本数据类型)

聚合量: 还可以再分解的数据 (对象)

标量替换: JIT借助逃逸分析,该对象不发生逃逸,只作用于某个方法会把该对象(聚合量)拆成若干个成员变量(标量)来代替

默认开启标量替换

 public class ScalarSubstitution {
     static class Man{
         int age;
         int id;
 ​
         public Man() {
         }
     }
 ​
     public static void createInstance(){
         Man man = new Man();
         man.id = 123;
         man.age = 321;
     }
     public static void main(String[] args) {
         long start = System.currentTimeMillis();
 ​
         for (int i = 0; i < 10000000; i++) {
             createInstance();
         }
 ​
         System.out.println("花费时间:"+(System.currentTimeMillis()-start)+"ms");
 ​
         try {
             TimeUnit.SECONDS.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }
 }
 //-Xmx200m -Xms200m -XX:+PrintGCDetails 
 //-XX:+DoEscapeAnalysis 设置开启逃逸分析
 //-XX:-EliminateAllocations 设置不开启标量替换 
 //开启逃逸分析 + 关闭标量替换 : 花费时间:93ms
 //开启逃逸分析 + 开启标量替换  : 花费时间:6ms

热点代码与热点探测

JIT编译器并不是编译所有的字节码,JIT编译器只编译热点代码

热点代码: 被多次调用的方法 或 方法中多次循环的循环体

栈上替换(OSR): JIT将方法中的热点代码编译为本地机器指令(被多次执行的循环体)

编译对象都是方法,如果是栈上替换则"入口"在方法的循环体开始那里

热点探测功能决定了被调用多少次的方法能成为热点代码

hotspot采用基于计数器的热点探测

  • 方法调用计数器 : 统计方法调用次数
  • 回边计数器 : 统计循环体执行循环次数

方法调用时先判断是否有执行编译后的机器码,有则直接使用方法区的Code cache中的机器码;没有机器码则判断计数器次数是否超过阈值,超过则触发编译,编译后机器码存储在方法区Code cache中使用;最后都没有就使用解释执行

总结

本篇文章将围绕执行引擎,深入浅出的解析执行引擎中的解释器、即时编译器各自执行的优缺点以及原理

执行引擎由解释器、即时编译器、垃圾收集器构成,默认情况下使用解释器与编译器的混合方式执行

即时编译器分为C1、C2编译器,其中C1编译快但优化小,C2开启逃逸分析使用栈上分配、锁消除、标量替换进行优化,编译耗时但是优化大

即时编译器并不是所有代码都编译,而是使用方法技术和循环计数来将热点代码编译成机器码存放在方法区的Code Cache中

在混合执行的模式下,解释器、C1、C2编译器同时工作,分层编译

最后

  • 参考资料
  • 《深入理解Java虚拟机》
  • 部分图片来源于网络

本篇文章将被收入JVM专栏,觉得不错感兴趣的同学可以收藏专栏哟~

觉得菜菜写的不错,可以点赞、关注支持哟~

有什么问题可以在评论区交流喔~


相关文章
|
4月前
|
存储 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
61 0
|
17天前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
25 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
3月前
|
存储 算法 Java
JVM组成结构详解:类加载、运行时数据区、执行引擎与垃圾收集器的协同工作
【8月更文挑战第25天】Java虚拟机(JVM)是Java平台的核心,它使Java程序能在任何支持JVM的平台上运行。JVM包含复杂的结构,如类加载子系统、运行时数据区、执行引擎、本地库接口和垃圾收集器。例如,当运行含有第三方库的程序时,类加载子系统会加载必要的.class文件;运行时数据区管理程序数据,如对象实例存储在堆中;执行引擎执行字节码;本地库接口允许Java调用本地应用程序;垃圾收集器则负责清理不再使用的对象,防止内存泄漏。这些组件协同工作,确保了Java程序的高效运行。
22 3
|
3月前
|
C# UED 开发者
WPF动画大揭秘:掌握动画技巧,让你的界面动起来,告别枯燥与乏味!
【8月更文挑战第31天】在WPF应用开发中,动画能显著提升用户体验,使其更加生动有趣。本文将介绍WPF动画的基础知识和实现方法,包括平移、缩放、旋转等常见类型,并通过示例代码展示如何使用`DoubleAnimation`创建平移动画。此外,还将介绍动画触发器的使用,帮助开发者更好地控制动画效果,提升应用的吸引力。
124 0
|
4月前
|
存储 缓存 自然语言处理
(三)JVM成神路之全面详解执行引擎子系统、JIT即时编译原理与分派实现
执行引擎子系统是JVM的重要组成部分之一,在JVM系列的开篇曾提到:JVM是一个架构在平台上的平台,虚拟机是一个相似于“物理机”的概念,与物理机一样,都具备代码执行的能力。
|
4月前
|
存储 安全 Java
Java面试题:在JVM中,堆和栈有什么区别?请详细解释说明,要深入到底层知识
Java面试题:在JVM中,堆和栈有什么区别?请详细解释说明,要深入到底层知识
73 3
|
4月前
|
算法 Java
Java面试题:列举并解释JVM中常见的垃圾收集器,并比较它们的优缺点
Java面试题:列举并解释JVM中常见的垃圾收集器,并比较它们的优缺点
81 3
|
5月前
|
Java 编译器
Java健壮性 Java可移植性 JDK, JRE, JVM三者关系 Java的加载与执行原理 javac编译与JAVA_HOME环境变量介绍 Java中的注释与缩进 main方法的args参数
Java健壮性 Java可移植性 JDK, JRE, JVM三者关系 Java的加载与执行原理 javac编译与JAVA_HOME环境变量介绍 Java中的注释与缩进 main方法的args参数
49 1
|
4月前
|
Java 编译器 C++
Java演进问题之HotSpot JVM中的分层编译策略如何解决
Java演进问题之HotSpot JVM中的分层编译策略如何解决
|
4月前
|
算法 安全 Java
Java面试题:解释JVM中的堆内存分代收集策略,并讨论年轻代和老年代的特点,描述Java中的线程池,并解释线程池的优点,解释Java中的`volatile`关键字的作用和使用场景
Java面试题:解释JVM中的堆内存分代收集策略,并讨论年轻代和老年代的特点,描述Java中的线程池,并解释线程池的优点,解释Java中的`volatile`关键字的作用和使用场景
44 0