编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 8.1彻底搞透彻内存模型
  • 8.2JVM内存回收清理机制
  • 8.3JVM监控和故障处理
  • 8.4JVM如何执行方法调用
  • 8.5JVM如何实现invoked
  • 8.6JVM性能调优的策略
  • 8.7内存上升排查和优化
  • 8.8JVM即时编译器原理
  • 8.9JVM性能如何调优实践

8.8JVM即时编译器原理

目录介绍

  • 01.Java编译基础概念
    • 1.1 什么叫做前端编译
    • 1.2 什么是运行时编译
    • 1.3 运行时编译步骤
  • 02.JIT编译器工作原理
    • 2.1 类编译到运行全流程
    • 2.2 类编译
    • 2.3 类加载过程
    • 2.4 类连接
    • 2.5 类初始化
  • 03.即时编译工作原理
    • 3.1 什么是即时编译
    • 3.2 即时编译器类型
    • 3.3 热点探测原理
  • 04.JIT编译优化技术
    • 4.1 方法内联
    • 4.2 逃逸分析
    • 4.3 逃逸分析之锁消除
    • 4.4 逃逸分析之标量替换

01.Java编译基础概念

1.1 什么叫做前端编译

  • 前端编译通常指的是将Java源代码(.java文件)编译为Java字节码(.class文件)的过程。
    • 这个过程由Java编译器(javac)完成。
    • 在前端编译阶段,Java编译器将源代码转换为中间表示形式,即Java字节码。这个过程主要涉及语法检查、语义分析和生成字节码等步骤,提前发现一些潜在的错误。
    • 前端编译的主要目的是将高级的Java源代码转换为与平台无关的中间表示形式,即Java字节码。Java字节码是一种与特定硬件平台无关的二进制格式,它可以在Java虚拟机(JVM)上运行。
  • 在前端编译完成后,Java字节码可以被传递给Java虚拟机执行。
    • Java虚拟机将字节码解释或即时编译为特定平台的本地机器码,然后在运行时执行。

1.2 什么是运行时编译

  • 运行时编译(Just-In-Time Compilation,JIT编译)是指在程序运行过程中将Java字节码(.class文件)动态地编译为本地机器码的过程。
    • 这个过程由Java虚拟机(JVM)的即时编译器完成。
    • 由于机器无法直接运行 Java 生成的字节码,所以在运行时,JIT 或解释器会将字节码转换成机器码,这个过程就叫运行时编译。
    • Java虚拟机在运行Java程序时,通常会将字节码解释执行。为了提高执行效率,JVM还可以使用即时编译器将热点代码(HotSpot)编译为本地机器码,以便直接在底层硬件上执行。
  • 运行时编译的主要目的是通过将热点代码编译为本地机器码来提高程序的执行速度。
    • 热点代码是指在程序运行过程中频繁执行的代码段,通常是由循环、频繁调用的方法或经过优化的代码块组成。

1.3 运行时编译步骤

  • 运行时编译的过程通常包括以下几个步骤:
    • 热点代码识别:JVM通过收集程序运行时的性能数据,识别出热点代码。
    • 即时编译:热点代码被即时编译器编译为本地机器码。
    • 本地机器码执行:编译后的本地机器码直接在底层硬件上执行,提高了执行速度。

02.JIT编译器工作原理

2.1 类编译到运行全流程

  • 先了解下 Java 从编译到运行的整个过程,为后面的学习打下基础。请看下图:
    • image
      image

