JVMの静、动态绑定异常捕获JIT即时编译

        在说明静态绑定和动态绑定之前,我们首先要了解在字节码指令的层面,JVM是如何调用方法的:

        例如我有以下的代码,很简单就是在main方法中调用了另一个静态方法

public class MethodTest {
    public static void main(String[] args) {
        study();
    }

    private static void study() {
        System.out.println("... study");
    }
}

        编译成的字节码文件如下:

        关键在于invokestatic 这个字节码指令,它的作用就是调用一个静态方法,与此类似的还有:

  • invokevirtual:用于调用实例方法
  • invokespecial:用于调用私有方法构造方法,通过super关键字调用父类的构造方法
  • invokeinterface:调用接口方法
  • invokedynamic:用于调用动态方法

1、静态绑定

        静态绑定也称为早期绑定(Early Binding)。在编译时就决定了要调用的方法,通常会发生在调用静态方法,私有方法,final方法时

        这三种方法都有一个共同点,那就是在编译期间就能被确定。

        在上面的案例中,我们可以看到,编译成的字节码文件中,调用方法的关键字后跟上了一个#符号引用,它指向常量池中的方法定义:

        静态绑定的情况下,符号引用是在第一次方法被调用时,替换成为直接引用。JVM会从常量池中找到编号为2的项,根据这些信息定位到实际的类和方法,将符号引用替换为直接引用。

        JVM在加载类时并不会立即解析所有符号引用。相反,符号引用的解析通常发生在第一次实际使用这些引用的时候。这种机制称为延迟解析。

        例如案例中study方法的解析过程:

  1. 首先会找到常量池中#2的元素。
  2. 然后会把MethodTest类加载到内存中。
  3. 通过符号引用中的方法名和描述符,定位到study方法。
  4. 将符号引用#2替换成直接指向study方法的引用。

2、动态绑定

        动态绑定也称为后期绑定(Late Binding)或运行时绑定(Runtime Binding)。在运行时决定调用哪个方法。

        最常见的场景是为了支持多态:当一个父类的引用变量引用子类的对象时,调用重写的方法时会在运行时决定调用子类的实现,通常发生在invokevirtualinvokeinterface字节码指令中。

        而虚方法表又是实现多态的一种方式,什么是虚方法表

        在前面的文章中提到过,类在加载阶段JVM会将读取到的字节码信息保存到内存的方法区中,生成一个InstanceKlass对象,InstanceKlass对象 中就包含了虚方法表。


           例如我现在有A一直到G这么多类,每个类的父类都是上一个类。如果G类需要调用A类中的方法,难道会从G一直找到A?答案是否定的。

        每个类中都有一个虚方法表,记录了类中的每个方法以及方法的地址:

        如果子类继承了父类,会先复制一份父类的虚方法表,然后加上自己特有的方法,如果重写了父类中的某个方法,就会在自己类的虚方法表中将父类中被重写方法的地址指向本类。(这就是为什么子类重写了父类的方法,以子类的该方法为准。)

        动态绑定时,字节码指令执行的流程是,首先根据每个对象头中的元数据指针,去找到方法区中的InstanceKlass对象,然后根据虚方法表获得对应方法的地址,最后调用方法。

        下面我们通过一个案例说明一下动态绑定的执行流程:

class Animal {
    public void makeSound() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.makeSound();  // 动态绑定
    }
}

        当a.makeSound();执行时,会执行以下的操作:

  1. 编译阶段:a是animal类型的引用,但是指向了Dog实例,使用invokevirtual 字节码指令去调用makeSound方法,但是此时不会去决定调用哪个实例的makeSound方法。并且此时是符号引用
  2. 加载和连接阶段:JVM会加载Animal和Dog类,并进行连接,此时常量池中的符号引用被替换成了直接引用 ,但是还没有动态绑定。
  3. 运行时阶段:当运行到a.makeSound()时,会真正的执行invokevirtual 字节码指令,JVM会发现a的实际类型是Dog实例。会去Dog类的虚方法表查找makeSound方法。因为Dog重写了父类的makeSound方法,makeSound方法实际指向了Dog类。
  4. 执行方法:JVM使用方法表中的直接引用调用Dog类的makeSound方法。

        Animal和Dog的虚方法表:

Animal VTable:
+-----------------+
| makeSound() ->  |  --> 指向 Animal::makeSound
+-----------------+

