「入门篇」初识JVM

2022/3/27 23:23:20

本文主要是介绍「入门篇」初识JVM,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

记录于 2022-01-02  17:25:12  ©GhostFace

1. 什么是JVM?


概念

来自百度百科

JVM是 Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。 引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。 Java虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。 Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。 Java虚拟机不仅是一种跨平台的软件,而且是一种新的网络计算平台。该平台包括许多相关的技术,如符合开放接口标准的各种API、优化技术等。Java技术使同一种应用可以运行在不同的平台上。Java平台可分为两部分,即Java虚拟机(Java virtual machine,JVM)和Java API类库。
大致可以了解到:理解
  • JVM是一种规范的、跨平台的软件,一种新的网络计算平台,本质上就是一个程序,通过仿真模拟各种计算机功能实现。
  • JVM可以让Java语言在不同平台上运行不需要重新编译 —— Java语言的特点:跨平台、可移植性
  • (.java)  → Java语言编译程序 → 在Java虚拟机上运行的目标代码 (.class字节码文件) —— “一次编译,多次运行”
  • Java虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。

总结: JVM是一种规范,它实现了Java的跨平台,是软件、网络计算平台或者说是程序,一个通过模拟各种计算机功能实现的程序。因为是程序,所以它有完善的架构,如处理器、堆栈、指令库等等。

跨平台:*.java 源代码文件经过编译器生成 *.class 字节码文件,通过这个 *.class 字节码文件拿到各种装有JDK或者JRE的操作系统中执行就能运行了。

思考问题?

  1. *.java 源代码文件 如何编译成 *.class 字节码文件?什么是字节码文件 (.class)?
  2. JVM 如何将 .class 字节码文件变成不同操作系统可以直接运行的机器语言?
  3. JVM 能干嘛?在实际开发中的运用?
  4. JVM 的体系结构?位置?

接下来就带着这些问题进一步探究吧~

1. *.java 源代码文件 → *.class 字节码文件的过程?什么是字节码文件?

java源文件转换为字节码文件的过程

*.java 源代码文件 经过 javac编译 成 *.class 字节码文件,这里给出流程图(图1),详细分析下次再细说,或者可以看看《深入理解Java虚拟机》原文第10章 (P456) [1]

编译过程大致可以分为3个过程(以Sun Javac为例)

1、解析与填充符号表过程;

解析主要包括词法分析和语法分析两个过程

2、插入式注解处理器的注解处理过程; 

3、语义分析与字节码的生成过程。                                      ©GhostFace

.java源代码到.class字节码的过程

     图1 .java源代码文件到.class字节码文件的过程

.java源代码文件通过javac编译器先进行解析,即对词法和语法的分析词法分析会将源代码转换为字符流,生成标记集合(标记集合包括:关键字、变量名、字面量和运算符等),而语法分析是将标记集合生成语法树的过程,(如果源代码中存在注解,则通过注解处理器进行处理)再进行下一步语义分析对源代码上下文有关性质的审查和数据及控制流分析,最后由字节码生成器生成.class字节码文件

什么是字节码文件?

来自CSDN社区  理解

Java字节码类文件(.class)是Java编译器编译Java源文件(.java)产生的“目标文件”。

它是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间(方便于网络的传输)。

Java源文件在被Java编译器编译之后, 每个类(或者接口)都单独占据一个class文件, 并且类中的所有信息都会在class文件中有相应的描述, 由于class文件很灵活, 它甚至比Java源文件有着更强的描述能力。

.class 字节码文件 由javac编译产生,是8位字节的二进制流文件。

特点:文件数据项紧凑,体积轻巧,占用内存空间小,便于网络传输

Class文件的结构属性

在理解之前先从整体看下java字节码文件包含了哪些类型的数据:

从一个例子开始

下面以一个简单的例子来逐步讲解字节码。

//Main.java
public class Main {
    
    private int m;
    
    public int inc() {
        return m + 1;
    }
}

通过以下命令, 可以在当前所在路径下生成一个Main.class文件。

javac Main.java

以文本的形式打开生成的class文件,内容如下:

查看代码
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 4d61 696e 2e6a 6176 610c
0007 0008 0c00 0500 0601 0010 636f 6d2f
7268 7974 686d 372f 4d61 696e 0100 106a
6176 612f 6c61 6e67 2f4f 626a 6563 7400
2100 0300 0400 0000 0100 0200 0500 0600
0000 0200 0100 0700 0800 0100 0900 0000
1d00 0100 0100 0000 052a b700 01b1 0000
0001 000a 0000 0006 0001 0000 0003 0001
000b 000c 0001 0009 0000 001f 0002 0001
0000 0007 2ab4 0002 0460 ac00 0000 0100
0a00 0000 0600 0100 0000 0800 0100 0d00
0000 0200 0e
  • 文件开头的4个字节("cafe babe")称之为 魔数,唯有以"cafe babe"开头的class文件方可被虚拟机所接受,这4个字节就是字节码文件的身份识别。
  • 0000是编译器jdk版本的次版本号0,0034转化为十进制是52,是主版本号,java的版本号从45开始,除1.0和1.1都是使用45.x外,以后每升一个大版本,版本号加一。也就是说,编译生成该class文件的jdk版本为1.8.0。