2.2 类编译

  • 在编写好代码之后,我们需要将 .java 文件编译成 .class 文件,才能在虚拟机上正常运行代码。
    • 文件的编译通常是由 JDK 中自带的 Javac 工具完成,一个简单的 .java 文件,我们可以通过 javac 命令来生成 .class 文件。
    • 通过 javap 反编译来看看一个 class 文件结构中主要包含了哪些信息:
    javac TestClass.java     //先运行编译class文件命令
    javap -v TestClass.class       //再通过javap打印出字节文件
  • 通过 javap 查看字节文件信息如下所示
    Classfile /Users/yc/github/YCJavaBlog/day8/TestClass.class
      Last modified 2024-6-25; size 947 bytes
      //魔数,这个是md5
      MD5 checksum 6cea821db1fe6d5b1b1aea403a6e5917
      Compiled from "TestClass.java"
    public class day8.TestClass
      //版本号,
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
      //常量池
    Constant pool:
       #1 = Methodref          #13.#30        // java/lang/Object."<init>":()V
       #2 = Class              #31            // java/lang/StringBuilder
       #3 = Methodref          #2.#30         // java/lang/StringBuilder."<init>":()V
       #4 = Methodref          #2.#32         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       #5 = Class              #33            // day8/TestClass
       #6 = String             #34            // yc
       #7 = Methodref          #2.#35         // java/lang/StringBuilder.toString:()Ljava/lang/String;
       #8 = Fieldref           #5.#36         // day8/TestClass.number:I
       #9 = Methodref          #37.#38        // java/lang/String.valueOf:(I)Ljava/lang/String;
      #10 = Fieldref           #5.#39         // day8/TestClass.str2:Ljava/lang/String;
      #11 = Fieldref           #40.#41        // java/lang/System.out:Ljava/io/PrintStream;
      #12 = Methodref          #42.#43        // java/io/PrintStream.println:(Ljava/lang/String;)V
      #13 = Class              #44            // java/lang/Object
      #14 = Utf8               str1
      #15 = Utf8               Ljava/lang/String;
      #16 = Utf8               ConstantValue
      #17 = Utf8               str2
      #18 = Utf8               number
      #19 = Utf8               I
      #20 = Utf8               <init>
      #21 = Utf8               ()V
      #22 = Utf8               Code
      #23 = Utf8               LineNumberTable
      #24 = Utf8               splice
      #25 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
      #26 = Utf8               print
      #27 = Utf8               <clinit>
      #28 = Utf8               SourceFile
      #29 = Utf8               TestClass.java
      #30 = NameAndType        #20:#21        // "<init>":()V
      #31 = Utf8               java/lang/StringBuilder
      #32 = NameAndType        #45:#46        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      #33 = Utf8               day8/TestClass
      #34 = Utf8               yc
      #35 = NameAndType        #47:#48        // toString:()Ljava/lang/String;
      #36 = NameAndType        #18:#19        // number:I
      #37 = Class              #49            // java/lang/String
      #38 = NameAndType        #50:#51        // valueOf:(I)Ljava/lang/String;
      #39 = NameAndType        #17:#15        // str2:Ljava/lang/String;
      #40 = Class              #52            // java/lang/System
      #41 = NameAndType        #53:#54        // out:Ljava/io/PrintStream;
      #42 = Class              #55            // java/io/PrintStream
      #43 = NameAndType        #56:#57        // println:(Ljava/lang/String;)V
      #44 = Utf8               java/lang/Object
      #45 = Utf8               append
      #46 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
      #47 = Utf8               toString
      #48 = Utf8               ()Ljava/lang/String;
      #49 = Utf8               java/lang/String
      #50 = Utf8               valueOf
      #51 = Utf8               (I)Ljava/lang/String;
      #52 = Utf8               java/lang/System
      #53 = Utf8               out
      #54 = Utf8               Ljava/io/PrintStream;
      #55 = Utf8               java/io/PrintStream
      #56 = Utf8               println
      #57 = Utf8               (Ljava/lang/String;)V
    {
      //字段表集合
      public day8.TestClass();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
    
      //方法表集合
      public java.lang.String splice(java.lang.String);
        descriptor: (Ljava/lang/String;)Ljava/lang/String;
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=2
             0: new           #2                  // class java/lang/StringBuilder
             3: dup
             4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
             7: aload_1
             8: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            11: ldc           #6                  // String yc
            13: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            16: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            19: areturn
          //属性表集合
          LineNumberTable:
            line 14: 0
    
      //方法表集合
      public void print();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=2, args_size=1
             0: getstatic     #8                  // Field number:I
             3: invokestatic  #9                  // Method java/lang/String.valueOf:(I)Ljava/lang/String;
             6: astore_1
             7: aload_0
             8: new           #2                  // class java/lang/StringBuilder
            11: dup
            12: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
            15: aload_1
            16: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            19: ldc           #6                  // String yc
            21: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            24: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            27: putfield      #10                 // Field str2:Ljava/lang/String;
            30: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
            33: new           #2                  // class java/lang/StringBuilder
            36: dup
            37: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
            40: ldc           #6                  // String yc
            42: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            45: aload_0
            46: getfield      #10                 // Field str2:Ljava/lang/String;
            49: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            52: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            55: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            58: return
          //属性表集合
          LineNumberTable:
            line 18: 0
            line 19: 7
            line 20: 30
            line 21: 58
    
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=1, locals=0, args_size=0
             0: bipush        10
             2: putstatic     #8                  // Field number:I
             5: return
          //属性表集合
          LineNumberTable:
            line 10: 0
            line 11: 5
    }
    SourceFile: "TestClass.java"
  • 一个简单的命令执行,前期编译的过程其实是非常复杂的。
    • 包括词法分析、填充符号表、注解处理、语义分析以及生成 class 文件,这个过程我们不用过多关注。
    • 只要从上代码编译文件中知道,编译后的字节码文件主要包括常量池和方法表集合这两部分就可以了。
  • 常量池主要记录的是类文件中出现的字面量以及符号引用。
    • 字面常量包括字符串常量(例如 String str=“abc”,其中"abc"就是常量),声明为 final 的属性以及一些基本类型(例如,范围在 -127-128 之间的整型)的属性。
    • 符号引用包括类和接口的全限定名、类引用、方法引用以及成员变量引用(例如 String str=“abc”,其中 str 就是成员变量引用)等。
  • 方法表集合中主要包含一些方法的的信息。
    • 字节码、方法访问权限(public、protect、prviate 等)、方法名索引(与常量池中的方法引用对应)、描述符索引、JVM 执行指令以及属性集合等。

