JVM系列之:宏观分析Java代码是如何执行的

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: JVM系列之:宏观分析Java代码是如何执行的

1.jpg


本文为《深入学习 JVM 系列》第一篇文章


前言


作为一名 Java 程序员,平日里都是和 Java 代码打交道,但是仅限于使用,比如说使用 Java 核心类库,以及调用第三方类库里的 API。凭借上述“本事”便可以专注于实现具体业务,并且依赖 Java 虚拟机自动执行乃至优化我们的应用程序。那么自己就仅限于此了吗?


众所周知,JVM 和并发是应聘面试中两个绕不开的考点,大厂一些岗位招聘要求上明确写着熟悉甚至精通 JVM,掌握 JVM 性能调优技术。而我们在写简历时也需要慎用熟悉和精通这两个词,面试官会根据简历来发出不同深度的提问,JVM 知识考察如果回答顺利,甚至表现优异,无疑增大了面试成功的几率。


在以往的招聘准备中,我有整理过 JVM 这一领域的面试考查点,内容详细且全面,应对面试绰绰有余,但是并不知其所以然,更没有使用 Java 相关调试工具,来验证某些知识。


就个人而言,学习之路就像搭建房子一样,Java 作为地基(当然更底层的还有计算机原理等等,这里就先不谈了),像 Spring 大家族可能是我们目前常用的房屋框架,其他一些中间件都是辅助房屋修建的(不仅适用于Java体系)。想要在 Java 开发这条路上走的更远,内功的修炼就必不可少了,技术更新迭代那么快,市面上涌现出那么多优秀的框架和中间件,想要全部学习完也不是一件容易事,不如自底向上,夯实基础,让知识体系更加健壮。


以上便是我开启深入学习 JVM 之路的缘由,后续会结合网上资源和书籍,从新人的角度尽力去解读每一个技术点。


首先让我们看一下 Java 虚拟机的介绍。


Java虚拟机


Java 虚拟机(Java Virtual Machine,缩写为JVM)是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件。


Java 虚拟机有多种多样的种类,由不同的厂商提供,比如 HotSpot VM、SUN Classic VM、Exact VM 等等。不同的虚拟机的具体实现会有所不同,但是都遵循着 Java 虚拟机的规范。当前 HotSpot 虚拟机占市场主导地位。


JVM 是跨语言的,多种语言(比如说 groovy、scala、Jython等等)可以运行在 JVM 虚拟机上,从而可以利用JVM带来的跨平台特性和优秀的垃圾回收机制。以及可靠的即时编译器。


JVM 就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译成对应平台(unix、windows等)的机器指令执行,每一条 Java 指令,Java 虚拟机规范中都有其详细定义,怎么取操作数,怎么处理操作数等。


字节码文件具有如下特性:一次编译,到处执行。因此成就了 Java 的跨平台,即 java 源文件经过 javac 编译器编译成的二进制.class 字节码的跨平台性。各个平台装有不同的 JVM,而 JVM 能将相同的字节码翻译成平台相关的机器码,进而执行。


字节码命令学习


在后续的学习中,我们经常会使用编译和解析命令,所以必须熟练掌握。


javac   编译命令
#如果想执行 java xxx 命令,需要删除 java文件中的 package语句
javap   解析命令  javap -v xxx.class
复制代码


解析 class 文件后,得到字节码文件,其中涉及到众多关于 JVM 字节码的指令。


比如说操作数栈的相关指令


2.jpg


更多指令推荐阅读JVM 字节码指令手册,这里就不再赘述了。


我们来看一个经典案例:


int j=0;
for(int i=0;i<100;i++)
    j = j++;
System.out.println(j);
复制代码


大家可以猜一下输出结果是多少,是 100吗?答案不是 100,而是0。如果把 j = j++; 替换为 j++;,那么输出 100,这是为什么呢?


这里就要介绍一下前置++(++i)和后置++(i++)的区别了,不知道大家是在什么时候接触到这块内容,我记得自己是在大学学习 C语言的时候接触的,虽然语言不同,但是最终效果都是一样的。两者之间的区别为:


  • 前置++是将自身加1的值赋值给新变量,同时自身也加1
  • 后置++是将自身的值赋给新变量,然后才自身加1


已经好久没写过 C代码了,那么我们从 Java 语言角度来分析其背后的秘密,这里就需要用到字节码。


在讲字节码之前,先了解一下 Java 虚拟机运行时内存区域,这里先大概有个印象,下图是基于 JDK8 。


3.jpg


我们先简单了解一下 Java 虚拟机栈,当线程启动的时候,会分配一块内存当做该线程的栈,每个栈由一系列的栈帧组成。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。方法和栈帧是一一对应的关系。一个方法的执行就对应一个栈帧的入栈。方法的结束,这个栈帧就会出栈。


而每个栈帧中都拥有:局部变量表(也称为局部变量数组或本地变量表,是一个一维的数组)、操作数栈、动态链接、方法出口信息。下文主要涉及局部变量表和操作数栈。

