javac编译过程浅析

/ 默认分类 / 0 条评论 / 878浏览

javac编译器工作原理

一.写在前面

关于编译型和解释型计算机语言的解释:

众所周知,无论是编译型还是解释性,其本质都是将高级语言(Java,C, Python等)翻译成机器能理解和运行的语言(机器码,二进制文件) 。区别主要是翻译发生的时机不同。

Go语言可以直接编译成可执行文件 , 如Window系统下,可以直接编译成.exe文件。因此是编译型语言。

Java语言首先通过编译器,得到.class文件,然后其虚拟机对.class文件进行逐条解释执行。因此我我认为Java本质上一种解释型的语言, 但是需要先经过编译器生成.class,为什么要这样做,个人认为是为了Java的跨平台特性。与Java不同的是,有些语言是直接翻译成机器码的(比如C#),这种是典型的编译型。

Java是解释型语言,也就是会在程序运行期间,运行的时候才会编译为机器码,只不过,这里编译为机器码是编译class字节码,因为在java中,是先将java高级语言通过javac编译器编译为二进制字节码. 也就是说,大致会经过这样的过程:

二.javac都做了啥?

按照前面介绍的,可以看出,javac主要的作用就是将java代码转为class字节码文件。 详细的步骤和细节如下:

  1. 词法解析

词法分析是将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符都可以称为标记,如“int a = b + 2”这句代码包含了6个标记,不可拆分,分别为int、a、=、b、+、2,虽然关键字int由3个字符构成,但是它只是一个标记(Token),不可再拆分。 (词法分析也就是将java中的代码拆分解析为一个标记集合,集合中的每一个元素都是拆分过程中的最小单元,例如,关键词,变量名,字面量,运算符等)

  1. 语法分析

把词法分析中的标记集合,生成语法树,树上的每一个节点代表着程序的一个语法结构,如操作,运算或方法调用等.

  1. 填充符号表

解析后的语法树最顶级的节点将被用来填充在符号表中,符号表存储着各个语法树的最顶级节点,填充后的符号表最终形成 待处理表。

  1. 语法糖解析和代码优化
语法糖解析:  编程语言为了 增加代码的可读性,以及减少编程出错率,提供了一些并不影响程序运行期仅在编译期有效的编程机制。

java语言中语法糖 有 泛型,拆箱与装箱,foreach循环,可变参数,switch,枚举等,在编译期将转换为字节码遵守的规范形式。

泛型使用类型擦出,拆装箱调用了valueOf与xxValue方法,foreach是迭代器 可变参数是数组,switch本质是 if else 的嵌套。

字节码替换:  在生成类的字节码之时,编译器后做一些默认性质的操作,当没有显示声明的构造器,则会创建默认的无参构造器,构造器分为 实例构造器与类构造器

在字节码层面 类构造器 是指多个static代码块中的语句 收敛生成的<cinit>指令。而构造代码块与显示的构造器将收敛生成实例构造器。

同时还会将 String类型的 +与+= 操作,默认替换为 对 StringBuffer或 StrignBudiuer的操作。

最后生成字节码。

三.java中的编译解释机制

按照前面介绍的我们可以看出,java是解释型语言,只不过是先将高级语言转为了字节码文件,然后在运行的时候是一行一行解释字节码文件为机器码然后运行.这本质上也就是解释性语言,只不过之所以增加了一步转为class字节码,是因为java的那句经典的口号,Write once, run anywhere,

编译型语言之所跨平台性差,是因为不同的操作系统所对应的可以识别的机器码(也就是二进制文件)是不同的,所以如果需要在其他平台运行,那么需要重新编译, 但是从上面的图片我们可以看出,javac将java高级代码编译为了字节码文件,而字节码文件可以在任何平台的jvm上运行,jvm层面已经做了夸系统平台的兼容,所以只需要在不同的平台提前安装好了对应的jvm即可一次编写,到处运行了.
但事实上,编译型语言也可以有很强的跨平台功能,比如Go语言,支持交叉编译,并且不需要安装Go环境即可运行编译后的Go程序。

ps:这里再来回忆一下解释器和编译器两者的概念:
编译器: 输入端是高级语言代码文件 -> 输出端是编译后的机器码文件
解释器: 输入端是高级语言代码(按行) -> 输出端是程序的执行结果

编译器:把源程序的每一条语句都编译成机器语言,并保存成二进制文件,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;
解释器:只在执行程序时,才一条一条的解释成机器语言给计算机来执行,所以运行速度是不如编译后的程序运行的快的; 并且这两者,输出的产物是不同的,一个是直接运行的代码的执行结果,另一个则是编译后的文件,只有在cpu中运行后才会得到结果.

所以,字节码并不是机器语言,要想让机器能够执行,还需要把字节码翻译成机器指令。这个过程是Java虚拟机做的,这个过程也叫编译。是更深层次的编译。至于为何要先转为class字节码前面已经解释了.现在要说的是,因为我们已经看出了java作为解释型语言,在执行效率上可能会相对较低,所以jvm对此做了下面的功能支持: JIT 即时编译技术。

从上面的图可以看出,jvm将class字节码解释执行,但是在解释到的过程中如果遇到热点代码,那么就会直接执行本地机器码而不需要解释执行了,也就是不需要先编译为机器码再执行,而是本地已经存在这段热点代码的机器码,直接运行即可,提高执行速度;

java代码最终在cpu运行的肯定都是机器码,现在java代码被编译为了class字节码文件,在jvm中解释执行的过程中(一行行),如果该段代码不是热点代码,那么解释器将其编译为机器码然后再执行这段机器码,如果发现这段代码是热点代码则会进入jit即时编译器,编译后的字节码会在一次性编译热区,下次再执行这段代码则会直接执行机器码.

那么怎样找出热点代码呢?

1. JVM进行一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测,其实进行热点探测并不一定要知道方法被调用了多少次。主要的探测方法:
基于采样的热点探测:主要是虚拟机会周期性的检查各个线程的栈顶,若某个或某些方法经常出现在栈顶,那这个方法就是“热点方法”。优点是实现简单;缺点是很难精确一个方法的热度,容易受到线程阻塞或外界因素的影响。
基于计数器的热点探测:主要就是虚拟机给每一个方法甚至代码块建立了一个计数器,统计方法的执行次数,超过一定的阀值则标记为此方法为热点方法。
2. Hotspot使用的基于计数器的热点探测方法。然后使用了两类计数器:方法调用计数器和回边计数器。当到达一定的阀值是就会触发JIT编译。
方法调用计数器 在JVM client模式下的阀值是1500次,Server是10 000次。可以通过虚拟机参数: -XX:CompileThreshold设置。但是JVM还存在热度衰减,时间段内调用方法的次数较少,计数器就减小。
回边计数器:主要统计的是方法中循环体代码执行的次数。

四.jvm中的编译模式

为何 HotSpot 虚拟机要使用解释器与编译器并存的架构? 解释器与编译器两者各有优势

解释器:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。

编译器:在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。

两者的协作:在程序运行环境中内存资源限制较大时,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。当通过编译器优化时,发现并没有起到优化作用,,可以通过逆优化退回到解释状态继续执行。

即时编译器与 Java 虚拟机的关系 即时编译器并不是虚拟机必需的部分,Java 虚拟机规范并没有规定 Java 虚拟机内必须要有即时编译器的存在,更没有限定或指导即时编译器应该如何去实现。

但是,即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一。它也是虚拟机中最核心且最能体现虚拟机技术水平的部分。

即时编译器的分类 Client Compiler - C1编译器 Server Compiler - C2编译器

HotSpot中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler。
-client:指定Java虚拟机运行在Client模式下。(对字节码进行简单优化,获得更快的编译速度)
-server:指定Java虚拟机运行在Server模式下。对于64位虚拟机而言,只存在Server模式,不存在Client模式。(对字节码的优化更深入,耗时长,但优化的代码执行效率高)

目前主流的 HotSpot 虚拟机(JDK1.7 及之前版本的虚拟机)默认采用一个解释器和其中一个编译器直接配合的方式工作,程序使用哪个编译器,取决于虚拟机运行的模式,就是文章开头提到的两种模式。

在 HotSpot 中,解释器和 JIT 即时编译器是同时存在的,他们是 JVM 的两个组件。对于不同类型的应用程序,用户可以根据自身的特点和需求,灵活选择是基于解释器运行还是基于 JIT 编译器运行。HotSpot 为用户提供了几种运行模式供选择,可通过参数设定,分别为:解释模式、编译模式、混合模式,HotSpot 默认是混合模式,需要注意的是编译模式并不是完全通过 JIT 进行编译,只是优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。
这三种模式可以通过jvm参数来设置:

-Xint: 完全采用解释器模式执行程序。(启动速度快,执行相对较慢)
-Xcomp: 完全采用即时编译器模式执行程序(编译器模式,先编译再执行)。如果即时编译出现问题,解释器会介入执行。(启动速度慢,执行快)
-Xmixed: 采用解释器和JIT编译器并存的方式共同执行程序。默认模式。(jit热点代码的概念引入)

下面我们来对这三种模式进行简单的测试:

在三种模式下执行下面的代码:

    @Test
    public void test8989(){
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        for (short i = 0; i > -1; i++) {
            System.out.println(i);
        }
        System.out.println("over");
        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
    }

默认模式(不指定jvm参数):

解释模式:

编译模式:

混合模式(也就是默认模式,只不过这里我显示指定了jvm参数):

感谢以下参考文章:
https://blog.csdn.net/qq_36627886/article/details/80402959
https://www.zhihu.com/question/398786680/answer/1258518094
https://www.cnblogs.com/runnerjack/p/10548199.html
https://blog.csdn.net/zl10086111/article/details/80907428
https://www.jianshu.com/p/732d9d960411
https://blog.csdn.net/ecidevilin/article/details/78630176
https://blog.csdn.net/Hathwayoung/article/details/109881552
https://blog.csdn.net/u011069294/article/details/107614443
https://blog.csdn.net/a219219219219/article/details/109760396
https://blog.csdn.net/qq_36627886/article/details/80402959
https://www.jianshu.com/p/bbd41e8ebd86
https://www.h5w3.com/68582.html
https://blog.csdn.net/xybelieve1990/article/details/100140883
https://blog.csdn.net/weixin_38608626/article/details/88350526
http://www.bubuko.com/infodetail-3636337.html
https://www.cnblogs.com/jueyoq/p/7861560.html
https://blog.csdn.net/qq_29693653/article/details/90947651
https://www.cnblogs.com/blogtech/p/10000162.html
https://www.cnblogs.com/coprince/p/8603492.html