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 划分了专门的内存区域来装载这些字节码数据以及运行时中间数据。