jvm和字节码

jvm以及字节码介绍

jvm介绍

在介绍类加载器(ClassLoader)和其机制之前先介绍JVM,JVM 大致可以划分为三个部分,分别是类加载器(Class Loader)、运行时数据区(Runtime Data Areas)和执行引擎(Excution Engine)。

三个部分具体又是干什么的,可以通过下面这幅图来了解

Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader会调用JVM的native方法(defineClass0/1/2)来定义一个java.lang.Class实例,通过上述解释可以知道类的加载就是由java类加载器实现的。

字节码

编写一个类文件测试一下,以最简单的helloworld为例子

1
2
3
4
5
6
package org.example;
public class Main {
public static void main(String[] args) {
System.out.println("hello world");
}
}

运行之后在target/classes/org/example目录下发现了class文件,里面的代码和源码为什么会一样?是因为IDEA默认使用Fernflower(反编译工具)将字节码文件(.class,java源代码编译后的文件)反编译为我们看的懂的java代码,通过view->show bytecode可以看到字节码

可以用jclasslib插件

字节码并不是机器码,操作系统无法直接识别,需要在操作系统上安装不同版本的 JVM 来识别。

通常情况下,我们只需要安装不同版本的 JDK(Java Development Kit,Java 开发工具包)就行了,它里面包含了 JRE(Java Runtime Environment,Java 运行时环境),而 JRE 又包含了 JVM。

javap是java内置的反编译工具,用javap -v -p Main.class看一下刚才的class文件

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
Classfile /Users/apple/Documents/javacode/classloader/target/classes/org/example/Main.class
Last modified 2025-3-20; size 539 bytes
MD5 checksum a848fbf47800b5f4f1d56bc08b626ee5
Compiled from "Main.java"
public class org.example.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // org/example/Main
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/example/Main;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Main.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 org/example/Main
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public org.example.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 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/example/Main;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 7: 0
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "Main.java"

看到上面Code:

1
2
3
4
0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

字节码指令序列通常由多条指令组成,每条指令由一个操作码和若干个操作数构成。

  • 操作码:一个字节大小的指令,用于表示具体的操作。
  • 操作数:跟随操作码,用于提供额外信息。

这段字节码序列的意思是调用 System.out.println 方法打印”Hello World”字符串。下面是详细的解释:

①、0: getstatic #2 <java/lang/System.out>

  • 操作码:getstatic
  • 操作数:#2
  • 描述:这条指令的作用是获取静态字段,这里获取的是java.lang.System类的out静态字段,它是一个PrintStream类型的输出流。#2 是一个指向常量池的索引,后面在讲类文件结构时会讲到。

②、3: ldc #3 <Hello World>

  • 操作码:ldc
  • 操作数:#3
  • 描述:这条指令的作用是从常量池中加载一个常量值(字符串”Hello World”)到操作数栈顶。#3 是一个指向常量池的索引,常量池里存储了字符串”Hello World”的引用。

③、5: invokevirtual #4 <java/io/PrintStream.println>

  • 操作码:invokevirtual
  • 操作数:#4
  • 描述:这条指令的作用是调用方法。这里调用的是PrintStream类的println方法,用来打印字符串。#4 是一个指向常量池的索引,常量池里存储了java/io/PrintStream.println方法的引用信息。

④、8: return

  • 操作码:return
  • 描述:这条指令的作用是从当前方法返回。

上面的 getstatic、ldc、invokevirtual、return 等就是 字节码指令的操作码。

可以使用 hexdump,一个在 Unix 和 Linux 系统中常用的工具,用于以十六进制的形式显示文件的内容,看一下字节码的二进制内容。

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
31
32
33
34
35
36
hexdump -C Main.class
00000000 ca fe ba be 00 00 00 34 00 22 0a 00 06 00 14 09 |�...4."......|
00000010 00 15 00 16 08 00 17 0a 00 18 00 19 07 00 1a 07 |................|
00000020 00 1b 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 |.....<init>...()|
00000030 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e |V...Code...LineN|
00000040 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 |umberTable...Loc|
00000050 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 |alVariableTable.|
00000060 00 04 74 68 69 73 01 00 12 4c 6f 72 67 2f 65 78 |..this...Lorg/ex|
00000070 61 6d 70 6c 65 2f 4d 61 69 6e 3b 01 00 04 6d 61 |ample/Main;...ma|
00000080 69 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e |in...([Ljava/lan|
00000090 67 2f 53 74 72 69 6e 67 3b 29 56 01 00 04 61 72 |g/String;)V...ar|
000000a0 67 73 01 00 13 5b 4c 6a 61 76 61 2f 6c 61 6e 67 |gs...[Ljava/lang|
000000b0 2f 53 74 72 69 6e 67 3b 01 00 0a 53 6f 75 72 63 |/String;...Sourc|
000000c0 65 46 69 6c 65 01 00 09 4d 61 69 6e 2e 6a 61 76 |eFile...Main.jav|
000000d0 61 0c 00 07 00 08 07 00 1c 0c 00 1d 00 1e 01 00 |a...............|
000000e0 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 07 00 1f 0c |.hello world....|
000000f0 00 20 00 21 01 00 10 6f 72 67 2f 65 78 61 6d 70 |. .!...org/examp|
00000100 6c 65 2f 4d 61 69 6e 01 00 10 6a 61 76 61 2f 6c |le/Main...java/l|
00000110 61 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 |ang/Object...jav|
00000120 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 |a/lang/System...|
00000130 6f 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 |out...Ljava/io/P|
00000140 72 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 |rintStream;...ja|
00000150 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 |va/io/PrintStrea|
00000160 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c |m...println...(L|
00000170 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 |java/lang/String|
00000180 3b 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 |;)V.!...........|
00000190 01 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 |............/...|
000001a0 01 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a |.....*........|
000001b0 00 00 00 06 00 01 00 00 00 05 00 0b 00 00 00 0c |................|
000001c0 00 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e |................|
000001d0 00 0f 00 01 00 09 00 00 00 37 00 02 00 01 00 00 |.........7......|
000001e0 00 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 |.............|
000001f0 0a 00 00 00 0a 00 02 00 00 00 07 00 08 00 08 00 |................|
00000200 0b 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 |................|
00000210 00 00 01 00 12 00 00 00 02 00 13 |...........|
0000021b

ca fe ba be就是.class,字节码的二进制内容对应为这里

1
b2 00 02 12 03 b6 00 04 b1

注意:这里是二进制文件的 16 进制表示,也就是 hex,一般分析二进制文件都是以 hex 进行分析。字节码指令和二进制之间的对应关系,以及对应的语义如下所示:

1
2
3
4
0xb2   getstatic       获取静态字段的值
0x12 ldc 常量池中的常量值入栈
0xb6 invokevirtual 运行时方法绑定调用方法
0xb1 return void 方法返回

JVM 就是靠解析这些字节码指令来完成程序执行的。常见的执行方式有两种,一种是解释执行,对字节码逐条解释执行;一种是 JIT,也就是即时编译,它会在运行时将热点代码优化并缓存起来,下次再执行的时候直接使用缓存起来的机器码,而不需要再次解释执行。

这样就可以提高程序的执行效率。

注意,当类加载器完成字节码数据加载任务后,JVM 划分了专门的内存区域来装载这些字节码数据以及运行时中间数据。