一个.java文件的"沉浮"之路
本文全为原创内容,转载请注明出处:https://www.jianshu.com/p/af78a314c6fc
1:引言:
由于昨天的网络问题,导致分享做的不是特别好。所以老薛这里写了一份分
享的笔记,希望可以帮助那些没有听课的同学稍稍弥补一下。
昨天的分享大概分为一下几个模块来讲解的:
i:分享初衷
ii:分享内容大纲
iii:建议
iv:一个.java文件的沉浮之路
a:什么是计算机?
b:计算机发展历史
c:.java文件如何变为.class文件
d:.class文件如何被JVM加载,加载内容如何解析
e:jvm和操作系统和底层硬件的简单交互。
2:分享初衷:
很多人遇到一个很尴尬的问题:一看就会,一问就懵,一敲就傻。
新技术层出不穷,到底学习什么?如何取舍。
业务代码很多,编写代码不够优,不知道如何增加自己的硬编码实例。
每天都很忙,不知道如何利用自己的碎片化时间武装自己的知识体系。
基于以上内容,希望能够找到一些技术。分享给大家,而这些技术必须具备如下特征:
可能实用价值不高,但是希望能够在未来很长一段时间对你而言都有帮助
可以很好的和目前的编码统一起来,不能由于新的技术加入,导致和目前的学习工作没有关系
3:分享内容大纲
基于刚才的内容,我大概在自己有限的学习数据库中整理了一下。我会在以下内容开始分享:
‘瞎’侃计算机组成与系统结构
‘胡’说数据结构和算法
‘嗨’聊计算机网络
实战编码:
你真的会写代码吗? 如何写出优雅高效的代码!
什么是面向对象?能够通过面向对象的方式去看待问题,而非将对象作为一个Data
手撕系列!能够通过java 写一些好玩的内容。比如服务器,jvm,Struts,Spring等
4:建议
大家也能看到我这里所有的内容都是加了一个字,所以我尽自己的最大能力把这个东西分享的好玩一点。并且也希望大家意识到,我和大家分享的内容只是我的理解,千万不能全信。
我希望如果你真的想和我一起增强自己的知识脉络,最好能做到以下几件事情:
存疑+坚持+广而告之+参与感
1:存疑 任何知识传播都是会存在差异性的。所以千万不能全信,要有自己的主观判断。
但是基于28法则,大概率我聊的80%内容你应该是要信的。嘿嘿~~
2:坚持 这个就不谈了,一句话总结,世界上最难的事情就是坚持,最简单的事情就是放弃。
3:广而告之 我希望我分享的内容你能够有一个落地,笔记,博客,一定要形成一个书面的内容。
4:参与感 千万记得,免费的东西是最贵的,因为耗费了你的时间,所以我会努力做的更好,而
如果你也认可这件事情,请一定要参与进来,而不是作为一个旁观者。
如果这些你都能做到,那么就和我一起并肩前行吧!
5:.java文件的沉浮之路
接下来进入到这次分享的主要内容,看看小J同学在执行过程中都经历了什么!
编写一个.java文件然后依次编译,执行。过程如下:
5-1:编写一个.java文件
5-2:通过javac和java指令执行
这个运行过程就不赘述了,我们直接上图:
5-3:揭秘第一部分.java文件如何变为.class文件
编译器遵守什么规则,将.java文件编译为.class文件
5-3-1:编译规则:
5-3-2:词法分析:
.java文件编译为.class文件时,先进行词法分析 如下图:
总结:这里着重解析.java文件编写的第5行代码,可以将例子后的代码理解为java中的第5行代码。
在进行词法分析时,会将内容进过线性分析,变成一张符号表。符号表为上图中的右侧内容。右侧的表格内容由三部分构成:
1: 唯一表示 1,2,3 类似符号表中的地址编号
2: 变量名称 比如:标识符position,操作符号 + 等等
3: 变量的值
由词法分析我们将整个.java文件中的内容变成了一张表。接下来:
5-3-2:语法分析:
词法分析变为一张符号表,继续语法分析
总结:通过形成的符号表,会生成一颗语法二叉树。在这里有一个很有意思的东西,就是我们观察二叉树,发现符号作为了根节点,而且+号的节点在*
号的节点之上。这里解析时倒叙解析。那么为什么*
号会在+号节点之下,其实如果你要定义一个语言时,一定要告知这个语言一定是一个形式化的,这个形式化的方式可以通过BNF
定义。在形式化的定义规则里,*
号的优先级别高于+
号。
`BNF`巴科斯范式(BNF: Backus-Naur Form 的缩写)。引入一种形式化符号来描述给定语言的
语法。就是通过BNF来定义语言的语法,按照定义的规则去形成编译时的语法分析。
扩展:
在双引号中的字("word")代表着这些字符本身。而double_quote用来代表双引号。
在双引号外的字(有可能有下划线)代表着语法部分。
尖括号( < > )内包含的为必选项。
方括号( [ ] )内包含的为可选项。
大括号( { } )内包含的为可重复0至无数次的项。
竖线( | )表示在其左右两边任选一项,相当于"OR"的意思。
::= 是“被定义为”的意思。
这部分内容主要涉及编译原理,有兴趣可以自行参考:
参考博文
5-3-3:java的bnf定义:
总结:大家可以根据上面的bnf扩展语法,我们发现java的bnf定义如果要声明一个类class_declaration
被这样定义:
需要编写一个修饰符,修饰符的定义:modifier:=="public"|"private"...
,这里我们可知定义类的时候必须时这些关键词,关键词已经给出来了。如果你写的class的修饰符不是modifier定义的词,那么编译时语法就会报错。依次类推。“class”
是必须要写的,同理如果定义类是不写则编译语法报错。在到后面的identifier
,也是有对应定义的值:identifier:=="a..z,$,_"<"...."
。其实这个不就是java规范中对于标识符的定义吗?不能以数字开头。其实要编写一门语言。必须要有形式化的bnf定义。
5-3-4:语义分析:
语义分析,注意计算机的语义分析一定要知道,计算机语言不能存在
二意性
。这个和自然语言不通。
总结:在语义分析是会做类型检查,控制流检查,类型检查等等。我们回想之前的java程序,在进行*
发运算时,由于rate时float类型,而60时一个int类型的字面值
。此时会将60这个int类型转换为float,其实类型转换是在编译时发生的。回想我们编写程序时的基本数据类型的类型转换错误,是不是发生在编译阶段呢?
5-3-5:中间代码生成,优化以及生产对应的目标文件:
产生最后的目标文件
总结:经过语义分析的语法二叉树开始准备生产对应的中间代码。这里我们发现第一行代码:temp1=int_to_float(60)
,注意,这里的语法二叉树相当于只是在二叉树上给了一个标示,说明60需要做类型转换。而将60转换为float这个是在生成中间代码中得到实际体现的。第二行代码temp2=id3*temp1
,id3是什么意思呢?如果你还记得符号表的组成部分,那么这个应该不能猜出来。它就是符号表中的唯一表示。id为3的符号表,id为3的符号表中的变量为rate,对应的值为3.1。然后继续操作,直到中间代码生成结束。
我们发现中间的代码有一些代码是不需要做的,这里做代码优化。将一部分不需要的代码省略。
最后生成目标的代码,这里因为截图不是java代码,所以最后显示的目标文件是一个汇编指令。movl id3 R2
,意思就是将id3的值也就是rate中的值存储到寄存器R2中,MUL #60.0,R2
,意思就是将60.0和R
2寄存器中的值进行相乘,将结果存放到R2寄存器。依次类推。movl R1,id1
,意思将R1寄存器中的值存储到id1中,id1其实就是变量position,值为R1寄存器的值。
到此,我们的.class文件也就生成了。
5-4:揭秘第二部分.class文件如何被java指令开始解析执行的。
5-4-1:操作系统执行可执行程序,调度计算机
5-4-1-1:计算机结构:
总结:
1:我们编写的一个不论是.java文件编译也好,解释执行也罢。首先都是存储到磁盘上的。
2:我们通过键盘输入javac,开始执行编译指令。
3:javac指令被操作系统读取,然后操作系统调度计算机,计算机中的io总线识别到本次调度指令,
然后通过io桥通知cpu,cpu将磁盘上的.java文件通过io桥链接io总线,将磁盘数据读取到主存当中。
4:然后.java文件准备开始编译。主存中的数据会被读取到cpu中的高速缓冲设备中,进行编译。
5:编译之后的.class文件,通过io桥链接io总线,被写出到磁盘。
6:如果编译时有语法错误,那么cpu通过io桥,链接io总线上的图形适配器,显示报错信息
当然这里链接以及调度的方式如果深究还是挺复杂的,暂时我们不做讨论,简单点就是"连找发"三元素理论。如果你不是本专业的,其实目前来讲这个对于我们不太重要,后续老薛会稍微展开再聊聊。当然,也可以参考下面的博文:参考博文地址。
这里之所以采用IO总线
的方式链接各个设备,原因是很方便我可以删除,增加设备,你如果使用辐射式链接,就要主机连接各个设备,每个设备都有一套自己的控制线路,很麻烦。我们可以不防将总线理解为一个链表,而辐射式理解为数组。并且总线很清晰能够规划出各个设备之间的关系,但是辐射的话,设备的相连关系会很复杂。
5-4-1-2:计算机结构遗留问题:
总结:计算机的产生是基于图灵计算模型
,而根据数据和指令集存放位置的不同分为了:芬诺依曼结构
,哈弗结构
。有兴趣大家可以稍微展开看看,老薛后续会展开来聊这两个东西。
5-4-2:.class文件如何执行
注意:jvm的加载过程不是本次分享的内容,老薛后续会展开聊,我们这次主要聊的点是,为什么jvm虚拟机可以去执行.class文件,这个文件中到底有什么东西?
总结:在整个执行过程中,我们发现一个程序编译为字节码文件之后,其实就是一大堆的二进制数据。这里字节码展示是通过16进制查看的。然后进过jvm加载,解析,在jvm中构建一个运行时结构,然后开始执行。我们接下来就编写程序,以及通过反汇编指令去深究一下.class文件中到底有哪些内容。
5-4-2-1:将我们之前编写的Demo.java编译之后进行读取:
注意:通过流的方式将Demo.class读取进来,且按照16进制显示数据如下
下图是16进制对应一览表
总结:
1: u:代表无符号位,u4代表无符号4个字节用来代表魔数。意思凭什么能够确定当前文件是.class呢?
由魔数确定。也就意味着.class文件的魔数为`cafebabe`,俗称`咖啡宝贝`。你也可以读取一下
jpg或者其他类型的文件的前4个字节是什么。
2: 后续的u2,无符号位2个字节代表次版本号,那就是以为当前次版本号为0
3: 后续的u2,无符号位2个字节代表主版本号,那么这里是是00 35,也就是53,也就是jdk9.
JDK各个版本对应关系
JDK 1.10= 54 JDK 1.9 = 53 JDK 1.8 = 52 JDK 1.7 = 51 JDK 1.6 = 50
JDK 1.5 = 49 JDK 1.4 = 48 JDK 1.3 = 47 JDK 1.2 = 46 JDK 1.1 = 45
4:扩展一下,这里采用的是大端模式,关于大小端我之前有聊过。大家也可以参考资料再看看。
其实采用大端有个地方特别好,这样我们再解析这个16进制内容就稍微方便一点,直接可以省略开头为
0的字节。
5: 后续的u2,无符号2个字节代表常量池个数,注意这里的常量池和我们一般意义下的内存中的常量池
还是有区别的,这里的常量池是包括所有字面常量,比如变量,类名,方法名等等。这里是00 30,那么
也就也为者有48个常量,往后数48个字节,都代表的是常量值。这里注意,48要加上本身常量池的个数 也会占空间,也就以为着一共只有47个常量。
6: 再往后的u2,代表的是访问标志,该类的修饰符,以及是否可以使用比如多态,如果可以使用那么意 味着就可以使用invokespecial,这个是java字节码调用指令,代表可以调用私有实例方法,构造器以 及使用super调用父类的实例方法等。剩下的依次类推。
大小端资料参考
通过javap -c -v反汇编,查看内容:
以下内容中两个`中间的内容是老薛写的注释
Classfile /Users/iongst/Desktop/Demo.class `class文件所在地址`
Last modified 2018年9月8日; size 884 bytes `最后一次修改时间以及大小`
MD5 checksum 8619092419ab6d93efe90de9a29f5e36 `md5值`
Compiled from "Demo.java" `编译自那个文件`
public class Demo `类的声明`
minor version: 0 `次版本号`
major version: 53 `主版本号 jdk9`
flags: (0x0021) ACC_PUBLIC, ACC_SUPER `访问标志,acc_public 代表是public修饰,acc_super 代表可以使用invokespecial `
this_class: #7 // Demo `thisclass thisclass指向了#7的地址,可以看看#7是什么`
super_class: #8 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 3 `对照进制一览表,分别代表接口个数为0,没有实现接口
fields字段个数为0 方法个数2个,属性有3个。问题2:这里的fields和attributes代表什么含义?`
Constant pool: `常量池`
#1 = Methodref #8.#17 // java/lang/Object."":()V
#2 = Float 3.1f
#3 = Float 60.0f
#4 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#5 = InvokeDynamic #0:#23 // #0:makeConcatWithConstants:(F)Ljava/lang/String;
#6 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = Class #26 // Deme `存储了#26 继续往下找`
#8 = Class #27 // java/lang/Object
#9 = Utf8
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 SourceFile
#16 = Utf8 Demo.java
#17 = NameAndType #9:#10 // "":()V
#18 = Class #28 // java/lang/System
#19 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#20 = Utf8 BootstrapMethods
#21 = MethodHandle 6:#31 // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#22 = String #32 // 计算结果是:\u0001
#23 = NameAndType #33:#34 // makeConcatWithConstants:(F)Ljava/lang/String;
#24 = Class #35 // java/io/PrintStream
#25 = NameAndType #36:#37 // println:(Ljava/lang/String;)V
#26 = Utf8 Demo `就存储了类名,问题1:为什么要存储类名?`
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Methodref #38.#39 // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#32 = Utf8 计算结果是:\u0001
#33 = Utf8 makeConcatWithConstants
#34 = Utf8 (F)Ljava/lang/String;
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (Ljava/lang/String;)V
#38 = Class #40 // java/lang/invoke/StringConcatFactory
#39 = NameAndType #33:#44 // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#40 = Utf8 java/lang/invoke/StringConcatFactory
#41 = Class #46 // java/lang/invoke/MethodHandles$Lookup
#42 = Utf8 Lookup
#43 = Utf8 InnerClasses
#44 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#45 = Class #47 // java/lang/invoke/MethodHandles
#46 = Utf8 java/lang/invoke/MethodHandles$Lookup
#47 = Utf8 java/lang/invoke/MethodHandles
`常量池个数一共有47个,所以虽然我们查看是48个是包含本身常量池的1的所以是48`
{
public Demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #2 // float 3.1f
5: fstore_2
6: iload_1
7: i2f
8: fload_2
9: ldc #3 // float 60.0f
11: fmul
12: fadd
13: fstore_3
14: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
17: fload_3
18: invokedynamic #5, 0 // InvokeDynamic #0:makeConcatWithConstants:(F)Ljava/lang/String;
23: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
26: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 6
line 6: 14
line 7: 26
}
SourceFile: "Demo.java"
InnerClasses:
public static final #42= #41 of #45; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #21 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#22 计算结果是:\u0001
文末总结:
大家如果真的能耐着性子把这个文章看完,在我看来真的很厉害了。后面也埋了一些坑,比如后面的方法调用,我这里一共添加了108号反编译之后的代码,后面很多代码老薛也没有讲解,这个作为后续内容。那么我们是不是可以这样理解,如果我们将一个.class文件读取进来之后,按照对照一览表
开始解析,只要我们能够分析出来运行时数据区,我们完全是可以自己写一个jvm的。这是没有问题哦。
如果大家有什么问题,大家可以在文末或者是微信私聊老薛。老薛微信 lukun0402
本章内容参考书籍资料:《编译原理》 《深入理解计算机系统》 《码农翻身》
课后作业:
- 我们知道cpu是个快速设备,内存稍次之,磁盘排第三,如果你走网络会更慢,那么整个执行过程其实是通过最低速设备决定的,你有什么比较好的办法能够解决这个问题吗?
- 给大家布置了数据结构的系统,如果兴趣的同学可以做一做。作业地址。也可以复制链接:https://github.com/iongst/dataStructure
- 在作业中也留了一些作业,编写数据结构时,在老薛写的接口中,已经留了思考的彩蛋,大家努力查找吧。