4.jpg


先看后置++的实现:


public static void main(String[] args){
    int i= 0;
    i = i++;
}
复制代码


javac 命令编译后,然后使用 javap 命令解析。


5.jpg


每个指令的含义如下:


iconst_0  //把数值0 push到操作数栈
istore_1 // 把栈顶int型数值存入本地变量第2个位置
iload_1 // 把本地变量第2个位置的值push到栈顶
iinc 1,1  // 把本地变量表第2个位置加1     
istore_1 // 把栈顶int型数值存入本地变量第2个位置
复制代码


整个过程如下:


6.jpg


可以发现变量a在执行 iinc 1,1的时候已经变成1了,但是 istore_1 又把变量 a所在位置覆盖成了0,所以执行完i = i++,i还是原来那个值。


接着看看前置++的实现:


public static void main(String[] args){
    int i= 0;
    i = ++i;
}
复制代码

7.jpg


对应指令行为表示为:


iconst_0  //把数值0 push到操作数栈
istore_1 // 把栈顶int型数值存入本地变量第2个位置
iinc 1,1  // 把本地变量表第2个位置加1
iload_1 // 把本地变量第2个位置的值push到栈顶     
istore_1 // 把栈顶int型数值存入本地变量第2个位置
复制代码


过程如下:


8.jpg


和后置++不同的地方在于,在变量进入操作数栈之前,就先执行了iinc指令,所以进入操作数的值是加1后的值,最后写回的值也是最新值。


通过上述案例,让我们对字节码有了初步的认识,那么 Java 源码到底是如何执行的呢?


Java执行过程


首先我们需要明白,从硬件视角来看,Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译成机器码。


Java 的执行过程其实就是将源码转换为机器码,然后在 CPU 中执行。


整体可以分为两个部分:


第一步由 javac 将源码编译成字节码,在这个过程中会进行词法分析、语法分析、语义分析,编译原理中这部分的编译称为前端编译。


第二步将字节码转换为机器码,最后在 CPU 中执行。


关于第二步的机器码转换和执行,在 HotSpot 里面,有两种形式:


第一种是解释执行,无需编译直接逐条将字节码翻译成机器码并执行。在解释执行的过程中,虚拟机同时对程序运行的信息进行收集,在这些信息的基础上,编译器会逐渐发挥作用,它会进行后端编译——触发即时编译。


第二种是即时编译(Just-In-Time compilation,JIT),把字节码编译成机器码,但不是所有的代码都会被编译,只有被JVM认定为的热点代码,才可能被编译。


怎么样才会被认为是热点代码呢?JVM 中会设置一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译,然后将机器码存入 codeCache 中。当下次执行时,再遇到这段代码,就会从 codeCache 中读取机器码,直接执行。


9.jpg


前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。


为了提升 Java 虚拟机的运行效率,刚刚提到的即时编译便是重要的手段之一。


JIT 编译器


即时编译建立在程序符合二八定律的假设上,也就是百分之二十的代码占据了百分之八十的计算资源。


对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,下次可以重复调用,以达到理想的运行速度。


即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善字节码编译语言性能的技术。在 HotSpot 实现中有多种选择:C1、C2和C1+C2,分别对应 client、server 和分层编译。


1、C1编译,面向的是对启动性能有要求的客户端 GUI 程序,采用的优化手段相对简单,因此编译时间较短。优化方式比较保守;


2、C2编译,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂,因此编译时间较长,但同时生成代码的执行效率较高。优化方式比较激进;


3、C1+C2在开始阶段采用C1编译,当代码运行到一定热度之后采用C2重新编译。


HotSpot 会根据 CPU 的数量设置编译线程的数目,并且按 1:2 的比例配置给 C1 及 C2 编译器。


总的来说,C1的编译速度更快,C2的编译质量更高,分层编译的不同编译路径,也就是JVM根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从 Java 8 开始,HotSpot 默认采用分层编译。


在 JDK1.8之前,分层编译默认是关闭的,可以添加-server -XX:+TieredCompilation参数进行开启。


在命令行输入 java -version 可以看到运行模式为 mixed mode。


% java -version
java version "9.0.4"
Java(TM) SE Runtime Environment (build 9.0.4+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode)
复制代码


在编译期间,JIT会对代码做很多优化。比如说方法内联,逃逸分析等,这些知识点比较多,后续会单独拎出来讲。


总结


本文介绍了 Java 虚拟机,以及从宏观上介绍 Java 代码如何在虚拟机中运行。

Java 的跨平台指的是 Java 源文件经过 javac 编译器编译成的二进制.class 字节码的跨平台性。各个平台装有不同的 JVM,而 JVM 能将相同的字节码翻译成平台相关的机器码,进而执行。


文中还通过一个示例演示了如何通过字节码分析一个方法的执行过程。


