JVM字节码之初探

  1. .java.class
  2. 如何查看 .class
  3. javap 命令的使用
  4. 字节码是什么

编译成为class文件

  1. 编写java文件

    1
    2
    3
    4
    5
    6
    7
    package com.yuda;

    public class HelloWorld {
    public static void main(String[] args) {
    System.out.println("Hello World!");
    }
    }
  2. 生成class文件

    1
    $ javac HelloWorld.java
  3. 查看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
    $ xxd HelloWorld.class
    00000000: cafe babe 0000 0034 0022 0a00 0600 1409 .......4."......
    00000010: 0015 0016 0800 170a 0018 0019 0700 1a07 ................
    00000020: 001b 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
    00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
    00000040: 756d 6265 7254 6162 6c65 0100 124c 6f63 umberTable...Loc
    00000050: 616c 5661 7269 6162 6c65 5461 626c 6501 alVariableTable.
    00000060: 0004 7468 6973 0100 154c 636f 6d2f 7975 ..this...Lcom/yu
    00000070: 6461 2f48 656c 6c6f 576f 726c 643b 0100 da/HelloWorld;..
    00000080: 046d 6169 6e01 0016 285b 4c6a 6176 612f .main...([Ljava/
    00000090: 6c61 6e67 2f53 7472 696e 673b 2956 0100 lang/String;)V..
    000000a0: 0461 7267 7301 0013 5b4c 6a61 7661 2f6c .args...[Ljava/l
    000000b0: 616e 672f 5374 7269 6e67 3b01 000a 536f ang/String;...So
    000000c0: 7572 6365 4669 6c65 0100 0f48 656c 6c6f urceFile...Hello
    000000d0: 576f 726c 642e 6a61 7661 0c00 0700 0807 World.java......
    000000e0: 001c 0c00 1d00 1e01 000c 4865 6c6c 6f20 ..........Hello
    000000f0: 576f 726c 6421 0700 1f0c 0020 0021 0100 World!..... .!..
    00000100: 1363 6f6d 2f79 7564 612f 4865 6c6c 6f57 .com/yuda/HelloW
    00000110: 6f72 6c64 0100 106a 6176 612f 6c61 6e67 orld...java/lang
    00000120: 2f4f 626a 6563 7401 0010 6a61 7661 2f6c /Object...java/l
    00000130: 616e 672f 5379 7374 656d 0100 036f 7574 ang/System...out
    00000140: 0100 154c 6a61 7661 2f69 6f2f 5072 696e ...Ljava/io/Prin
    00000150: 7453 7472 6561 6d3b 0100 136a 6176 612f tStream;...java/
    00000160: 696f 2f50 7269 6e74 5374 7265 616d 0100 io/PrintStream..
    00000170: 0770 7269 6e74 6c6e 0100 1528 4c6a 6176 .println...(Ljav
    00000180: 612f 6c61 6e67 2f53 7472 696e 673b 2956 a/lang/String;)V
    00000190: 0021 0005 0006 0000 0000 0002 0001 0007 .!..............
    000001a0: 0008 0001 0009 0000 002f 0001 0001 0000 ........./......
    000001b0: 0005 2ab7 0001 b100 0000 0200 0a00 0000 ..*.............
    000001c0: 0600 0100 0000 0700 0b00 0000 0c00 0100 ................
    000001d0: 0000 0500 0c00 0d00 0000 0900 0e00 0f00 ................
    000001e0: 0100 0900 0000 3700 0200 0100 0000 09b2 ......7.........
    000001f0: 0002 1203 b600 04b1 0000 0002 000a 0000 ................
    00000200: 000a 0002 0000 0009 0008 000a 000b 0000 ................
    00000210: 000c 0001 0000 0009 0010 0011 0000 0001 ................
    00000220: 0012 0000 0002 0013 ........
  4. cafe babe 是class文件的魔数, 用来表示这个文件是个class文件, 很多文件都是以固定的字节开头作为魔数, 比如 PDF为0x255044462D, png文件为0x89504E47.

  5. 0000 0034 魔数后4个字节分别表示副版本号(Minor Version)和主版本号(Major Version), 例子中52(0x34), 是主版本号, 对应了Java8, 其他版本对应如下:

Java 版本 Major Version
Java 1.4 48
Java 5 49
Java 6 50
Java 7 51
Java 8 52
Java 9 53
  1. 紧接着后面是常量池, 分为两部分, 2字节用来保存长度信息和若干个常量池项. 目前Java虚拟机定义了14种常量类型例如 CONSTANT_Utf8_info (1), CONSTANT_Integer_info(3), CONSTANT_Float_info(4), CONSTANT_Long_info(5) CONSTANT_Double_info(6), CONSTANT_Class_info(7),CONSTANT_String_info(8), CONSTANT_Fieldref_info(9), CONSTANT_Methodref_info(10), CONSTANT_InterfaceMethodref_info(11), CONSTANT_NameAndType_info(12), CONSTANT_MethodHandle_info(15), CONSTANT_MethodType_info(16), CONSTANT_InvokeDynamic_info(18). 不同类型的常量项, 数据所占的大小也会不同.
  2. 常量池之后是类访问标记(Access flags), 用来标识一个类是否是final与abstract, 由2个字节表示, 一共16个标志位, 目前仅仅使用了其中8个.
Flag Name Value Interpretation
ACC_PUBLIC 1 标识是否是 public
ACC_FINAL 10 标识是否是 final
ACC_SUPER 20 已经不用了
ACC_INTERFACE 200 标识是类还是接口
ACC_ABSTRACT 400 标识是否是 abstract
ACC_SYNTHETIC 1000 编译器自动生成,不是用户源代码编译生成
ACC_ANNOTATION 2000 标识是否是注解类
ACC_ENUM 4000 标识是否是枚举类
  1. 然后是类、超类、接口索引表, 用来确定类的继承关系.
  2. 字段表(Fields), 类中的字段会保存在这个集合中(不包含方法中定义的变量), 可变长的.
  3. 方法表, 用来表示类中定义的方法, 同样是可变长的.
  4. 属性表, 也是class中最后一部分内容, 是可变长的, 预定义了 23 种属性, 用来表示类中字段表, 方法表等它们的属性信息.

javap命令的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Usage: javap <options> <classes>
where possible options include:
-help --help -? Print this usage message 打印此使用信息
-version Version information 版本信息
-v -verbose Print additional information 详细打印附加信息
-l Print line number and local variable tables 打印行号和局部变量表
-public Show only public classes and members 只显示公共类和成员
-protected Show protected/public classes and members 显示受保护/公共类和成员
-package Show package/protected/public classes
and members (default) 显示package/protected/public类和成员
-p -private Show all classes and members 私有显示所有类和成员
-c Disassemble the code 反汇编代码
-s Print internal type signatures 打印内部类型签名
-sysinfo Show system info (path, size, date, MD5 hash)
of class being processed 显示正在处理的类的系统信息(路径、大小、日期、MD5散列)
-constants Show final constants 显示最终常数
-classpath <path> Specify where to find user class files 指定在何处查找用户类文件
-cp <path> Specify where to find user class files 指定在何处查找用户类文件
-bootclasspath <path> Override location of bootstrap class files 覆盖bootstrap类文件的位置

主要的参数是 -c -v -l -p -s

字节码

Java 虚拟机的指令由一个字节长度的操作码(opcode)和紧随其后的可选的操作数(operand)构成, 所以称作字节码

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.yuda;

/**
* @author canyu
* @data 2020/5/26 23:52
*/
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
private static final String test(){
return "a";
}
}

javap 解析后

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
••• yuda (master) javap -c -v -l -s -p HelloWorld.class
Classfile /E:/JvProject/streamDemo/build/classes/java/main/com/yuda/HelloWorld.class
Last modified 2020-5-27; size 630 bytes
MD5 checksum 2ae8a3ee70267f30e4abc01443db2ff7
Compiled from "HelloWorld.java"
public class com.yuda.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #26 // Hello World!
#4 = Methodref #27.#28 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #29 // a
#6 = Class #30 // com/yuda/HelloWorld
#7 = Class #31 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/yuda/HelloWorld;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 test
#20 = Utf8 ()Ljava/lang/String;
#21 = Utf8 SourceFile
#22 = Utf8 HelloWorld.java
#23 = NameAndType #8:#9 // "<init>":()V
#24 = Class #32 // java/lang/System
#25 = NameAndType #33:#34 // out:Ljava/io/PrintStream;
#26 = Utf8 Hello World!
#27 = Class #35 // java/io/PrintStream
#28 = NameAndType #36:#37 // println:(Ljava/lang/String;)V
#29 = Utf8 a
#30 = Utf8 com/yuda/HelloWorld
#31 = Utf8 java/lang/Object
#32 = Utf8 java/lang/System
#33 = Utf8 out
#34 = Utf8 Ljava/io/PrintStream;
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (Ljava/lang/String;)V
{
public com.yuda.HelloWorld();
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 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/yuda/HelloWorld;

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 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;

private static final java.lang.String test();
descriptor: ()Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
Code:
stack=1, locals=0, args_size=0
0: ldc #5 // String a
2: areturn
LineNumberTable:
line 12: 0
}
SourceFile: "HelloWorld.java"

其中 <opcode> [<operand1>, <operand2>] 格式的就是字节码, 由操作码和操作数组成, 操作码长度只有1个字节, 最多能表示256个类型, 目前已经有200+了. 字节码并不是机器码, 而是对机器码的一层抽象, 可以通过JIT(Just in time) 进一步编译成为机器码, 所以Java可以做到一次编译到处运行.

最后看看结构图

jvm_bytecode.png