【今日最佳】对于程序员而言,所谓的二八定律指的是 花百分之八十的时间去学习日常研发中不常见的那百分之二十的原理。
据说阿里某程序员对书法十分感兴趣,退休后决定在这方面有所建树。于是花重金购买了上等的文房四宝。
一日,饭后突生雅兴,一番磨墨拟纸,并点上了上好的檀香,颇有王羲之风范,又具颜真卿气势,定神片刻,泼墨挥毫,郑重地写下一行字:hello world。
当然了,这是个专属程序员的段子哈哈哈。
那么问题来了,写了这么久的Hello World,大家确定自己了解自己写的东西背后是什么原理吗?(o≖◡≖)
【给出2分钟,该知识点涉及到了Java程序执行流程,包括编译、加载和执行,你是否能够理清呢?】
接下来进入严肃时间 (@ ̄ー ̄@)
与众不同的Hello World
public class Main {
private static String word = "Hello World!";
public static void main(String[] args) {
new Main().say();
}
private void say() {
System.out.println(word);
}
}
整个代码的执行过程可以分为三个阶段:
- 代码编译
- 类加载
- 类执行
代码编译
代码编译的作用就是将我们编写的 Main.java文件转化为Main.class文件,.class在这里又被称为字节码文件,打开就是一堆的火星文【反正就是看不懂】,在这里我们可以将编译的过程看作生产JVM原料的过程,使用的工具就是jdk提供的工具javac。
大致流程如下:
- 词法分析,即将源代码的字符流转变为Token集的过程。白话文描述下,就是在我们实际编程中,单个字符是最小单位,而实际上在编程过程中,标记才是最小单位,如关键字、变量名、字面量、运算符等都可以成为Token,貌似还是有点蒙蔽,举个例子(>﹏<),比如整型int在我们编程中它就是三个字符组成的,就是i、n、t 三个字符,而在编译过程中它就是一个Token,不可拆分。
这个过程对我们来说其实是完全屏蔽的,但是实际上它是现代经典编译原理的
套路,词法分析也是为了给后面编译做准备的】
- 语法分析,通过词法分析拿到Token集后,下一步就是构建抽象语法树了,所谓的抽象语法树其实就是一种用来描述程序代码语法结构的树形表示方式,其中语法树的每一个节点都代表着程序代码中的一个语法结构,如包、类型、修饰符、运算符等。
在我们眼中,Main.java已经可以清晰理解到底写的是什么东西了,但是对于JVM来说还是一脸懵逼的,所以才需要构建成语法树,在这一步后就不会再对源码文件进行操作了,后续的操作都建立在抽象语法树上
-
填充符号表,符号表是由一组符号地址和符号信息构成的表格,这个表格在编译的不同阶段都会被用到,如在目标代码生成阶段,会对符号名进行地址分配,而符号表就是地址分配的依据。
-
语义分析,语义分析阶段也可以说是语义检测阶段,上面说到语法分析会构建一棵语法树,那么这棵语法树是否是正确合理的,就由语义分析来做了,语义分析会通过标注检查和数据及控制流分析检查两步入手,在生成字节码的最后一步信息把关。
-
字节码生成,这是javac编译过程的最后一个阶段了,字节码生成阶段并不只是简简单单的将前面各个步骤生成的信息转化成字节码然后放入磁盘中,还进行了少量的代码添加和转换工作,如我们自己重载的构造函数。
编译期到这里就结束了,那么由谁来将这些原料传输给JVM虚拟机呢?这个时候就要看看类加载的过程了。
类加载
类加载简单来说就是将由类加载器将编译后的字节码文件【Main.class】加载到虚拟机中
,那么自然而然的,要先介绍下四种类加载器
说说四种类加载器
可以从上图中看出,类加载器可以分为四种,而第四种是由我们自己实现的,那么其他三种由JVM提供的类加载在我们启动该Main程序的过程中起到了什么作用呢?
首先说说启动类加载器 Bootstrap ClassLoader ,启动类加载器的作用主要是加载 %JAVA_HOME%\jre\lib\rt.jar 类库,将其加载到虚拟机内存中,那么rt.jar类库到底有什么作用呢?rt.jar下包含了Java的基础类库,也就是Java doc里面看到的所有的类的class文件,感兴趣的朋友可以自己打开目录看下。
其次是扩展类加载器 Extension ClassLoader ,扩展类加载器的作用主要是负责加载JAVA_HOME\jre\lib\ext目录下的所有类库,主要是载入扩展包。
再者是系统类加载器 Application ClassLoader, 也称之为应用程序类加载器,负责加载用户类路径(也就是我们配置的CLASSPATH)上所指定的类库,是应用程序中默认的类加载。
看完以上三个类加载器的简单描述过程,是不是有种终于知道了我们配置的jdk环境的最终作用了吧,是的,就是让类加载器识别到后加载各种类库。
那么问题来了?是哪个类加载器加载了我们的Hello World程序呢?是的,就是应用程序中默认的类加载器 Application ClassLoader。
知道了类加载器后,那接下来总要了解下类加载器怎么加载的吧?
说说类加载的过程
网上找了张图片,简单明了。虽说是简单明了,不过确实异常重要的,因为是面试热点(✿◡‿◡)
加载
其实就是上文说到的系统类加载器 Application ClassLoader将编译后的Main.class文件加载到内存中。
【思考】抛出个问题,所谓的加载到内存中,我们都知道JVM把内存分成了几大模块,那么请问是加载到哪个模块中?热点面试题,答案见文末!
链接
链接中包含了三部曲,总的作用就是负责将Main.class的二进制数据合并到JRE中。
关于三部曲,其实很好理解;
首先是验证阶段,类加载器将二进制字节流加载到虚拟机中,肯定是需要进行验证的,避免危害虚拟机自身安全,而这也是验证阶段存在的价值;
接下来是准备阶段,准备阶段是正式为类变量分配内存并且设置类变量默认值的地方,比如上面HelloWorld程序中的
private static String word = “Hello World!”;
注意我描述的第一个是类变量,也就是static所描述的变量,其次是默认值,也就是上面的word的默认值null,如果是数字则为0。
最后是解析阶段,解析阶段的作用主要是将常量池内的符号引用替换为直接引用的过程,解析阶段其实有点难理解,至少是比上面的两个阶段要难理解的,我这里尽量直白点;
所谓的符号引用指的是包含了类的信息、方法名、方法参数等信息的字符串,而当第一次运行时,JVM会根据这行字符串去检索到对应的方法入口,而为了下次不用再做同样的检索,在第一次运行的时候就会将符号引用替换成直接引用,这样后面就可以省去一定的消耗了;这里的直接引用其实就是指偏移量,虚拟机可以通过偏移量直接找到方法入口,不再需要做检索了。
初始化
终于来到初始化阶段了,上面我们有说到word默认值是null,是系统赋的默认值,而在初始化阶段,则是根据我们人为的初始化类变量和其他资源,比如上面的word则被我初始化成了"Hello World!"。
类执行
上面说到Main.class被加载到了Java虚拟机内存中,那么接下来便是执行的过程了。那么由谁来执行这一过程呢?
如图
实际上,一个Java虚拟机在运行的时候可以划分为三个子系统:
- 类加载子系统
- 执行引擎子系统
- 垃圾收集子系统
很明显、很清晰,图中的类加载子系统在上面已经谈了,执行引擎子系统就是负责执行这一部分的,那么过程是怎么样的呢?
其实很简单,执行引擎会把字节码转换为机器码【what?竟然还要转换。拜托<(ˉ^ˉ)>,字节码是被JVM识别的语言,字节码才是最终被操作系统识别的语言】
然后操作系统才可以真正调用,很多学或者做Java的人都听过JIT,但是都不知道具体是干嘛的,没错说的就是你。
这里终于可以解释下了,字节码转换成机器码的翻译工作使用的就是JIT(Just In Time)即时编译器(对热代码整段编译)和Java字节码解释器(一行一行解释字节码)来完成的。
这里给下JIT编译的工作流程:
JVM字节码 -> 机器无关优化 -> 中间代码 -> 机器相关优化 -> 中间代码 -> 寄存器分配器 -> 中间代码 -> 目标机器码生成器 -> 目标机器码
最后执行引擎会找到main()这个入口方法,并且执行其中的字节码指令。
最后,关于HelloWorld执行过程,基本上阐述完毕了,关于执行程序期间,JVM内存分配问题,是一个比较大的模块,欲知详情,请关注公众号,我们下次再聊!!!
【思考解惑】加载阶段完成后,虚拟机会将Main.class的二进制字节流按照虚拟机所需的格式存储在方法区之中,然后在内存中实例化一个java.lang.Class类的对象,作为程序访问方法区中的这些类型数据的外部接口,实例化后的java.lang.Class类的对象也是存放在方法区中的。
公众号主营:服务端编程相关技术解说,具体可以看历史文章。
公众号副业:各种陪聊吹水,包括技术、就业、人生经历、大学生活、内推等等。
欢迎关注,一起侃大山