性能文章>JVM系列之:宏观分析Java代码是如何执行的>

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

2月前
241717

前言

作为一名 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 字节码的指令。

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

更多指令推荐阅读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 。

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

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

先看后置++的实现:

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

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

每个指令的含义如下:

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

整个过程如下:

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

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

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

对应指令行为表示为:

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

过程如下:

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

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

Java执行过程

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

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

整体可以分为两个部分:

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

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

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

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

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

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

前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。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 默认采用分层编译。

 

JVM系列阅读

JVM系列之:你真的了解垃圾回收吗

JVM系列之:JVM是如何创建对象的

JVM系列之:JVM是怎么实现invokedynamic的?

JVM系列之:关于方法句柄的那些事

JVM系列之:关于HSDB的一点心得

JVM系列之:JVM是如何实现反射的

JVM系列之:关于JVM类加载的那些事

JVM系列之:聊一聊Java异常

JVM系列之:JVM如何执行方法调用

JVM系列之:聊聊Java的数据类型

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

分类:
标签:
请先登录,再评论

感谢整理,很扎实!

2月前

为你推荐

字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
据说99.99%的人都会答错的类加载的问题
概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。 同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗请仔细注意
Java多线程——并发测试
编写并发程序时候,可以采取和串行程序相同的编程方式。唯一的难点在于,并发程序存在不确定性,这种不确定性会令程序出错的地方远比串行程序多,出现的方式也没有固定规则。那么如何在测试中,尽可能的暴露出这些问
Java多线程知识小抄集(一)
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。 1.interr