

重读 JVM - javac & javap
source link: https://www.sevenyuan.cn/2020/02/16/java/2020-02-16-reread-jvm1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近看到周志明大神的《深入理解 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 $16^1$ + 4 $16^0$ = 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 ]
包括:
最后一个参数 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 也有帮助~)
Recommend
-
652
北京市人大常委会不久前建议,采取措施引进高端人才,减少低端劳动力,以应对北京市人口持续增加的压力。虽然遭到质疑,但有关城区仍是“你说你的、我干我的”,开始对餐馆、洗浴、美容美发、小百货、小建材清理整顿
-
44
-
63
-
69
重读黑名单事件!阿里巴巴史上最震撼的人事地震调查(上)
-
46
重读阿里史上最震撼的人事地震(下):帝国奠基者开始解体
-
19
这次来复习一下常用的 ParNew 和 CMS GC 的概念和一些调优建议 GC GC 全称是 Garbage Collect ,译为 「垃圾回收」,在代码编写过程中,我们new 一个对象后,在使用和结束阶段,都可以不需要关注内存...
-
11
【每日蓝桥】6、一三年省赛JavaC组真题“逆波兰表达式”
-
7
The Anatomy of ct.sym — How javac Ensures Backwards Compatibility Posted at Apr 26, 2021 One of the ultimate strengths of Java is its strong notion of backwards compatibility: Ja...
-
5
CodeAssist A javac APIs-based code editor that supports building Android apps. CodeAssist does not use gradle. Editing the build.gradle file will not do anything. There are currently no plans with using Gradle. Featur...
-
6
功能:读由java语言编写的类和接口的定义,并将它们编译成字节代码的class文件。javac 可以隐式编译一些没有在命令行中提及的源文件。 1. 命令结构 javac [ options ] [ sourcefiles ] [ @files ] 参数可...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK