目录

介绍

# 介绍

Java程序之所以能够实现跨平台,本质就是因为他是运行在虚拟机上,而不同平台只需要安装对应平台的JVM即可运行(JRE中包含),所有的Java程序都采用统一的标准,在任何平台编译出来的字节码文件(.class)也是同样的,最后实际上是将编译后的字节码交由JVM进行处理执行。 ![[Pasted image 20220306010820.png]]

正是得益于这种统一规范,除了Java以外,还有很多种JVM语言,例如Kotlin、Groovy等。他们语法虽与Java不同,但最终编译得到的字节码文件,和Java是相同规范的,同样可以交给JVM处理。 ![[Pasted image 20220306011431.png]]

# 虚拟机的发展历程

在1996年,Java1.0发布时,第一款商用虚拟机Sun Classic VM开始了他的使命,他提供了一个Java解释器,也就是将我们的class文件进行读取,最后得到一条条的命令,JVM再将命令依次执行,虽然这种运行方式简单易懂,但效率极低,同样的代码每次都需要重新翻译再执行。

我们需要一个更高效的方法来运行Java程序,现在大多数主流的JVM都是包含即时编译器。JVM会根据当前代码进行判断,当发现某段代码块或方法运行十分频繁时,会将代码认为“热点代码”。为提高热点代码的执行效率,在运行时,虚拟机会将这些代码编译为与本地平台相关的机器码并进行各种层次的优化,完成任务编译器称为即时编译器(Just In Time Compiler)。 ![[Pasted image 20220306021443.png]]

在JDK1.4时,Sun Classic VM退出了历史舞台,取而代之的是沿用至今的HotSpot VM,他说目前最为广泛使用的虚拟机,拥有上面所说的热点代码检测技术、准确式内存管理(即虚拟机知道内存中某个位置的数据具体类型是什么)等技术。

2018年4月,Oracle公司公布了最新的GraalVM,这是一种全新的虚拟机,能够实现所有语言统一运行在虚拟机中。

GraalVM是基于HotSpot基础上增强而成的跨语言全栈虚拟机,可以作为“任何语言”的运行平台使用,并且支持无额外开销地混合使用这些编程语言,支持不同语言中混用对方接口和对象。

# 构建第一个java汇编

现在我们在java中写一个类,例如

public class main {  
    public int test(){  
         int a = 10;  
		 int b = 20;  
		 int c = a+b;  
		 return c;  
	}  
}
1
2
3
4
5
6
7
8

在写完这个简单的类后,我们可以点击构建去构建文件,然后使用javap -v [构建文件的路径],可以获得该文件的java汇编指令,此时我们可以从打印的结果中找到如下内容:

  public int test();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1 //依次对应 所需使用的栈的深度、本地变量的数量、堆栈上最大对象数量(这里指的是this)
		  // 下面是所需执行的命令
         0: bipush        10 //0是程序的偏移地址,然后是指令,最后是操作数
         2: istore_1
         3: bipush        20
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: iload_3
        11: ireturn
      LineNumberTable:
        line 3: 0
        line 4: 3
        line 5: 6
        line 6: 10
		//局部变量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Lmain;
            3       9     1     a   I
            6       6     2     b   I
           10       2     3     c   I
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

我们可以看到,java文件在编译后也会生成类似C语言那样的汇编指令,但这些指令是交给JVM执行的指令(虚拟机提供了一个类似物理机的运行环境,也有程序计数器之类的东西),最下方存放的是局部变量表,实际上this也在其中,所以我们才能在非静态方法中使用this关键字,在最上方标记了方法的返回值类型、访问权限等

介绍一下例子中的命令代表的含义:

  • bipush 将单字节的常量值推到栈顶
  • istore_1 将栈顶的int类型数值存入到第二个本地变量
  • istore_2 将栈顶的int类型数值存入到第三个本地变量
  • istore_3 将栈顶的int类型数值存入到第四个本地变量
  • iload_1 将第二个本地变量推向栈顶
  • iload_2 将第三个本地变量推向栈顶
  • iload_3 将第四个本地变量推向栈顶
  • iadd 将栈顶的两个int变量相加,并将结果压入栈顶
  • ireturn 方法的返回操作

JVM运行字节码时,所有操作基本围绕了两种数据结构,一种是堆栈(本质是栈结构),还有一种是队列,如果JVM执行某条指令时,该指令需要对数据进行操作,那么被操作的数据在执行指令前,必须要压到堆栈上,JVM会自动将栈顶数据作为操作数。如果堆栈上的数据需要暂时保存起来时,它会被存储到局部变量队列上。

# 流程说明

首先,程序会先执行bipush指令,将常量10压入栈顶。然后执行istore_1指令,将位于栈顶的数值(即刚压入的常量10)存入到第二个本地变量中(即局部变量表第二个变量,即为a),从而实现int a = 10的操作。第三第四条指令以此类推。

当程序执行到iload_1指令时,会将第二个本地变量推向栈顶(即a的值推入栈顶),再执行iload_2将第三个本地变量推向栈顶(即b的值推入栈顶),然后执行iadd指令,将栈顶顶部的两个值相加,即 10 + 20,再将结果推入栈顶,此时栈顶值为30。

然后执行istore_3将栈顶的值存入第四个本地变量中,iload_3会将第四个变量的值推入栈顶,并执行ireturn返回该值。

在该流程中,不难发现,其实执行的操作无非就是入栈和出栈操作,并且大部分指令都是没有操作数的,传统的汇编指令有一操作数、二操作数甚至三操作数的指令,Java相比C编译出来的汇编指令,执行起来会更加复杂,实现某个功能的指令条数也会增多,这就是为什么Java执行效率实际上不如C/C++的,虽然能够实现跨平台,但牺牲了性能,因此在追求性能的Android平台上,采用的是定制版JVM,并且是基于寄存器的指令集架构。此外,某些情况下,我们还能用JNI机制来通过Java调用C/C++编写的程序以提升性能。

最近更新
01
基本知识
07-18
02
卷积神经网络识别图像
07-18
03
损失函数
07-18
更多文章>