程序员进阶系列:你真的懂 HelloWorld 吗?
作为入了门的 Java 程序员,相信在脑海中都能够秒写出 HelloWorld.java,都知道编译成 HelloWorld.class,然后就可以跨平台执行了。
常言道:知人知面不知心。敢问,你真的懂 HelloWorld.class 吗?你真的懂她的内心吗?
不清楚,也无所谓,只因有一颗求知的心。
先让慌乱的内心平静下来,跟随小猿的脚步,一起从字节码层面看看 HelloWorld。希望通过此篇分享对字节码文件有个全局的认识,并对 HelloWorld 执行原理有个大致的了解。
1
准备:工欲善其事必先利其器
首先具备 Java 环境(能打开此文章,说明你肯定具备此环境)。
能开发代码的工具(不强求IntelliJ IDEA),然后写出如下图 HelloWorld.java 就可以。
编译 HelloWorld.java 源文件,生成对应的字节码文件。
然后需要一个能查看 class 文件的工具(不强求UltraEdit,只要能查看 16 进制的文件就行,俗称:Hex Viewer),如果按照默认记事本,打开 class 文件的效果是这样子的。
这打开的方式肯定不对,换种开启的方式,用 UltraEdit(本文统称 UE) 进行打开。
虽然不是乱码,但是还是看不懂啊,不过仔细瞧。引入眼帘的便是开头的 CA FE BA BE(咖啡宝贝) ,这个东西叫做魔数。
每个 class 文件的头 4 个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 class 文件。
如若要是这么说下去,估计都会彻底疯掉,换种方式进行分解。
接下来对 HelloWorld.class 文件进行反编译,当然推荐可以使用工具 ClassPy、JavaClassViewer、jclasslib 查看 class 文件结构,本次就用 jdk 自带的命令 javap 来查看 class 文件的结构,并把反编译的内容重定向输出到文件 hello_javap.txt 中。
javap -v HelloWorld.class >> hello_javap.txt
javap 是 Java class 文件分解器,可以反编译,也可以查看 java 编译器生成的字节码,用于分解 class 文件,可以解析出当前类对应的 code 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
上面的所有的准备工作,皆是为了得到 hello_javap.txt 文件。
Classfile /D:/workspace/codeonce/out/production/codeonce/think/twice/code/once/HelloWorld.class
Last modified 2020-8-23; size 578 bytes
MD5 checksum 20602b9ebb70bbd1247c77f3729ec8d5
Compiled from "HelloWorld.java"
public class think.twice.code.once.HelloWorld
SourceFile: "HelloWorld.java"
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello World!
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // think/twice/code/once/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lthink/twice/code/once/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World!
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 think/twice/code/once/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public think.twice.code.once.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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lthink/twice/code/once/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 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
如此这般,天书一样,着实让人头大... ...心莫慌,再次让慌乱的内心平静下来,跟随小猿的脚步,一起去分析字节码文件,尝试彻底搞懂它。
2
解剖:化繁为简,逐个拆解。
一:Classfile 文件信息
Classfile /D:/workspace/codeonce/out/production/codeonce/think/twice/code/once/HelloWorld.class //class文件的路径
Last modified 2020-8-23; size 578 bytes //最后一次修改时间以及该class文件的大小
MD5 checksum 20602b9ebb70bbd1247c77f3729ec8d5 //该类的MD5值
Compiled from "HelloWorld.java" //编译自源文件名
这块感觉不用详细解释,仔细去看,应该都能懂。
第 1 行:class 文件的路径
第 2 行:最后一次修改时间;该 class 文件的大小。
第 3 行:MD5 checksum 值,例如下载文件的场景下会用于检查文件完整性,检测文件是否被恶意篡改。
第 4 行:编译自 HelloWorld.java 源文件。
二:类主体部分定义信息
public class think.twice.code.once.HelloWorld //包名及类名
SourceFile: "HelloWorld.java" //源文件名
minor version: 0 //次版本号
major version: 52 //主版本号,52 对应 JDK 1.8
flags: ACC_PUBLIC, ACC_SUPER //该类的权限修饰符(访问标志)
重点关注第 3、4 两行,为什么要重点关注呢?业务开发中估计多数都遇到过 Unsupported major.minor version 的错误。其实就是通过高版本的 JDK 进行编译(例如 JDK 1.8),然后跑在低版本的 JDK 上(JDK 1.5),就会报版本不支持。
为了使用方便,特意整理一 JDK 各版本图,请拿走不谢。
三:常量池信息
Constant pool: // 常量池,#数字相当于是常量池里的一个索引
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V //方法引用
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; //字段引用
#3 = String #23 // Hello World!
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // think/twice/code/once/HelloWorld //类引用
#6 = Class #27 // java/lang/Object //类引用
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lthink/twice/code/once/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V //返回值
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World!
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 think/twice/code/once/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
#数字相当于是常量池里的一个索引,例如上面代码段里 #1 代表的是一个方法引用,并且该引用由 #6.#20 构成。
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V //方法引用
#6 = Class #27 // java/lang/Object //类引用
#27 = Utf8 java/lang/Object
#20 = NameAndType #7:#8 // "<init>":()V //返回值
#7 = Utf8 <init>
#8 = Utf8 ()V
在 JVM 规范中常量类型定义了很多,本次只汇总遇到的几个。
四:构造方法信息
public think.twice.code.once.HelloWorld();
descriptor: ()V //方法描述符,这里的V表示void
flags: ACC_PUBLIC //权限修饰符
Code:
stack=1, locals=1, args_size=1
0: aload_0 // aload_0 把this装载到了操作数栈中
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable: //行号表
line 3: 0 //源代码的第 3 行,0 代表字节码里的 0
LocalVariableTable: // 本地变量表
Start Length Slot Name Signature
0 5 0 this Lthink/twice/code/once/HelloWorld; // 索引为0,变量名称为 this
descriptor:方法入参和返回描述; flags:访问权限控制符为 public; stack:方法对应栈帧中的操作数栈的深度为 1; locals:本地变量数量为 1; args_size:参数数量为 1; aload:从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶; invokespecial:调用一个初始化方法; LineNumberTable、LocalVariableTable:前者代表行号表,是为调试器提供源码行号与字节码的映射关系;后者代码本地变量表,存放方法的局部变量信息,属于调试信息。
思考一:通过这段字节码信息,印证了一个准则:在没有显示声明构造的情形下,Java 会默认提供无参构造方法。
思考二:虽然是无参构造器,为什么 args_size 的值是 1 呢?是因为无参构造器和非静态方法调用会默认传入 this 变量参数,其中 aload_0 即表示的 this。
五:main 方法的信息
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 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
通过 descriptor 、flags 能直观的能够读懂 main 方法的入参,返回值以及访问修饰符;通过 LocalVariableTable 运行时候的局部变量表,能够看到 main 函数的 args 参数保存在了 LocalVariableTable 中。
3
解剖:main 方法的运行流程。
重点关注 main 方法中的如下指令(红色圈住部分)
(一)指令 getstatic #2
表示从索引位置 2 获取静态变量,而 #2 又是引用 #21.#22 构成。
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; //字段引用
#21 = Class #28 // java/lang/System
#28 = Utf8 java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
兜了一大圈,其实 getstatic #2 指令就是为了拿到输出对象流。
(二)指令 ldc #3
指令 ldc #3 是把常量压入栈中,#3 对应的是字符串 Hello World。
#3 = String #23 // Hello World!
#23 = Utf8 Hello World!
(三)指令 invokevirtual #4
invokevirtual #4 是方法引用,查表过去就是 #24.#25
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#24 = Class #31 // java/io/PrintStream
#31 = Utf8 java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
#24 则是类引用 #31 java/io/PrintStream,#25 则是方法 println((Ljava/lang/String;)V) 的引用,这里其实是在执行打印操作。
最后,贴一个字节码里的指令与源代码的一个对应关系图。
4
寄语写最后
本次,主要对 Java 字节码有个简单的认识,让大家从字节码角度看看 HelloWorld,看似很容易的入门程序,背后的原理确实不简单。希望通过本次分享,大家对 Java 字节码不再陌生,也希望大家能够学以致用,能够亲自去分析 i++、++i ;字符串拼接效率等诸多场景执行原理。
另外,在 Java 的世界里,有 Java Language Specificatio、Java Virtual Machine Specification 两种规范,直译过来就是 Java 语言规范以及 JVM 规范,本次主要参考 JVM 规范。
闲暇之余,推荐大家多读一读:
https://docs.oracle.com/javase/specs/index.html
https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf
好了,本次就谈到这里,一起聊技术、谈业务、喷架构,少走弯路,不踩大坑。会持续输出原创精彩分享,敬请期待!
推荐阅读:
- Mat转换为QImage
- 将多张图片无缝拼接方法
- 【Golang语言社区】H5游戏开发-纯javascript模仿微信打飞机小游戏
- 模式识别---图像二值化
- 双边过滤算法
- C++对于大型图片的加载缩放尝试
- ijg库解码超大型jpeg图片
- JS基础(下)
- Go语言_并发篇
- AttributeError: 'int' object has no attribute 'log'
- makefile在编译的过程中出现“except class name”
- 调参过程中的参数 学习率,权重衰减,冲量(learning_rate , weight_decay , momentum)
- 【Golang语言社区】游戏编程--js开发实现简单贪吃蛇游戏(20行代码)
- mxnet框架样本,使用C++接口
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 自己动手编写一个Mybatis插件:mybatis脱敏插件
- 【每日一题】36. Valid Sudoku
- 【网易云课堂】Java语言程序设计进阶----第一周编程作业
- 11 Confluent_Kafka权威指南 第十一章:流计算
- 简直骚操作,ThreadLocal还能当缓存用
- 品优购(IDEA版)-第一天
- 品优购(IDEA版)-第二天
- 品优购第四天
- 深度学习框架OneFlow的并行特色(附框架源码和教程)
- 图解Java设计模式
- python 如何解决 No module named ‘pip‘问题
- 用多智能体强化学习算法MADDPG解决"老鹰捉小鸡"问题
- 网站日志实时分析之Flink处理实时热门和PVUV统计
- 大数据量下的集合过滤—Bloom Filter
- 实时数仓链路分享:kafka =>SparkStreaming=>kudu集成kerberos