内容简介:最近看到周志明大神的《深入理解 Java 虚拟机》出了第三版,想想之前看完了第二版,当时处于一知半解的状态,所以趁着这个机会,重新学习,看完了第三版,于是做个记录。class 文件出现的目的是为了平台兼容性,Java 的口号是「一次编写,到处运行」 “Write once,run anywhere”,所以用 Java 这门高级语言的编写 .java 文件后,通过编译器编译输出 .class 这种平台无关的字节码文件,不需要关注是哪个厂商生产的 jvm。
最近看到周志明大神的《深入理解 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 ]
包括:
标志 | 解释 |
---|---|
-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 也有帮助~)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
深入浅出数据分析
Michael Milton / 李芳 / 电子工业出版社 / 2009 / 88.00元
《深入浅出数据分析》以类似“章回小说”的活泼形式,生动地向读者展现优秀的数据分析人员应知应会的技术:数据分析基本步骤、实验方法、最优化方法、假设检验方法、贝叶斯统计方法、主观概率法、启发法、直方图法、回归法、误差处理、相关数据库、数据整理技巧;正文以后,意犹未尽地以三篇附录介绍数据分析十大要务、R工具及ToolPak工具,在充分展现目标知识以外,为读者搭建了走向深入研究的桥梁。 本书构思跌宕......一起来看看 《深入浅出数据分析》 这本书的介绍吧!