Dog VTable:
+-----------------+
| makeSound() ->  |  --> 指向 Dog::makeSound
+-----------------+

        虽然在连接阶段中的解析这一步会把符号引用替换成直接引用,但是还没有进行动态分派。需要在运行时根据对象实际的类型查询虚方法表。

3、异常捕获处理

        在之前的文章中,有提到过每个方法都有其异常表,例如有一个方法:

public class ExceptionTest {
    public static void main(String[] args) {
        try {
            int i = 0;
        }catch (Exception e){
            int i = 2;
        }
    }
}

        它的字节码指令:

0 iconst_0
1 istore_1
2 goto 8 (+6)
5 astore_1
6 iconst_2
7 istore_2
8 return

        对应的异常表:

        这个异常表中的起始PC结束PC,表示捕获异常生效的字节码起始和结束位置。

        跳转PC指的是出现异常并被捕获后,跳转到字节码指令的位置。

        如果有多个catch分支捕获不同的异常呢?

public class ExceptionTest {
    public static void main(String[] args) {
        try {
            int i = 0;
        }catch (ClassCastException e){
            int i = 2;
        }catch (NullPointerException ex){
            int i= 3;
        }
    }
}

        在异常表的层面会从上往下遍历,如果出现的异常与第一个不匹配,就会去查找第二个,第三个...

        Finally代码块的处理,Finally代表无论是否出现异常,最终一定会执行的代码,那么在字节码的层面是如何进行处理的?

public class ExceptionTest {
    public static void main(String[] args) {
        try {
            int i = 0;
        }catch (ClassCastException e){
            int i = 2;
        }finally {
            int i = 10;
        }
    }
}

         首先看一下编译后的异常表:

        会发现除了catch中的ClassCastException,还多了两个any捕获类型,表示捕获所有类型的异常。

        Nr.1的any,其实对应的就是try块中的代码,Nr.2的any,对应的是catch块中的代码。

        实际上是把finally中的逻辑插入到了try和catch代码块中。

