重读 JVM - javac & javap
点击上方☝SpringForAll社区 轻松关注!
本文来源:http://r6d.cn/aaLRd
最近看到周志明大神的《深入理解 Java 虚拟机》出了第三版,想想之前看完了第二版,当时处于一知半解的状态,所以趁着这个机会,重新学习,看完了第三版,于是做个记录。
class
引入
class 文件出现的目的是为了平台兼容性,Java 的口号是「一次编写,到处运行」 “Write once,run anywhere”,所以用 Java 这门高级语言的编写 .java 文件后,通过编译器编译输出 .class 这种平台无关的字节码文件,不需要关注是哪个厂商生产的 jvm。
在上图中,实现平台无关性的核心在于虚拟机和字节码存储格式的 .class 文件,了解到,通过其它语言编写的程序也能在 jvm 上运行,例如 ruby、groovy 语言等,是通过 jruby、groovyc 编译器,输出字节码格式的 .class 文件,最终能够在 jvm 上运行。
.java -> .class, javac
从编写的 .java 文件到 .class 文件,可以通过 javac 命令进行编译
例如编写一个 TestClass.java
package cn.sevenyuan;
public class TestClass {
private int number;
public int inc() {
return number + 1;
}
}
编译语句:(加了 -verbose 是可以在输出设备上显示虚拟机运行信息)
$ javac -verbose TestClass.java
其中,package 包名随意,文件名记得要与类名一致,不然编译时将会报错,例如文件名为 TestClass.java,但是类名是 class Test,编译错误如下:
$ javac -verbose TestClass.java
TestClass.java:3: 错误: 类Test是公共的, 应在名为 Test.java 的文件中声明
public class Test {
^
1 个错误
class 文件格式
类加载器读取的是 .class 文件,在日常代码编写的时候,的确不需要关注它,但为了深入学习和了解它的结构,可能之后会使用到,所以这里做个记录。
class 文件是一组以 8 个字节为基础单位的二进制流,每个数据项严格按照顺序紧凑地排列在文件中,中间没有间隔符。
下图使用的是 UltraEdit
这个软件,打开 .class 字节码文件的内容(这里来复习一下计算机的字节码格式,一个字节有 8 位,每一位是 0 或 1,是机器能够识别的二进制语言)
打开文件能看到里面是 16 进制的文本信息
magic number
前四个字节「cafebabe」:是一个魔数,它的唯一作用就是表示该文件能否被 jvm 识别,关于它的小故事可以另外搜索一下~
minor version & major version
魔数后面的四个字节:第五和第六的「00 00」表示次版本号(minor version),第七和第八字节「00 34」表示的是主版本号(Major version),第一代 jvm 1.1 的版本号是 45,十六进制的 0x34 转换成十进制为 3 + 4 = 52,所以与第一代相隔 7 个版本, 表示我使用的是 jdk8,第八代 jvm。
设置版本号的原因是,jvm 不能执行比自己版本高的 class 文件,也就是说,如果使用 jdk9 编译的代码,是不能再 jvm8 上运行的,但可以向下兼容,使用 jdk7 编译的代码,能在 jvm8 上运行。
如果用低版本 jdk 运行高版本的 class 字节码,将会报以下错误:
常量池 constant pool
在次主版本号后面,是常量池入口,常量池可以用来比喻为 class文件里的资源仓库。由于常量池中常量的数量不是固定的,所以在入口处需要告知常量池中有多少个常量。
而且下标起点与常规的 java 习惯不太一样,它的下标是从 1 开始的,入口位置在 class 文件的偏移地址:0x00000008
详细数据项对照表请参考书中的 6-3 配图
类型 | 标志 | 说明 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
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 | 11 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NumberAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 表示方法类型 |
CONSTANT_Dynamic_info | 17 | 表示一个动态计算常量 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
CONSTANT_Module_info | 19 | 表示一个模块 |
CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 |
常量池中每一项常量都是一个表,每种不同类型都能从常量表中找出对应项。表中的 tag 和 value,tag 表示它的类型,value 就是它的值。
我是这样理解常量池中的数据项,tag info,类比于 String name 这种编程习惯,前面是类型修饰符,后面是它的值。
数据项之间有着完全不同的结构,如果要手工参考这么多张表找出实际含义,有点费眼,所以推荐下面这个字节码反编译工具:javap
分析工具 javap
简介
javap 全称是 Java class file disassembler
,/jdk/bin 目录下的字节码反编译工具,使用该工具,可以反编译出当前类对应的类名、版本号、常量池和代码区(code)等信息,反编译出来的信息更加清晰和直观。
通过 man javap
命令就能在终端下初步了解 javap
的用法
使用方式:javap [ options ] class
其中, 可能的选项 [ options ]
包括:
标志 | 解释 |
---|---|
-help –help -? | 输出此用法消息 |
-version | 版本信息 |
-v -verbose | 输出附加信息 |
-l | 输出行号和本地变量表 |
-public | 仅显示公共类和成员 |
-protected | 显示受保护的/公共类和成员 |
-package | 显示程序包/受保护的/公共类和成员 (默认) |
-p -private | 显示所有类和成员 |
-c | 对代码进行反汇编 |
-s | 输出内部类型签名 |
-sysinfo | 显示正在处理的类的 系统信息 (路径, 大小, 日期, MD5 散列) |
-constants | 显示最终常量 |
-classpath | 指定查找用户类文件的位置 |
-cp | 指定查找用户类文件的位置 |
-bootclasspath | 覆盖引导类文件的位置 |
最后一个参数 class
,是前面编译后的文件,输入时不需要带上 .class 后缀
查看反编译后的结果
拿开头编译出来的 TestClass.class
试验
$ javap -verbose TestClass
Classfile /Users/jingqi/Deploy/Project/VSCode/TestClass.class
Last modified 2020-2-16; size 293 bytes
MD5 checksum 1b9eeadb7d1396ca4fa706e0b0bc7ac8
Compiled from "TestClass.java"
public class cn.sevenyuan.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // cn/sevenyuan/TestClass.number:I
#3 = Class #17 // cn/sevenyuan/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 number
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // number:I
#17 = Utf8 cn/sevenyuan/TestClass
#18 = Utf8 java/lang/Object
{
public cn.sevenyuan.TestClass();
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 3: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0 // 0处的局部变量中的objectref被压入操作数堆栈,这里的是 this 对象
1: getfield #2 // Field number:I,从操作数堆栈中弹出的引用类型为objectref,这里获取的是 number 对象引用
4: iconst_1 // 将int常量 1 压入操作数堆栈
5: iadd // 弹出栈中的 number 值和 int 常量 1,进行加操作,并将结果压入栈
6: ireturn // 从方法返回int
LineNumberTable:
line 7: 0
}
SourceFile: "TestClass.java"
在输出信息头部,能看到 minor version
、 major version
和 Constant pool
等前面提到的信息,比根据字节码去查找一一对应看得更舒适。
刚开始看代码去里的 aload_0 、iadd 和 iconst_1 等可能有些疑惑,反编译出来 JVM
指令集可以参考 oracle
官方文档:The Java Virtual Machine Instruction Set
例如 aload_0 指令可以这样搜索查看:
参考文档后,可以大致理解我们 inc()
方法在操作系统下底层的逻辑:
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0 // 0处的局部变量中的objectref被压入操作数堆栈,这里的是 this 对象
1: getfield #2 // Field number:I,从操作数堆栈中弹出的引用类型为objectref,这里获取的是 number 对象引用
4: iconst_1 // 将int常量 1 压入操作数堆栈
5: iadd // 弹出栈中的 number 值和 int 常量 1,进行加操作,并将结果压入栈
6: ireturn // 从方法返回int
LineNumberTable:
line 7: 0
小结
常规开发中,使用的是 java 高级语言,可能没有多少关注到 jvm 底层执行逻辑,这次了解学习 class 字节码,直接查看十六位进制文件有点吃力,所以通过 javap
命令来查看反编译后的信息,学习 jvm 指令集。
通过简单对比后,了解到简单的 inc() 方法,里面一行的 return number + 1
代码,经过反汇编之后,原来经历了
this 对象入栈 number 对象引用入栈 整型常量 1 入栈 对象出栈,两者相加后,将结果压入栈 最后弹出栈信息
机器只认识操作码,简单的数值加一经过反编译后,可以看到里面的局部变量表、常量池和操作数栈,机器后续一系列复杂操作都从中可以窥探,所以了解学习字节码格式,之后学习操作系统会有一定的帮助(或者说两者可以互补,操作系统知识对学习 jvm 也有帮助~)
资料参考
深入理解Java Class文件格式(一) 通过javap命令分析java汇编指令
墙裂推荐
【深度】互联网技术人的社群,点击了解!
关注公众号,回复“spring”有惊喜!!!
如果资源对你有帮助的话