.java文件编译过程和执行过程分析以及计算机简单认识

一个.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文件

.java文件编译过程和执行过程分析以及计算机简单认识_第1张图片
编写一个.java文件

5-2:通过javac和java指令执行

这个运行过程就不赘述了,我们直接上图:

.java文件编译过程和执行过程分析以及计算机简单认识_第2张图片
java跨平台原理

5-3:揭秘第一部分.java文件如何变为.class文件

编译器遵守什么规则,将.java文件编译为.class文件

5-3-1:编译规则:

.java文件编译过程和执行过程分析以及计算机简单认识_第3张图片
编译过程

5-3-2:词法分析:

.java文件编译为.class文件时,先进行词法分析 如下图:

.java文件编译过程和执行过程分析以及计算机简单认识_第4张图片
词法分析

总结:这里着重解析.java文件编写的第5行代码,可以将例子后的代码理解为java中的第5行代码。
在进行词法分析时,会将内容进过线性分析,变成一张符号表。符号表为上图中的右侧内容。右侧的表格内容由三部分构成:

1: 唯一表示 1,2,3 类似符号表中的地址编号
2: 变量名称 比如:标识符position,操作符号 + 等等
3: 变量的值

由词法分析我们将整个.java文件中的内容变成了一张表。接下来:

5-3-2:语法分析:

词法分析变为一张符号表,继续语法分析

.java文件编译过程和执行过程分析以及计算机简单认识_第5张图片
语法分析

总结:通过形成的符号表,会生成一颗语法二叉树。在这里有一个很有意思的东西,就是我们观察二叉树,发现符号作为了根节点,而且+号的节点在*号的节点之上。这里解析时倒叙解析。那么为什么*号会在+号节点之下,其实如果你要定义一个语言时,一定要告知这个语言一定是一个形式化的,这个形式化的方式可以通过BNF定义。在形式化的定义规则里,*号的优先级别高于+号。

`BNF`巴科斯范式(BNF: Backus-Naur Form 的缩写)。引入一种形式化符号来描述给定语言的
语法。就是通过BNF来定义语言的语法,按照定义的规则去形成编译时的语法分析。
    扩展:
        在双引号中的字("word")代表着这些字符本身。而double_quote用来代表双引号。
        在双引号外的字(有可能有下划线)代表着语法部分。
        尖括号( < > )内包含的为必选项。
        方括号( [ ] )内包含的为可选项。
        大括号( { } )内包含的为可重复0至无数次的项。
        竖线( | )表示在其左右两边任选一项,相当于"OR"的意思。
        ::= 是“被定义为”的意思。
这部分内容主要涉及编译原理,有兴趣可以自行参考:

参考博文

5-3-3:java的bnf定义:

.java文件编译过程和执行过程分析以及计算机简单认识_第6张图片
java的bnf定义

总结:大家可以根据上面的bnf扩展语法,我们发现java的bnf定义如果要声明一个类class_declaration被这样定义: 需要编写一个修饰符,修饰符的定义:modifier:=="public"|"private"...,这里我们可知定义类的时候必须时这些关键词,关键词已经给出来了。如果你写的class的修饰符不是modifier定义的词,那么编译时语法就会报错。依次类推。“class”是必须要写的,同理如果定义类是不写则编译语法报错。在到后面的identifier,也是有对应定义的值:identifier:=="a..z,$,_"<"...." 。其实这个不就是java规范中对于标识符的定义吗?不能以数字开头。其实要编写一门语言。必须要有形式化的bnf定义。

5-3-4:语义分析:

语义分析,注意计算机的语义分析一定要知道,计算机语言不能存在二意性。这个和自然语言不通。

.java文件编译过程和执行过程分析以及计算机简单认识_第7张图片
语义分析

总结:在语义分析是会做类型检查,控制流检查,类型检查等等。我们回想之前的java程序,在进行*发运算时,由于rate时float类型,而60时一个int类型的字面值。此时会将60这个int类型转换为float,其实类型转换是在编译时发生的。回想我们编写程序时的基本数据类型的类型转换错误,是不是发生在编译阶段呢?

5-3-5:中间代码生成,优化以及生产对应的目标文件:

产生最后的目标文件

.java文件编译过程和执行过程分析以及计算机简单认识_第8张图片
中间代码生成,优化以及生产对应的目标文件

总结:经过语义分析的语法二叉树开始准备生产对应的中间代码。这里我们发现第一行代码: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:计算机结构:
.java文件编译过程和执行过程分析以及计算机简单认识_第9张图片
中间代码生成,优化以及生产对应的目标文件

总结:

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文件,这个文件中到底有什么东西?

.java文件编译过程和执行过程分析以及计算机简单认识_第10张图片
执行过程

总结:在整个执行过程中,我们发现一个程序编译为字节码文件之后,其实就是一大堆的二进制数据。这里字节码展示是通过16进制查看的。然后进过jvm加载,解析,在jvm中构建一个运行时结构,然后开始执行。我们接下来就编写程序,以及通过反汇编指令去深究一下.class文件中到底有哪些内容。

5-4-2-1:将我们之前编写的Demo.java编译之后进行读取:

注意:通过流的方式将Demo.class读取进来,且按照16进制显示数据如下

.java文件编译过程和执行过程分析以及计算机简单认识_第11张图片
读取16进制内容

下图是16进制对应一览表

.java文件编译过程和执行过程分析以及计算机简单认识_第12张图片
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
  • 在作业中也留了一些作业,编写数据结构时,在老薛写的接口中,已经留了思考的彩蛋,大家努力查找吧。

你可能感兴趣的:(.java文件编译过程和执行过程分析以及计算机简单认识)