通过java -version命令稍加验证, 可得结果。

Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

继续往下是常量池... 知道是这么分析的就可以了,然后我们通过工具反编译字节码文件继续去看。

反编译字节码文件

使用到java内置的一个反编译工具javap可以反编译字节码文件, 用法: javap <options> <classes>

其中<options>选项包括:

查看代码
-help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置

输入命令javap -verbose -p Main.class查看输出内容:

查看代码
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.class
  Last modified 2018-4-7; size 362 bytes
  MD5 checksum 4aed8540b098992663b7ba08c65312de
  Compiled from "Main.java"
public class com.rhythm7.Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#19         // com/rhythm7/Main.m:I
   #3 = Class              #20            // com/rhythm7/Main
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/rhythm7/Main;
  #14 = Utf8               inc
  #15 = Utf8               ()I
  #16 = Utf8               SourceFile
  #17 = Utf8               Main.java
  #18 = NameAndType        #7:#8          // "<init>":()V
  #19 = NameAndType        #5:#6          // m:I
  #20 = Utf8               com/rhythm7/Main
  #21 = Utf8               java/lang/Object
{
  private int m;
    descriptor: I
    flags: ACC_PRIVATE

  public com.rhythm7.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/rhythm7/Main;

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/rhythm7/Main;
}
SourceFile: "Main.java"

字节码文件信息

开头的7行信息包括:Class文件当前所在位置,最后修改时间,文件大小,MD5值,编译自哪个文件,类的全限定名,jdk次版本号,主版本号。

然后紧接着的是该类的访问标志:ACC_PUBLIC, ACC_SUPER,访问标志的含义如下:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为Public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可以设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义.
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标志这是一个注解
ACC_ENUM 0x4000 标志这是一个枚举

常量池

Constant pool意为常量池。

常量池可以理解成Class文件中的资源仓库。主要存放的是两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量类似于java中的常量概念,如文本字符串,final常量等,而符号引用则属于编译原理方面的概念,包括以下三种:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符号(Descriptor)
  • 方法的名称和描述符

不同于C/C++, JVM是在加载Class文件的时候才进行的动态链接,也就是说这些字段和方法符号引用只有在运行期转换后才能获得真正的内存入口地址。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析并翻译到具体的内存地址中。 直接通过反编译文件来查看字节码内容:

#1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
#4 = Class              #21            // java/lang/Object
#7 = Utf8               <init>
#8 = Utf8               ()V
#18 = NameAndType        #7:#8          // "<init>":()V
#21 = Utf8               java/lang/Object

第一个常量是一个方法定义,指向了第4和第18个常量。以此类推查看第4和第18个常量。最后可以拼接成第一个常量右侧的注释内容:

java/lang/Object."<init>":()V

这段可以理解为该类的实例构造器的声明,由于Main类没有重写构造方法,所以调用的是父类的构造方法。此处也说明了Main类的直接父类是Object。 该方法默认返回值是V, 也就是void,无返回值。

第二个常量同理可得:

#2 = Fieldref           #3.#19         // com/rhythm7/Main.m:I
#3 = Class              #20            // com/rhythm7/Main
#5 = Utf8               m
#6 = Utf8               I
#19 = NameAndType        #5:#6          // m:I
#20 = Utf8               com/rhythm7/Main

复制代码此处声明了一个字段m,类型为I, I即是int类型。关于字节码的类型对应如下:

标识字符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型,以分号结尾,如Ljava/lang/Object;

对于数组类型,每一位使用一个前置的[字符来描述,如定义一个java.lang.String[][]类型的维数组,将被记录为[[Ljava/lang/String;

方法表集合

在常量池之后的是对类内部的方法描述,在字节码中以表的集合形式表现,暂且不管字节码文件的16进制文件内容如何,我们直接看反编译后的内容。

private int m;
  descriptor: I
  flags: ACC_PRIVATE

此处声明了一个私有变量m,类型为int,返回值为int

public com.rhythm7.Main();
   descriptor: ()V
   flags: ACC_PUBLIC
   Code:
     stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
        4: return
     LineNumberTable:
       line 3: 0
     LocalVariableTable:
       Start  Length  Slot  Name   Signature
           0       5     0  this   Lcom/rhythm7/Main;

这里是构造方法:Main(),返回值为void, 公开方法。

code内的主要属性为:

  • stack: 最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1

  • locals: 局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。方法参数(包括实例方法中的隐藏参数this),显示异常处理器的参数(try catch中的catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的。

  • args_size: 方法参数的个数,这里是1,因为每个实例方法都会有一个隐藏参数this

  • attribute_info: 方法体内容,0,1,4为字节码"行号",该段代码的意思是将第一个引用类型本地变量推送至栈顶,然后执行该类型的实例方法,也就是常量池存放的第一个变量,也就是注释里的"java/lang/Object.""



这篇关于「入门篇」初识JVM的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程