程序员进阶系列:你真的懂 HelloWorld 吗?

做为入了门的 Java 程序员,相信在脑海中都可以秒写出 HelloWorld.java,都知道编译成 HelloWorld.class,而后就能够跨平台执行了。

常言道:知人知面不知心。 敢问,你真的懂 HelloWorld.class 吗? 你真的懂她的心里吗?

不清楚,也无所谓,只因有一颗求知的心。

先让慌乱的心里平静下来,跟随小猿的脚步,一块儿从字节码层面看看 HelloWorld。但愿经过此篇分享对字节码文件有个全局的认识,并对 HelloWorld 执行原理有个大体的了解。

1javascript

 准备:工欲善其事必先利其器   

首先具有 Java 环境(能打开此文章,说明你确定具有此环境)。html



能开发代码的工具(不强求IntelliJ IDEA),而后写出以下图 HelloWorld.java 就能够。java



编译 HelloWorld.java 源文件,生成对应的字节码文件。程序员


而后须要一个能查看 class 文件的工具(不强求UltraEdit,只要能查看 16 进制的文件就行,俗称:Hex Viewer),若是按照默认记事本,打开 class 文件的效果是这样子的。shell



这打开的方式确定不对,换种开启的方式,用 UltraEdit(本文统称 UE) 进行打开。json



虽然不是乱码,可是仍是看不懂啊,不过仔细瞧。引入眼帘的即是开头的 CA FE BA BE(咖啡宝贝) ,这个东西叫作魔数。ruby

每一个 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 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。oracle


上面的全部的准备工做,皆是为了获得 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_SUPERConstant 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 //该类的权限修饰符(访问标志)

重点关注第 三、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.htmlhttps://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf

好了,本次就谈到这里,一块儿聊技术、谈业务、喷架构,少走弯路,不踩大坑。会持续输出原创精彩分享,敬请期待!

推荐阅读:
Java线程池深度揭秘
完全搞懂 Java 线程池,干啥都再也不发憷
Java程序跑的快,全要靠线程带
fastjson的这些坑,你误入了没?
真实|技术人员该如何站好最后一班岗?
Java 8 的这些特性,你知道吗?
改掉这些坏习惯,还怕写不出健壮的代码?(一)
改掉这些坏习惯,还怕写不出优雅的代码? (二)
改掉这些坏习惯,还怕写不出优雅的代码? (三)
改掉这些坏习惯,还怕写不出健壮的代码? (四)
改掉这些坏习惯,还怕写不出精简的代码? (五)
改掉这些坏习惯,还怕写不出精简的代码? (六)

本文分享自微信公众号 - 一猿小讲(yiyuanxiaojiangV5)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索