JVM学习日志(三) Java代码执行流程

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 简述 Java代码执行流程

Java代码执行流程

  • java代码编译成为.class文件后,会生成对应的字节码指令,然后经过JVM识别后调用字节码执行引擎来将字节码指令转换成可供CPU执行的机器码(0101010),在这个过程中,有两个比较重要的组件:解释器,即时编译器

JVM执行引擎

  • 执行引擎是Java虚拟机四大组成部分中的一个核心组成(另外三个分别是类加载器子系统,运行数据区,垃圾回收器),java虚拟机的执行引擎主要是用来执行java字节码,JVM的执行引擎执行字节码通过两种解释器执行的,字节码解释器与模板解释器,运行过程中,可能会触发即时编译(JIT),涉及到几种即时编译器,下面将分别进行介绍

解释器

  • 解释器分为字节码解释器和模板解释器

字节码解释器

  • 字节码解释器是解释执行的,所谓的解释执行,就是将java字节码转换成C++代码,在将C++代码编译成本地代码(硬编码),之所以会转成C++代码,是因为HotSpot虚拟机(现目前虚拟机都指的是HotSpot虚拟机)是由C++代码编写的,所以Java字节码指令的底层实现都是由C++代码实现,执行字节码指令其实就是对应的C++代码,而执行C++代码之前会将C++代码编译成本地代码,然后再执行

  • 所以,字节码解释器在工作的时候,他是逐行代码进行解释执行,准确的说是逐条字节码指令解释执行,由于字节码指令操作对象是栈帧中的操作数栈(伴随着入栈,出栈操作),因此我们常说字节码解释器是"基于栈的字节码解释执行引擎"

  • 代码实例:

  • 有如下java代码

    public int add(){
         
        int a = 10;
        int b = 20;
        return a + b;
    }
    
  • 通过javap命令(或者Idea中的 jclasslib插件),生成字节码如下:

    public int add();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
            stack=2, local=3, args_size=1
                0:bipush    10
                2:istore_1
                3:bipush    20
                5:istore_2
                6:iload_1
                7:iload_2
                8:iadd
                9:ireturn
    
  • 由上面这段字节码可知,Java虚拟机会为这个add()方法分配深度为2的操作数栈和个数为3的局部变量表(Code属性中包含了操作数栈深度和局部变量表大小)

  • 为什么局部变量表的大小是三个呢,程序中明明只有a,b两个局部变量?

    • 是因为add()方法是非对象实例方法(方法)的局部变量表中索引为0的位置永远是this指针
    • 内存模型为

    image-20230404172233646.png

  • 上面的执行流程是这样:

    • 0:bipush 10
      • 将单字节的常量值(-128 - 127)推送到操作数栈顶,这里的10指的是参数,也就是将10压入操作数栈的栈顶
    • 2:istore_1
      • 将栈顶int型数值出栈,并存入局部变量表中索引为1的位置
    • 3:bipush 20
      • 将参数20推送到操作数据栈顶
    • 5:istore_2
      • 将栈顶int型数据值出栈并存入局部变量表中索引为2的位置
    • 6:iload_1
      • 从局部变量表中将索引为1的变量压入操作数栈中
    • 7:iload_2
      • 从局部变量表中将索引为2的变量压入操作数栈中
    • 8:iadd
      • 取出操作数栈中的数据并执行加法运算,并将返回值压入操作数栈中
    • 9:ireturn
    • 结束方法运行,并将栈顶整型元素返回给方法调用者
  • 通过上述代码执行过程的分析可以看出,字节码解释器的执行过程就是逐条执行字节码指令

    • 注意:上面所说的逐条字节码指令进行解释执行,是指java虚拟机规范中给出的一种模型,实际上java虚拟机并不会按部就班的根据字节码指令逐条执行,因为虚拟机会对字节码指令做一系列优化,(如:指令重排),而且现在的虚拟机都是解释执行和编译执行混合模式,执行时的优化程度会更大,所以这里说的只是一种概念

模板解释器

  • 模板解释器是编译执行的,他的执行过程是直接将java字节码编译成本地代码(硬编码),然后执行本地代码,相当于对于每一条字节码指令,都将他最后生成的本地代码给提交前写死了,JVM初始化的时候,就会直接将字节码指令对应的本地代码给加载到内存中,执行的时候直接执行对应的本地代码即可,这样就跳过了字节码转C++以及由C++编译成本低代码的过程,提高了执行速度。JVM通过def()函数为字节码指令创捷模板的,每个模板都会生成该指令对应的本地代码

三种运行模式

字节码解释器

  • 早期JVM带用的就是该模式,字节码解释器模式下,逐条指令执行,代码运行速度相对较慢,因为他的编译条件相对宽松,编译所需要的信息较少,因此编译速度快,可以通过虚拟机参数-Xint设置

模板解释器

  • 模板解释器执行速度很快,但是因为它需要将代码全部编译成本地代码,因此他便以需要收集的信息比较多,所以编译速度比较慢,可以通过虚拟机参数-Xcomp设置