Java 的执行过程其实就是将源码转换为机器码,然后在 CPU 中执行。在 HotSpot 中执行代码分为解释执行和编译执行(JIT编译器)两种方式,何种时机选择何种方式来执行代码,为了提高运行效率,HotSpot 默认采用分层编译。

目录
相关文章
|
24天前
|
缓存 JavaScript Java
常见java OOM异常分析排查思路分析
Java虚拟机(JVM)遇到内存不足时会抛出OutOfMemoryError(OOM)异常。常见OOM情况包括:1) **Java堆空间不足**:大量对象未被及时回收或内存泄漏;2) **线程栈空间不足**:递归过深或大量线程创建;3) **方法区溢出**:类信息过多,如CGLib代理类生成过多;4) **本机内存不足**:JNI调用消耗大量内存;5) **GC造成的内存不足**:频繁GC但效果不佳。解决方法包括调整JVM参数(如-Xmx、-Xss)、优化代码及使用高效垃圾回收器。
95 15
常见java OOM异常分析排查思路分析
|
17天前
|
设计模式 Java
Java设计模式:组合模式的介绍及代码演示
组合模式是一种结构型设计模式,用于将多个对象组织成树形结构,并统一处理所有对象。例如,统计公司总人数时,可先统计各部门人数再求和。该模式包括一个通用接口、表示节点的类及其实现类。通过树形结构和节点的通用方法,组合模式使程序更易扩展和维护。
Java设计模式:组合模式的介绍及代码演示
|
21天前
|
存储 缓存 监控
【Java面试题汇总】JVM篇(2023版)
JVM内存模型、双亲委派模型、类加载机制、内存溢出、垃圾回收机制、内存泄漏、垃圾回收流程、垃圾回收器、G1、CMS、JVM调优
【Java面试题汇总】JVM篇(2023版)
|
6天前
|
Java
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
20 5
|
8天前
|
Java API 开发者
探索Java中的Lambda表达式:简洁与强大的代码实践
本文深入探讨Java中Lambda表达式的定义、用法及优势,通过实例展示其如何简化代码、提升可读性,并强调在使用中需注意的兼容性和效率问题。Lambda作为Java 8的亮点功能,不仅优化了集合操作,还促进了函数式编程范式的应用,为开发者提供了更灵活的编码方式。
|
4天前
|
Java 开发者
探索Java中的Lambda表达式:简化你的代码之旅##
【8月更文挑战第62天】 Java 8的发布为开发者带来了诸多新特性,其中最引人注目的无疑是Lambda表达式。这一特性不仅让代码变得更加简洁,还极大地提升了开发的效率。本文将通过实际示例,展示如何利用Lambda表达式来优化我们的代码结构,同时探讨其背后的工作原理和性能考量。 ##
|
29天前
|
缓存 JavaScript Java
常见java OOM异常分析排查思路分析
Java虚拟机(JVM)遇到 OutOfMemoryError(OOM)表示内存资源不足。常见OOM情况包括:1) **Java堆空间不足**:内存被大量对象占用且未及时回收,或内存泄漏;解决方法包括调整JVM堆内存大小、优化代码及修复内存泄漏。2) **线程栈空间不足**:单线程栈帧过大或频繁创建线程;可通过优化代码或调整-Xss参数解决。3) **方法区溢出**:运行时生成大量类导致方法区满载;需调整元空间大小或优化类加载机制。4) **本机内存不足**:JNI调用或内存泄漏引起;需检查并优化本机代码。5) **GC造成的内存不足**:频繁GC但效果不佳;需优化JVM参数、代码及垃圾回收器
常见java OOM异常分析排查思路分析
|
12天前
|
Java
JAVA并发编程系列(9)CyclicBarrier循环屏障原理分析
本文介绍了拼多多面试中的模拟拼团问题,通过使用 `CyclicBarrier` 实现了多人拼团成功后提交订单并支付的功能。与之前的 `CountDownLatch` 方法不同,`CyclicBarrier` 能够确保所有线程到达屏障点后继续执行,并且屏障可重复使用。文章详细解析了 `CyclicBarrier` 的核心原理及使用方法,并通过代码示例展示了其工作流程。最后,文章还提供了 `CyclicBarrier` 的源码分析,帮助读者深入理解其实现机制。
|
7天前
|
Java API 开发者
探索Java中的Lambda表达式:简化代码,提升效率
【9月更文挑战第27天】在Java 8中引入的Lambda表达式为编程带来了革命性的变化。通过简洁的语法和强大的功能,它不仅简化了代码编写过程,还显著提升了程序的执行效率。本文将深入探讨Lambda表达式的本质、用法和优势,并结合实例演示其在实际开发中的应用。无论你是Java新手还是资深开发者,都能从中获得启发,优化你的代码设计。
|
8天前
|
Java Linux Python
Linux环境下 代码java调用python出错
Linux环境下 代码java调用python出错
24 3
下一篇
无影云桌面