4、JIT即时编译

        JIT(Just-In-Time)即时编译器是一种在程序运行时将Java字节码动态编译为机器码的技术,以提高程序的执行效率。

        第一篇中提到过,Java语言支持跨平台特性的实现在于,Java程序在开发完成后会被编译成字节码,然后JVM会将字节码转换为具体平台的机器码进行执行。

        如果有一些代码的执行频率较高,这样的代码会被称之为热点代码,会被JIT即时编译器编译成机器码的同时进行优化,保存在内存中。

        在JVM中,一般有两种即时编译器:

  • C1:适用于需要快速启动时间的应用,如桌面应用。它在做简单优化的同时,能快速完成编译。
  • C2:适用于长时间运行的服务器端应用。它进行更多、更复杂的优化,以获得最佳性能。

        通常情况下C1和C2不会单独工作,而是会协同进行,这就引出了分层编译机制

        4.1、分层编译

        分层编译是JVM中一个重要的优化策略,它结合了C1和C2编译器的优点,既能实现快速启动,又能在长时间运行时提供高效的优化:

  1. Tier 0 - 解释执行:JVM启动时,所有方法最初都是通过解释器执行的。这允许应用程序快速启动,因为解释执行不需要任何编译时间。
  2. Tier 1 - 简单编译(C1 without profiling):当一个方法被调用多次,达到一定的阈值时,JVM会使用C1编译器对其进行简单的编译。这种编译会生成未经复杂优化的机器码,但执行速度比解释执行要快。
  3. Tier 2 - 带性能分析的编译(C1 with profiling):在这个层次上,C1编译器不仅进行编译,还会在生成的机器码中插入性能分析代码(profiling code)。这些性能分析代码会收集运行时数据,例如方法调用频率、分支预测信息和类型分布等。这些数据将用于后续更高级别的优化。
  4. Tier 3 - 更高级的编译(C1 with more profiling):这一层次进一步加强性能分析,同时进行更多的中等优化。
  5. Tier 4 - 高级编译(C2):当方法经过充分的性能分析并被标记为热点方法时,JVM会使用C2编译器对其进行高级编译。C2编译器会利用收集到的性能数据进行深入的优化,包括内联、循环优化和逃逸分析等。

        由此可见在分层编译时,JVM会优先使用C1编译器为C2编译器收集信息,协同C2编译器进行编译。C1和C2一般都是用独立的线程进行处理,线程中存有队列存放需要编译的任务。

        那么C1和C2是如何协同工作的?

  1. 启动阶段:JVM启动时,所有方法通过解释器执行(Tier 0)。这保证了应用程序能够快速启动。
  2. 热点探测:JVM通过计数器机制监控方法的执行频率。当某个方法调用次数达到Tier 1的阈值时,C1编译器介入,对该方法进行简单编译。
  3. 性能分析和优化:在Tier 2和Tier 3层次上,C1编译器插入性能分析代码,收集运行时的性能数据。JVM根据这些数据判断哪些方法应该进一步优化,并在合适的时候使用C2编译器对热点方法进行高级编译。
  4. 持续优化:C2编译器对方法进行高级优化,生成高效的机器码。C2编译的机器码会替换之前C1编译的机器码或解释执行的代码。如果运行时情况发生变化,例如方法的调用频率下降或性能特征改变,JVM可以重新调整编译策略,可能会回退到C1编译,甚至返回解释执行。(称之为取消优化
        4.2、方法内联

        方法内联是指在编译时,将被调用的方法的代码直接插入到调用点,而不是在运行时进行方法调用。这样做可以避免参数传递,接受返回值,创建栈帧等。

        例如我有以下的代码:

public int add(int a, int b) {
    return a + b;
}

public int calculate() {
    int x = 10;
    int y = 20;
    return add(x, y);
}

        在没有进行内联时,调用add方法,会产生一个新的栈帧,并且需要传递参数,得到返回的结果。

        而通过内联,会得到如下的效果:

public int calculate() {
    int x = 10;
    int y = 20;
    return x + y;  // add(x, y) 的内联结果
}

        通常内联后还会进行一次常量折叠(因为案例中x和y的值是在编译时就能确定,不会动态发生变更):

public int calculate() {
    return 30;  // 常量折叠后的代码
}

        通过上面的简单案例,我们对于什么是方法内联有了一定的认识,下面总结一下方法内联的过程:

  • JIT编译器会根据一定的标准来识别哪些方法适合进行内联。这些标准包括方法的大小、调用频率、编译层次(如C1或C2编译器)和方法的特性(如是否为虚方法)。
  • 一旦确定某个方法可以内联,JIT编译器会将该方法的字节码直接插入到调用点,替代原来的方法调用指令。(在代码层面,就如同上面案例将return add(x, y) 替换成return x + y)
  • 在插入内联方法的代码后,JIT编译器会进行进一步的优化,例如常量折叠、消除无用代码和循环展开等,以最大限度地提高执行效率。

        而需要实现方法内联,也要满足一定的条件,首先是方法的大小:IT编译器通常会设置一个方法大小的阈值,超过这个阈值的方法将不会被内联。其次是调用频率 ,经常被调用的方法更有可能被内联。以及访问修饰符 ,一般被private,final,static修饰的方法更容易被内联,因为它们的调用行为是确定的,不会被子类重写或动态绑定。(这一条不由得让我想到了曾经看到的八股文中final关键字的作用,其中就有一条被final修饰的常量会被虚拟机内联提高效率)

        4.3、逃逸性分析

        逃逸性分析(Escape Analysis)是JVM(Java虚拟机)JIT(Just-In-Time)编译器用来优化内存分配和垃圾回收的重要技术。通过分析对象的动态作用域,JVM可以确定哪些对象不会“逃逸”出其创建的方法线程,从而进行进一步优化,如栈上分配和同步消除。

        根据是否发生逃逸,及逃逸的范围,一般会将对象划分为以下的种类:

        不逃逸:对象完全在创建它的方法内部使用,未被返回或传递给外部。

        对于不逃逸的对象,JVM可以在栈上分配内存,而不是堆上。栈上分配的对象随着方法结束自动销毁,无需垃圾回收。

public class EscapeAnalysisDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Test test = new Test();
            System.out.println(test);
        }
    }
}

        方法逃逸:对象作为返回值或参数传递给外部方法,但不逃逸出创建它的线程。        

public class EscapeAnalysisDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Test test = new Test();
            method1(test);
        }
    }
}

        线程逃逸:对象被其他线程访问,通常通过共享变量或线程间通信传递。

public class EscapeAnalysisDemo {
    public static void main(String[] args) {
        Test test = new Test();
        new Thread(()->{
            System.out.println(test);
        },"t1");
    }
}

        我们再通过另一个案例详细看下逃逸性分析的过程以及JVM做出的优化

public class Example {
    public static class Point {
        int x, y;
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    public void calculate() {
        Point p = new Point(10, 20);
        System.out.println(p.x + p.y);
    }

    public static void main(String[] args) {
        Example example = new Example();
        example.calculate();
    }
}

        JVM的JIT编译器会分析Point对象的逃逸性:

