前言
相信任何一个JAVA爱好者或者是程序员都知道大致的JAVA程序加载流程:
编辑源程序 --> 编译生成字节码文件 --> 解释运行字节码文件
但是这样解释经不起细问,编译是怎么编译的?字节码文件怎样生成?解释运行字节码文件你怎么解释,又怎样运行?其中有哪些流程?又有哪些加载器起到了作用?作用是什么?… 这个,就需要我们进行细纠,而本篇博客,笔者也会尽力从目前所能了解到的知识的角度对这些问题进行一一解答。 在进行具体的解答之前,先给出图示(我知道我画的丑),各位读者大大可以对照着图阅读。
一丶编译阶段
在编译阶段的主要任务就是将 .Java文件编译生成.class文件,而这一过程由JAVAC编译器来完成, JAVAC编译器是个啥?
这是一个由纯JAVA语法编写的一个编辑器。作用大概如下: 1.按照JAVA语言语法对源文件检测,看看是否违规 2.如果不符合规范,那么报错 3.如果符合规范,那么按照字节码的文件规范生成对应的字节码文件。
其中具体细分为以下四个阶段。
1>语法分析
词法解析是编译器执行的字节码编译的第一步。主要就是将代码中的语句分割成一个一个的符号,再仔细一点,就是将JAVA源程序里面的标识符和关键字等转化为一个一个的Token序列 词法解析器的接口是javac.parser.Lexer ,它属于同包下Scanner类的子类,它的主要任务是按照单个字符的方式读取Java源文件中的关键字和标识符等,然后将其转换为符合Java规范的Token序列。 而Token其实就是一个枚举类型,其内部定了许多符合Java语法规范并与源码字符集相对应的枚举常量。
2>词法分析
词法分析的目的就是将经过词法分析得到的Token整合为一棵结构化的抽象语法树。 负责词法解析工作的是javac.parser.JavacParser类。 而检测工作则有以下方法来完成:
(1) qualident() 方法解析 package 语法节点
(2)importDeclaration() 方法解析 import 语法树
(3)调用 classDeclaration() 方法解析 class 语法树
3>语义解析
语法树生成了,但是百炼成钢,接下里这颗树还要被不断修整。语义解析会将这颗不够完善的语法树扩充地更加完善。 并且对这课语法树进行检测,看它符不符合JAVA语义要求 具体步骤:
1.为没有构造方法的类型添加默认的无参构造函数 2.检查任何变量是否在使用之前都被初始化过 3.检查变量类型和值是不是匹配 4.将String类型和常量进行合并处理 5.检查代码中的操作是不是都可达 6.异常检查 7.将语法糖的内容正常化
4>字节码生成器
到了这一步,我们的字节码文件才可以差不多算是一个完整的宝宝,那接下来,就应该调用javac.jvm.Gen类,将这棵语法树编译为Java字节码文件。 到了这一步,字节码文件才算真正的生成,可编译是编译完了,问题是现在还没有解决地址问题,那么怎么办?那接下来它就要被类加载系统抱走准备继续深造了。
======================================================= 这里咋们可以大致来查看一下字节码文件的规范。
(该规范来源于JAVA官方对于字节码文件规范的叙述,出处:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html)
二丶加载阶段
加载阶段的主要是任务是:通过一个类的全限定名,然后用类加载器加载成生成对应的二进制字节流,并且在内存中生成该类的java.lang.class对象,用于类的各种数据的访问入口 这里给一下几种类加载器的工作原理
(此处对于类加载器的总结来源于囧辉大佬,@程序员囧辉)
1>启动类加载器(Bootstrap ClassLoader)
这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
2> 扩展类加载器(Extension ClassLoader)
这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
3> 应用程序类加载器(Application ClassLoader)
这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
4> 自定义类加载器:
用户自定义的类加载器
三丶链接阶段
1> 验证
这里的验证主要是就是验证对应的二进制字节流是否符合JAVA虚拟机的语法规范以及安全性能的检查。 包括对于文件格式的验证,对于元数据的验证,对于字节码,符号引用等等的验证。
2> 准备
准备就是在内存上给这个类当中的静态变量开辟空间并且初始化,这里的初始化指的就是通常情况下,各个数据类型的对应的默认值。
PS:这里的初始化可以看之前博客----数组,里面对应的初始化和此处相同。
3>解析(重要步骤,这里就是给地址了)
解析指的是把常量池当中的符号引用替换为直接引用。 什么意思?
比如说,我现在有一个方法 myWorld () ,它的内存地址是 0x10086,符号引用就是 myWorld(),但是直接引用就是 0x10086(话费查询直通车…)
四丶初始化
初始化阶段,会将类中的JAVA初始化代码开始执行。主要就是静态变量的赋值以及静态代码块(static { } )中的语句,换句话说这就是一个执行类构造器的过程,并且只对static修饰得到变量或者语句进行初始化。 这里要注意,如果包含多个静态代码块,则会按照从上而下的顺序将这些静态代码块合并成为 < clinit >。具体程序运行如下:
结果如下:
可以看到,静态代码块只会在类初始化阶段只执行一次,而实例代码块在每一个对象初始化的阶段都会执行并且只执行一次。接下来再具体:
实例代码块:
静态代码块:
可以看到,静态代码块被根据定义次序合并成了 < clinit >,而实例代码块,就和我上一篇博客讲的一样,被和构造代码块合并在了一起,并且在构造代码块之前。 而如果此类还有父类,那么具体执行流程为:
父类静态变量(代码块) --> 子类静态变量(代码块)–> 父类非静态变量(构造器)–> 父类构造器 --> 子类非静态变量(代码块) --> 子类构造器
五丶总结
这就是JAVA对程序猿的宠爱嘛~~
|