字节码解释器 + 模板解释器

  • 可以通过虚拟机参数-Xmixed设置,通过上面的分析,大家会觉得,既然模板解释器执行速度快,那JVM肯定会次啊用模板解释器来执行,其实并不是,在说明原因之前,需要先提一个概念:热点代码

三种运行模式性能比较

  • 代码通过一段算术运算,分别设置参数:-Xint, -Xcomp, -Xmixed然后运行,可以看到-Xint耗时明显最多,所以他的执行性能最差,编译执行和混合执行模式在这段代码上性能差不多,如果当一个程序非常大的话,可能模板执行器模式耗时就会比混合模式更久些,因为编译需要的更长的时间

热点代码

热点代码

  • 顾名思义,热点代码就是经常执行的代码,虚拟机会判断一个方法或者一个代码块的运行特别频繁,就会将这些代码认定为热点代码,热点代码主要包含两类代码:被多次调用的方法,被多次执行的循环体,多次调用的方法很好理解,多次执行的循环体是指一个方法内如果存在一个循环体,虽然这个方法可能只是被执行一次或者寥寥几次,但是这个循环体却被多次执行,主要体现在循环次数,那么这个循环体也是热点代码

热点探测

  • 虚拟机是如何判断一段代码是不是热点代码呢,这就需要有一套热点探测的机制了,JVM主要有两种热点探测机制:
    • 基于采样的热点探测
      • 虚拟机定期检查各个线程的栈顶,如果发现某个方法频繁出入栈顶,就将该方法认定为热点代码
    • 基于计数器的热点探测
      • 为每个代码块(方法或者循环体)维护一个计数器,每执行一次代码快,计数器+1,当计数器值达到阈值时,就认定该代码块为热点代码,HotSpot采用的就是这种方式,Client编译器模式下,该阈值是1500;Server编译器模式下,该阈值是10000,也可以通过虚拟机参数-XX:CompileThreshold设定,不过热点代码的地位不是一直保持热度的,当该代码块一段时间没有执行或者执行次数无法达到阈值时,热度就会衰减一半,比如某个热点代码快的热点值是10000,热度衰减后就变成了5000

热点代码缓存

  • 热点代码缓存在方法区中的一块内存空间,这块内存空间叫Code Cache,可以通过命令java-client-XX: +PrintFlagsFinal -version | grep CodeCache 查看Code Cache 默认大小

    image-20230408135306324.png

  • 介绍了热点代码之后,再来说说现在的JVM为什么采用混合执行模式,试想一下,假如采用了模板解释器模式,不管三七二十一,直接对所有的代码都编译成硬编码,可能大部分代码都只是执行一次或者少数几次,这样编译全部代码就造成了性能浪费,而且对于现在动辄几百兆甚至几个G的应用,如果采用纯模板解释器,应用的启动就会很慢,可能启动后很长时间都无法使用,这也无法接受,因此采用混合模式,在应用启动后的前期,使用字节码解释器解释执行,当程序与逆行了一段时间之后,JVM就能够根据代码块的执行次数判断哪些代码属于热点代码,就会将热点代码博爱村在方法区,下次执行的时候就直接使用模板解释器编译执行,就无需在编译了,从而提升了性能,现在的虚拟机默认采用的混合模式,可以通过java -version 命令查看:

    image-20230408135807394.png

即时编译器

  • 前面介绍的模板解释器所执行的本地代码就是即时编译器生成的,因此所谓的混合运行模式(-Xmixed)其实就是字节码解释器和即时编译器混合使用的方式,所以当使用解释执行模式时(-Xint),则只使用了字节码解释器,即时编译器并没有用;当使用编译模式执行时,这时候即时编译器就会起到作用(编译后的本地代码给模板解释器使用)。常见的即时编译器主要有两钟:C1编译器(客户端编译器)和C2编译器(服务端编译器),当虚拟机采用Client模式工作时,使用的是C1即时编译器,采用Server模式工作室,使用的是C2即使编译器,默认为Server模式,见上文中java-version执行结果的截图,也可以使用-client和-server参数设置

C1编译器

  • Client模式下的即时编译器,他出发的条件相对宽松,需要收集的信息较少,他会对字节码在编译的时候进行浅程度的优化,因此相对于C2编译器,他的编译速度更快,但是执行速度相对较慢

C2编译器

  • Server模式下的即使编译器,触发的条件比较苛刻,他会对代码进行更深层次的优化,因此他的编译程度比较深,编译后的代码质量更高,随之带来的也是更高的编译耗时,但是他的执行速度更快

