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

简介: 简述 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是半编译半解释型语言"。
相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
目录
相关文章
|
监控 Java Unix
6个Java 工具,轻松分析定位 JVM 问题 !
本文介绍了如何使用 JDK 自带工具查看和分析 JVM 的运行情况。通过编写一段测试代码(启动 10 个死循环线程,分配大量内存),结合常用工具如 `jps`、`jinfo`、`jstat`、`jstack`、`jvisualvm` 和 `jcmd` 等,详细展示了 JVM 参数配置、内存使用、线程状态及 GC 情况的监控方法。同时指出了一些常见问题,例如参数设置错误导致的内存异常,并通过实例说明了如何排查和解决。最后附上了官方文档链接,方便进一步学习。
3330 4
|
监控 测试技术 Go
告别传统Log追踪!GOAT如何用HTTP接口重塑代码监控
本文介绍了GOAT(Golang Application Tracing)工具的使用方法,通过一个Echo问答服务实例,详细展示了代码埋点与追踪技术的应用。内容涵盖初始化配置、自动埋点、手动调整埋点、数据监控及清理埋点等核心功能。GOAT适用于灰度发布、功能验证、性能分析、Bug排查和代码重构等场景,助力Go项目质量保障与平稳发布。工具以轻量高效的特点,为开发团队提供数据支持,优化决策流程。
972 90
|
安全 Oracle Java
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
749 0
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
Java程序员必学:JVM架构完全解读
Java 虚拟机(JVM)是 Java 编程的核心,深入理解其架构对开发者意义重大。本文详细解读 JVM 架构,涵盖类加载器子系统、运行时数据区等核心组件,剖析类加载机制,包括加载阶段、双亲委派模型等内容。阐述内存管理原理,介绍垃圾回收算法与常见回收器,并结合案例讲解调优策略。还分享 JVM 性能瓶颈识别与调优方法,分析 Java 语言特性对性能的影响,给出数据结构选择、I/O 操作及并发同步处理的优化技巧,同时探讨 JVM 安全模型与错误处理机制,助力开发者提升编程能力与程序性能。
Java程序员必学:JVM架构完全解读
|
存储 运维 Kubernetes
Java启动参数JVM_OPTS="-Xms512m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError"
本文介绍了Java虚拟机(JVM)常用启动参数配置,包括设置初始堆内存(-Xms512m)、最大堆内存(-Xmx1024m)及内存溢出时生成堆转储文件(-XX:+HeapDumpOnOutOfMemoryError),用于性能调优与故障排查。
1364 0
|
监控 测试技术 开发者
一行代码改进:Logtail的多行日志采集性能提升7倍的奥秘
一个有趣的现象引起了作者的注意:当启用行首正则表达式处理多行日志时,采集性能出现下降。究竟是什么因素导致了这种现象?本文将探索Logtail多行日志采集性能提升的秘密。
707 57
|
SQL druid Oracle
【YashanDB知识库】yasdb jdbc驱动集成druid连接池,业务(java)日志中有token IDENTIFIER start异常
客户Java日志中出现异常,影响Druid的merge SQL功能(将SQL字面量替换为绑定变量以统计性能),但不影响正常业务流程。原因是Druid在merge SQL时传入null作为dbType,导致无法解析递归查询中的`start`关键字。
|
Java Maven
java项目中jar启动执行日志报错:no main manifest attribute, in /www/wwwroot/snow-server/z-server.jar-jar打包的大小明显小于正常大小如何解决
在Java项目中,启动jar包时遇到“no main manifest attribute”错误,且打包大小明显偏小。常见原因包括:1) Maven配置中跳过主程序打包;2) 缺少Manifest文件或Main-Class属性。解决方案如下:
3342 8
java项目中jar启动执行日志报错:no main manifest attribute, in /www/wwwroot/snow-server/z-server.jar-jar打包的大小明显小于正常大小如何解决
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
存储 监控 算法
Java JVM 面试题
Java JVM(虚拟机)相关基础面试题
495 4