2.3 类加载过程

  • 当一个类被创建实例或者被其它对象引用时,虚拟机在没有加载过该类的情况下,会通过类加载器将字节码文件加载到内存中。
    • 不同的实现类由不同的类加载器加载,JDK 中的本地方法类一般由根加载器(Bootstrp loader)加载进来,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现加载,而程序中的类文件则由系统加载器(AppClassLoader )实现加载。
    • 在类加载后,class 类文件中的常量池信息以及其它数据会被保存到 JVM 内存的方法区中。

2.4 类连接

  • 类在加载进来之后,会进行连接、初始化,最后才会被使用。
    • 在连接过程中,又包括验证、准备和解析三个部分。
  • 验证:
    • 验证类符合 Java 规范和 JVM 规范,在保证符合规范的前提下,避免危害虚拟机安全。
  • 准备:
    • 为类的静态变量分配内存,初始化为系统的初始值。对于 final static 修饰的变量,直接赋值为用户的定义值。
    • 例如,private final static int value=123,会在准备阶段分配内存,并初始化值为 123,而如果是 private static int value=123,这个阶段 value 的值仍然为 0。
  • 解析:将符号引用转为直接引用的过程。
    • 在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。类结构文件的常量池中存储了符号引用,包括类和接口的全限定名、类引用、方法引用以及成员变量引用等。
    • 如果要使用这些类和方法,就需要把它们转化为 JVM 可以直接获取的内存地址或指针,即直接引用。

2.5 类初始化

  • 类初始化阶段是类加载过程的最后阶段
    • 在这个阶段中,JVM 首先将执行构造器 方法,编译器会在将 .java 文件编译成 .class 文件时,收集所有类初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 () 方法。
  • 初始化类的静态变量和静态代码块为用户自定义的值,初始化的顺序和 Java 源码从上到下的顺序一致。例如:
    private static int i=1;
    static{
      i=0;
    }
    public static void main(String [] args){
      System.out.println(i);
    }
    
    //此时运行结果为:
    0
  • 再来看看以下代码:
    static{
      i=0;
    }
    private static int i=1;
    public static void main(String [] args){
      System.out.println(i);
    }
    //此时运行结果为:
    1
  • 子类初始化时会首先调用父类的 () 方法,再执行子类的 () 方法,运行以下代码:
    public class Parent{
      public static String parentStr= "parent static string";
      static{
        System.out.println("parent static fields");
        System.out.println(parentStr);
      }
      public Parent(){
        System.out.println("parent instance initialization");
     }
    }
    
    public class Sub extends Parent{
      public static String subStr= "sub static string";
      static{
        System.out.println("sub static fields");
        System.out.println(subStr);
      }
    
      public Sub(){
        System.out.println("sub instance initialization");
      }
    
      public static void main(String[] args){
        System.out.println("sub main");
        new Sub();
     }
    }
    
    //运行结果
    parent static fields
    parent static string
    sub static fields
    sub static string
    sub main
    parent instance initialization
    sub instance initialization
  • JVM 会保证 () 方法的线程安全,保证同一时间只有一个线程执行。
    • JVM 在初始化执行代码时,如果实例化一个新对象,会调用 方法对实例变量进行初始化,并执行对应的构造方法内的代码。
贡献者: yangchong211
上一篇
8.7内存上升排查和优化
下一篇
8.9JVM性能如何调优实践