即时编译器触发条件

  • 上面其实已经在热点代码中介绍过了,当方法执行次数达到指定的阈值时,就会触发即时编译,同时也可以指定,即时编译器编译的其实就是热点代码,只有存在热点代码时,即时编译器才会起作用

  • 当虚拟机执行到一个方法的时候,会先判断该方法是不是已经编译过(判断是否存在给i方法对应的本地代码),如果存在,则直接执行该本地代码,如果不存在,才会将该方法的热带你计数器+1,然后判断是否达到设定的阈值,一旦达到了阈值,则会触发即时编译器进行编译,所以当一个代码块已淡出法国即时编译,那么以后每次执行他都是执行他被编译后的本地机器码,也即采用得是编译执行模式(-Xcomp)

    需要注意的是,当即时编译器进行编译时,执行引擎并不会等待其编译完成,而是直接使用字节码解释器解释执行,当即时编译器编译完成后,下一次在执行该方法的时候就会执行已经编译后的本地代码了,这样做的目的就是尽可能的提高执行效率,防止同步等待造成的性能浪费

即时编译器如何实现的

  • 如果触发了即时编译,那么就会产生一个即时编译任务,将这个任务放到队列里,然后由VM_THREAD线程(linux系统)从这个队列里取出任务执行,而这个VM_THREAD线程可能会有多个,可以通过命名java -client -XX:+PrintFlagsFinal -version | grep ClCompilerCount 查看有多少个线程(默认4个),可以通过参数-XX:CICompilerCount=xxx进行设置,从而对即时编译器进行调优

    image-20230408144331531.png

混合编译

  • 由于即时编译器编译本地代码需要占用程序运行时间,通常要编译出优化成都越高的代码,所花费的时间便会越长,而且想要编译出来优化成都更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释器执行阶段的速度也有影响,为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机在编译子系统中加入了分层编译的功能(即混合编译),混合编译根据编译器编译,优化的规模与耗时,划分出来不同的编译层次,其中包括:
    • 第0层:程序春节时执行,并且执行器不开启性能监控功能(Profiling)
    • 第1层:使用客户端编译器将字节码编译成为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能
    • 第2层:仍然使用客户端编译器执行,今开其方法及汇编次数统计等有限的性能监控功能
    • 第3层:仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转,虚方法调用版本等全部的统计信息
    • 第4层:使用服务端编译器将字节码编译为本地代码,相比客户端编译器,服务端编译器就会启用更多的编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化

javaC编译器与即时编译器

  • 本文所说的"编译"都是指即时编译器的编译过程,它与javaC将java代码编译成自己阿尔玛是完全不同的,java编译器是由java代码实现,是java虚拟机之外的编译器,又称前端编译器;二即时编译器完全就是在java虚拟机内部运行,由C++代码实现,又称为后端编译器,他主要是运行时将字节码编译成本地机器码,由于java成需要运行的话,需要先经过javaC将.java文件编译成为.class文件,这个过程在迅即外部进行,然后由执行引擎执行字节码,而执行引擎执行字节码主要是由解释器+即使编译器搭配进行,这个过程在虚拟机内部进行,基于此,我们常说的"java是半编译半解释型语言"。
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
60 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
31 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
15天前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
15天前
|
小程序 前端开发 算法
|
20天前
|
Java API 开发者
Java如何实现企业微信审批流程
大家好,我是V哥。本文分享如何在企业微信中实现审批流程,通过调用企业微信的开放API完成。主要内容包括获取Access Token、创建审批模板、发起审批流程和查询审批结果。提供了一个Java示例代码,帮助开发者快速上手。希望对你有帮助,关注V哥爱编程,编码路上同行。
|
21天前
|
人工智能 Oracle Java
解决 Java 打印日志吞异常堆栈的问题
前几天有同学找我查一个空指针问题,Java 打印日志时,异常堆栈信息被吞了,导致定位不到出问题的地方。
30 2
|
23天前
|
SQL IDE Java
入门Cloud Toolkit:简化你的Java应用开发与部署流程
【10月更文挑战第19天】作为一名长期从事Java开发的程序员,我一直致力于寻找能够简化日常开发工作的工具。在众多工具中,阿里巴巴推出的Cloud Toolkit引起了我的注意。这款免费的插件旨在帮助开发者更轻松地进行开发、测试及部署工作,尤其是在与云服务交互时表现尤为出色。本文将从个人的角度出发,介绍Cloud Toolkit的基本功能及其使用技巧,希望能帮助初学者快速上手这款实用工具。
18 1
|
30天前
|
前端开发 安全 Java
java发布公告的实现流程
构建一个Java公告发布系统涉及到前端界面设计、后端业务逻辑处理、数据库设计与交互、安全性保障等多个环节。通过采用现代的开发框架和最佳实践,可以高效地开发出既安全又易于维护的系统。随着需求的增长,系统还可以进一步扩展,比如增加评论功能、通知订阅、多语言支持等。
29 1
|
1月前
|
Java 应用服务中间件 程序员
JVM知识体系学习八:OOM的案例(承接上篇博文,可以作为面试中的案例)
这篇文章通过多个案例深入探讨了Java虚拟机(JVM)中的内存溢出问题,涵盖了堆内存、方法区、直接内存和栈内存溢出的原因、诊断方法和解决方案,并讨论了不同JDK版本垃圾回收器的变化。
29 4
|
1月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
43 3