        Point对象是在calculate()方法中被创建,p.x和p.y也都是发生在calculate()方法中的,Point对象没有传递给其他方法,也不会返回给其他方法。

        4.3.1、栈上分配:

        由于Point对象不逃逸,JVM可以选择在栈上分配Point的内存,而不是在堆上。这样,当 calculate()方法结束时,Point对象的内存会自动释放,无需垃圾回收。

        4.3.2、同步消除

        假设我们在calculate()方法中使用了同步代码块:

    public void calculate() {
        synchronized(new Point(10, 20)){
            System.out.println(p.x + p.y);
        }
    }

        同步块可以被消除,因为没有其他线程会访问Point对象。

        4.3.3、标量替换

        JVM可以将Point对象分解为两个局部变量X和Y,避免对象的创建

public void calculate() {
    int x = 10;
    int y = 20;
    int sum = x + y;
    System.out.println(sum);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/685246.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

小程序 UI 风格美不胜收

小程序 UI 风格美不胜收 小程序 UI 风格美不胜收

论文阅读《SELECTIVE DOMAIN-INVARIANT FEATURE FOR GENERALIZABLE DEEPFAKEDETECTION》

作者&#xff1a;Yingxin Lai、 Guoqing Yang1、Yifan He2、Zhiming Luo、Shaozi Li 期刊&#xff1a;ICASSP-2024 目的&#xff1a;解决泛化性的问题&#xff0c;提出了3个模块 论文整体的架构图&#xff1a;&#xff08;挑选域特征不变&#xff0c;减少对图像内容或者风格…

完全平方数

完全平方数 完全平方数动态规划 完全平方数 给你一个整数 n &#xff0c;返回 和为 n 的完全平方数的最少数量 。 完全平方数 是一个整数&#xff0c;其值等于另一个整数的平方&#xff1b;换句话说&#xff0c;其值等于一个整数自乘的积。例如&#xff0c;1、4、9 和 16 都是…

289M→259M得物包体积治理实践

一、前言 iOS应用的包体积大小是衡量得物性能的重要指标&#xff0c;过大包体积会降低用户对应用的下载意愿&#xff0c;还会增加用户的下载等待时间以及用户手机的存储空间&#xff0c;本文重点介绍在包体积治理中的新思路以及原理与实践。 二、原理介绍 Macho产物测试 我…

想要修改word文档怎么移除编辑权限?学会这两个方法,轻松搞定

日常办公和学习中&#xff0c;Word文档是我们不可或缺的工具。然而&#xff0c;有时我们可能会遇到一些设置了编辑权限的文档&#xff0c;这可能是由于文档的创建者希望控制文档的修改和传播&#xff0c;或者是因为文档在某些共享或协作环境中被设置为只读模式。在这种情况下&a…

网工内推 | 网络运维工程师,H3CIE认证优先,13薪,享股票期权

01 畅读 &#x1f537;招聘岗位&#xff1a;高级网络运维工程师 &#x1f537;职责描述&#xff1a; 1.负责线上业务网络技术运维工作&#xff0c;保障并优化线上网络质量&#xff1b; 2.规划并构建公司线上业务网络架构&#xff1b; 3.规划线上业务网络质量评估与监控体系&…

mysql中 redo日志(上)

大家好。我们知道InnoDB 存储引擎是以页为单位来管理存储空间的&#xff0c;我们进行的增删改查操作其实本质上都是在访问页面。而在真正访问页面之前&#xff0c;需要把在磁盘上的页缓存到内存中的Buffer Pool之后才可以访问。那么我们思考一个问题&#xff1a;如果我们只在内…

vue2中使用tinymce

vue2中使用tinymce的记录 本篇文章主要实现的功能&#xff1a; &#xff08;1&#xff09;【查看】时禁用编辑 &#xff08;2&#xff09;【编辑】时某些内容是不可编辑的 &#xff08;3&#xff09;【内容】前端拼接编辑器模板 &#xff08;4&#xff09;【内容】编辑器模板中…

【漏洞复现】锐捷校园网自助服务系统 login_judge.jsf 任意文件读取漏洞(XVE-2024-2116)

0x01 产品简介 锐捷校园网自助服务系统是锐捷网络推出的一款面向学校和校园网络管理的解决方案。该系统旨在提供便捷的网络自助服务&#xff0c;使学生、教职员工和网络管理员能够更好地管理和利用校园网络资源。 0x02 漏洞概述 校园网自助服务系统/selfservice/selfservice…

Java核心: 为图片生成水印

今天干了一件特别不务正业的事&#xff0c;做了一个小程序用来给图片添加水印。事情的起因是需要将自己的身份证照片分享给别人&#xff0c;手边并没有一个趁手的工具来生成图片水印。很多APP提供了水印的功能&#xff0c;但会把我的图片上传到他们的服务器&#xff0c;身份证太…

台式机安装Windows 11和Ubuntu 22双系统引导问题

一、基本情况 1.1、硬件情况 电脑有2个NVMe固态硬盘&#xff0c;1个SATA固态硬盘&#xff0c;1个机械硬盘。其中一个NVMe固态硬盘是Windows系统盘&#xff0c;另一个NVMe固态为Windows软件和文件盘&#xff0c;SATA固态硬盘为Ubuntu专用&#xff0c;机械硬盘为数据备份盘。 …

Find My电动螺丝刀|苹果Find My技术与螺丝刀结合,智能防丢,全球定位

电动螺丝刀&#xff0c;别名电批、电动起子&#xff0c;是用于拧紧和旋松螺钉用的电动工具。它不仅提高了工作效率&#xff0c;还大大减轻了工作者的体力负担。在装配线等生产环境中&#xff0c;电动螺丝刀已经成为了不可或缺的工具。电动螺丝刀的批头还具备接地防静电功能&…

Leetcode:四数之和

题目链接&#xff1a;18. 四数之和 - 力扣&#xff08;LeetCode&#xff09; 普通版本&#xff08;排序 双指针&#xff09; 主旨&#xff1a;类似于三数之和的解法&#xff0c;但需要多加一些限制&#xff0c;同时为了防止多个数组元素的相加之和出现整型溢出问题还要将整型…

IDEA 2022

介绍 【尚硅谷IDEA安装idea实战教程&#xff08;百万播放&#xff0c;新版来袭&#xff09;】 jetbrains 中文官网 IDEA 官网 IDEA 从 IDEA 2022.1 版本开始支持 JDK 17&#xff0c;也就是说如果想要使用 JDK 17&#xff0c;那么就要下载 IDEA 2022.1 或之后的版本。 公司…

《TCP/IP网络编程》(第十三章)多种I/O函数(2)

使用readv和writev函数可以提高数据通信的效率&#xff0c;它们的功能可以概括为**“对数据进行整合传输及发送”**。 即使用writev函数可以将分散在多个缓冲中的数据一并发送&#xff0c;使用readv函数可以由多个缓冲分别接受&#xff0c;所以适当使用他们可以减少I/O函数的调…

Refused to load the stylesheet问题解决方案

今天项目部署的过程中遇到一个安全策略问题的报错&#xff0c;大概意思就是处于安全考虑&#xff0c;不允许src外链其他不安全的静态文件 解决这种问题的一个思路大概就是找到index.html文件先看下是否存在 <meta http-equiv"Content-Security-Policy" content&…

用PlayCanvas打造一个令人惊叹的3D图在线展示

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 PlayCanvas实例化渲染&#xff1a;大规模渲染优化 应用场景 在游戏开发中&#xff0c;经常需要渲染大量相同或相似模型。传统方法需要为每个模型创建单独的渲染对象&#xff0c;这会消耗大量内存和GPU资源。实…

问你为什么选择Kafka,你会怎么回答?

可靠的含义在百度百科的解释是&#xff1a;可以信赖、可以相信、可靠的朋友。那Kafka究竟是不是一个可靠的朋友呢&#xff1f;既然全世界绝大部分高可用系统都有Kafka的支持&#xff0c;Kafka必定有其过人之处&#xff0c;跟着我来分析分析。 另外多提一嘴Kafka在GitHub目前已…

【AIGC X UML 落地】通过多智能体实现自然语言绘制UML图

前天写了篇博文讲到用PlantUML来绘制C类图和流程图。后台有读者留言&#xff0c;问这步能否自动化生成&#xff0c;不想学习 PlantUML 语法。 我想了下&#xff0c;发现这事可行&#xff0c;确实可以做到通过自然语言的描述就能实现 UML图的绘制&#xff0c;昨天晚上加了个班到…

安装TPMmanager

sudo apt-get install qt4-qmake sudo apt-get install libqt4-dev下载TPMManager&#xff0c;解压之后拖入Ubuntu&#xff0c;进入目录 https://gitcode.com/Rohde-Schwarz/TPMManager/overview?utm_sourcecsdn_github_accelerator&isLogin1 cd tpmmanager-master qmake…