Java虚拟机(JVM)的理解
编写一个 java 文件
写一个 Hello. java,内容如下
public class Hello {
int i = 0;
public static void main(String[] args) {
System.out.println("Hello JVM");
}
public void method1(double d){
}
public void method2() {
int i = 0;
int a = i++;
int b = ++i;
}
public class InnerClass {
}
}
使用javac编译
通过 javac Hello.java 对 Hello.java进行编译,获取到 Hello.class 字节码,使用 hexdump Hello.class 命令就查看我们的16进制字节码
➜ Desktop javac Hello.java
➜ Desktop hexdump Hello.class
0000000 ca fe ba be 00 00 00 37 00 29 0a 00 07 00 19 09
0000010 00 06 00 1a 09 00 1b 00 1c 08 00 1d 0a 00 1e 00
0000020 1f 07 00 20 07 00 21 07 00 22 01 00 0a 49 6e 6e
0000030 65 72 43 6c 61 73 73 01 00 0c 49 6e 6e 65 72 43
0000040 6c 61 73 73 65 73 01 00 01 69 01 00 01 49 01 00
0000050 06 3c 69 6e 69 74 3e 01 00 03 28 29 56 01 00 04
0000060 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 75 6d 62 65
0000070 72 54 61 62 6c 65 01 00 04 6d 61 69 6e 01 00 16
0000080 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000090 69 6e 67 3b 29 56 01 00 07 6d 65 74 68 6f 64 31
00000a0 01 00 04 28 44 29 56 01 00 07 6d 65 74 68 6f 64
00000b0 32 01 00 0a 53 6f 75 72 63 65 46 69 6c 65 01 00
00000c0 0a 48 65 6c 6c 6f 2e 6a 61 76 61 01 00 0b 4e 65
00000d0 73 74 4d 65 6d 62 65 72 73 0c 00 0d 00 0e 0c 00
00000e0 0b 00 0c 07 00 23 0c 00 24 00 25 01 00 09 48 65
00000f0 6c 6c 6f 20 4a 56 4d 07 00 26 0c 00 27 00 28 01
0000100 00 05 48 65 6c 6c 6f 01 00 10 6a 61 76 61 2f 6c
0000110 61 6e 67 2f 4f 62 6a 65 63 74 01 00 10 48 65 6c
0000120 6c 6f 24 49 6e 6e 65 72 43 6c 61 73 73 01 00 10
0000130 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d
0000140 01 00 03 6f 75 74 01 00 15 4c 6a 61 76 61 2f 69
0000150 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 01 00
0000160 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74
0000170 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00
0000180 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000190 69 6e 67 3b 29 56 00 21 00 06 00 07 00 00 00 01
00001a0 00 00 00 0b 00 0c 00 00 00 04 00 01 00 0d 00 0e
00001b0 00 01 00 0f 00 00 00 26 00 02 00 01 00 00 00 0a
00001c0 2a b7 00 01 2a 03 b5 00 02 b1 00 00 00 01 00 10
00001d0 00 00 00 0a 00 02 00 00 00 01 00 04 00 02 00 09
00001e0 00 11 00 12 00 01 00 0f 00 00 00 25 00 02 00 01
00001f0 00 00 00 09 b2 00 03 12 04 b6 00 05 b1 00 00 00
0000200 01 00 10 00 00 00 0a 00 02 00 00 00 05 00 08 00
0000210 06 00 01 00 13 00 14 00 01 00 0f 00 00 00 19 00
0000220 00 00 03 00 00 00 01 b1 00 00 00 01 00 10 00 00
0000230 00 06 00 01 00 00 00 0a 00 01 00 15 00 0e 00 01
0000240 00 0f 00 00 00 31 00 01 00 04 00 00 00 0d 03 3c
0000250 1b 84 01 01 3d 84 01 01 1b 3e b1 00 00 00 01 00
0000260 10 00 00 00 12 00 04 00 00 00 0d 00 02 00 0e 00
0000270 07 00 0f 00 0c 00 10 00 03 00 16 00 00 00 02 00
0000280 17 00 18 00 00 00 04 00 01 00 08 00 0a 00 00 00
0000290 0a 00 01 00 08 00 06 00 09 00 01
000029b
字节码结构(The ClassFile Structure)
文档:https://docs.oracle.com/javase/specs/jvms/se18/html/jvms-4.html#jvms-4.1
ClassFile {
u4 magic;// 4个字节 魔法 ca fe ba be
u2 minor_version;// 2个字节 版本 00 00
u2 major_version;// 主版本 00 37
u2 constant_pool_count;// 常量池个数
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
反编译字节码
使用 javap 反编译可以看到文件的基本结构
➜ Desktop javap Hello.class
Compiled from "Hello.java"
public class Hello {
int i;
public Hello();
public static void main(java.lang.String[]);
public void method1(double);
public void method2();
}
使用 javap -c 可以得到每个方法的 Code 信息
➜ Desktop javap -c Hello.class
Compiled from "Hello.java"
public class Hello {
int i;
public Hello();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field i:I
9: return
public static void main(java.lang.String[]);
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello JVM
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void method1(double);
Code:
0: return
public void method2();
Code:
0: iconst_0
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_2
7: iinc 1, 1
10: iload_1
11: istore_3
12: return
}
使用 javap -v 可以看到更详细的信息
➜ Desktop javap -v Hello.class
Classfile /Users/Chen/Desktop/Hello.class
Last modified 2022年3月24日; size 667 bytes
MD5 checksum 478f158d1c0254ac79dde15c84490701
Compiled from "Hello.java"
public class Hello
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #6 // Hello
super_class: #7 // java/lang/Object
interfaces: 0, fields: 1, methods: 4, attributes: 3
Constant pool:// 常量池
#1 = Methodref #7.#25 // java/lang/Object."<init>":()V
#2 = Fieldref #6.#26 // Hello.i:I
#3 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #29 // Hello JVM
#5 = Methodref #30.#31 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Class #32 // Hello
#7 = Class #33 // java/lang/Object
#8 = Class #34 // Hello$InnerClass
#9 = Utf8 InnerClass
#10 = Utf8 InnerClasses
#11 = Utf8 i
#12 = Utf8 I
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 method1
#20 = Utf8 (D)V
#21 = Utf8 method2
#22 = Utf8 SourceFile
#23 = Utf8 Hello.java
#24 = Utf8 NestMembers
#25 = NameAndType #13:#14 // "<init>":()V
#26 = NameAndType #11:#12 // i:I
#27 = Class #35 // java/lang/System
#28 = NameAndType #36:#37 // out:Ljava/io/PrintStream;
#29 = Utf8 Hello JVM
#30 = Class #38 // java/io/PrintStream
#31 = NameAndType #39:#40 // println:(Ljava/lang/String;)V
#32 = Utf8 Hello
#33 = Utf8 java/lang/Object
#34 = Utf8 Hello$InnerClass
#35 = Utf8 java/lang/System
#36 = Utf8 out
#37 = Utf8 Ljava/io/PrintStream;
#38 = Utf8 java/io/PrintStream
#39 = Utf8 println
#40 = Utf8 (Ljava/lang/String;)V
{
int i;
descriptor: I
flags: (0x0000)
public Hello();// 默认的无参构造方法
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1// 非静态方法入参都会有一个this,所以args_size为1
0: aload_0// 指令,可以通过字节码指令表查询到对应指令所做的操作
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field i:I
9: return
LineNumberTable:
line 1: 0
line 2: 4
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello JVM
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
public void method1(double);
descriptor: (D)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=0, locals=3, args_size=2
0: return
LineNumberTable:
line 10: 0
public void method2();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_2
7: iinc 1, 1
10: iload_1
11: istore_3
12: return
LineNumberTable:
line 13: 0
line 14: 2
line 15: 7
line 16: 12
}
SourceFile: "Hello.java"
NestMembers:
Hello$InnerClass
InnerClasses:
public #9= #8 of #6; // InnerClass=class Hello$InnerClass of class Hello
查看内部类结构
➜ Desktop javap -v Hello\$InnerClass// 查看内部类结构
Classfile /Users/Chen/Desktop/Hello$InnerClass.class
Last modified 2022年3月24日; size 324 bytes
MD5 checksum 442562f7390b367a4cd1b276f9420f73
Compiled from "Hello.java"
public class Hello$InnerClass
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // Hello$InnerClass
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 1, attributes: 3
Constant pool:
#1 = Fieldref #3.#15 // Hello$InnerClass.this$0:LHello;
#2 = Methodref #4.#16 // java/lang/Object."<init>":()V
#3 = Class #17 // Hello$InnerClass
#4 = Class #20 // java/lang/Object
#5 = Utf8 this$0
#6 = Utf8 LHello;
#7 = Utf8 <init>
#8 = Utf8 (LHello;)V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 SourceFile
#12 = Utf8 Hello.java
#13 = Utf8 NestHost
#14 = Class #21 // Hello
#15 = NameAndType #5:#6 // this$0:LHello;
#16 = NameAndType #7:#22 // "<init>":()V
#17 = Utf8 Hello$InnerClass
#18 = Utf8 InnerClass
#19 = Utf8 InnerClasses
#20 = Utf8 java/lang/Object
#21 = Utf8 Hello
#22 = Utf8 ()V
{
final Hello this$0;
descriptor: LHello;
flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC
public Hello$InnerClass(Hello);// 内部类持有外部类
descriptor: (LHello;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LHello;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
LineNumberTable:
line 18: 0
}
SourceFile: "Hello.java"
NestHost: class Hello
InnerClasses:
public #18= #3 of #14; // InnerClass=class Hello$InnerClass of class Hello
在 Code 信息中可以看到我们的 操作数栈(stack)的大小,本地变量表(locals)大小,传入参数(args_size)数量。
操作数栈和本地变量表的大小单位是 Slot,double 和 long 类型会占用2个 Slot.
每个非静态方法的传入参数都会有一个 this.
成员变量的初始化是在默认构造方法进行初始化的。
虚拟机字节码指令表
文档:https://juejin.cn/post/6889766684061368328,在字节码中每个指令都用一个字节表示
JVM运行时的数据区域
被所有线程共享的
堆、方法区
堆:程序运行时创建的所有实例对象
对象:
对象头:MarkWord、类型指针、数组长度(对象是数组时)
具体数据
对齐填充
方法区:存放的是会被调用的一些方法、静态常量等。
方法区存储的是从Class文件加载进来的静态变量、类信息、常量池以及编译器编译后的代码。
方法区中,常量池可以分为 Class 文件常量池以及运行时常量池,java 程序运行后,Class 文件中的信息被字节码执行引擎加载到了方法区,从而形成了运行时常量池。
每个线程私有的
本地方法栈、虚拟机栈、程序计数器
本地方法栈:存放的是调用本地(native)方法的相关信息
虚拟机栈:线程运行过程中调用方法的记录,调用方法必然是后运行的结果在前面返回
程序计数器:程序运行时可能会遇到 CPU 资源被切换走再获得,为了避免丢失前一次运行的信息
虚拟机栈中的栈帧
每个方法执行的时候,都会在虚拟机栈的栈顶创建一个栈帧,等到方法执行完毕,对应的栈帧就会栈并销毁。
栈帧包含:局部变量表、操作数栈、动态连接、返回地址
该区域可能会抛出的异常:
StackOverflowError(栈深度大于虚拟机运行的深度)
OOM(虚拟机动态扩展时,无法申请到足够内存)
垃圾回收
JVM堆的结构划分:新生代、老年代
新生代:
eden(80%)
from(10%)
to(10%)
对象会在新生代的 eden 区域中创建(大对象会直接进入老年代),第一次 eden 区满了以后进行 minor GC 将 存货对象 age + 1,然后放入 from 区域,将 eden 区域内存清空。
以后每次 minor GC 都将 eden 区域和 from 区域中的存活对象 age + 1,然后放入 to 区域,然后将 to 区域和 from 区域互相调换。
在 age 达到一定值时会移动到老年代。
在 minor GC 时,存活对象大于 to 区域,那么会直接进入老年代。
垃圾回收算法
标记-清除算法(会产生空闲内存碎片)
标记-整理算法(防止产生内存碎片)
复制算法(效率最高,但是内存利用率低)
JVM中新生代使用复制算法,老年代使用的是标记整理算法
关于跨代引用
为了防止不能确定新生代的对象是否被老年代的对象引用而需要进行 full GC,通过 card table 将老年代分成若干区域,所以在 minor GC 时只需要对表中记录的老年代区域进行扫描就可以了。