Java虚拟机(JVM)的理解

本文详细介绍了Java虚拟机(JVM)的工作原理,从编译Java源代码到生成字节码,再到使用javap反编译查看字节码的结构和方法。讲解了字节码的结构,包括操作数栈、本地变量表和方法调用,以及JVM运行时数据区域,如堆、方法区、本地方法栈、虚拟机栈和程序计数器。同时,探讨了垃圾回收机制,如新生代、老年代的划分,以及不同垃圾回收算法。此外,还涉及了内部类的结构和字节码指令表。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 时只需要对表中记录的老年代区域进行扫描就可以了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值