一文读懂Java编译全过程

java代码首先要通过前端编译器编译成.class字节码文件,然后再按一定的规则加载到JVM(java 虚拟机)内运行,有三种运行方式,解释模式(javac)、编译模式(C1 JIT、C2 JIT)、混合模式(javac+(C1 OR C2))。解释模式下,一边执行字节码一边解释执行;编译模式下,字节码编译为机器码后执行;混合模式下,正常情况下使用解释执行,但是针对经常执行的代码,会采用JIT技术进行编译执行。无论是server运行模式下还是client运行模式下,都有可能采用解释+(C1 OR C2 )执行。但本文的重点不在执行,而是编译,包括前端编译器、C1 JIT、C2 JIT。

如:一个是client 虚拟机模式,一个是server虚拟机模式,都是混合模式执行。

语言处理器种类

  1. 编译器,如gcc、javac。
  2. 解释器,如Ruby、Python、JAVA.EXE等一些一些语言使用解析器来实现的。
  3. IDE,如Eclipse、NetBeans等。
  4. 代码分析器,如FindBugs等。
  5. 反编译器,如JD、Jad、Reflector.NET等。

Java编译过程

Java文件编译过程包括两个阶段,第一阶段是在编译阶段编译成Java字节码的过程,有些书籍中叫前端编译器,如Oracle的javac编译器;第二阶段是在运行时,通过JVM的编译优化组件,对代码中的部分代码编译成本地代码,即JIT编译,如HotSpot中的C1、C2编译器( Thus the threads used by client JIT compiler are called c1 compiler threads. Threads used by the server JIT compiler are called c2 compiler threads.)。JVM整个编译过如下图所示。

其中,编译状态有如下9种。

//编译状态
public enum CompileState {INIT(0),//初始化PARSE(1),//解析ENTER(2),//处理符号表PROCESS(3),//核心处理ATTR(4),//符号解析FLOW(5),//流分析TRANSTYPES(6),//解泛型为非泛型等类型转换UNLAMBDA(7),//解LAMBDA表达式LOWER(8),//解语法糖GENERATE(9);//生成字节码}

下面是JIT编译器和C1(C2)编译器编译流程。

Javac前端编译器

当我们在控制台执行javac命令时,找到javac对应的环境变量的可执行文件,通过JNI方式调用com.sun.tools.javac.Main.java中的main方法进入。也就是说Javac编译工作是由Java代码完成的。像javap,javah等命令也都是通过Java代码完成的。

   /*** launcher的入口.* Note: 该方法调用了System.exit.* @param args 命令行参数*/public static void main(String[] args) throws Exception {System.exit(compile(args));}//此代码段在Main#compile方法中,用于读取Java文件对象用于编译。if (!files.isEmpty()) {// add filenames to fileObjectscomp = JavaCompiler.instance(context);List<JavaFileObject> otherFiles = List.nil();JavacFileManager dfm = (JavacFileManager)fileManager;for (JavaFileObject fo : dfm.getJavaFileObjectsFromFiles(files))otherFiles = otherFiles.prepend(fo);for (JavaFileObject fo : otherFiles)fileObjects = fileObjects.prepend(fo);}//调用JavaCompiler#compile方法comp.compile(fileObjects,//要编译的文件对象classnames.toList(),//注解处理的类名processors);//用户提供的注解处理器

最终调用JavaCompiler.compile()方法进行编译处理。如果自行编译,可以调用java中提供的工具类ToolProvider.getSystemJavaCompiler() 自行进行编译。如下是JavaCompiler.compiler()方法。

   /*** 主方法:要编译的文件列表,返回所有编译的类* @param sourceFileObjects 要编译的文件对象* @param classnames 为类中注解处理的类名* @param processors 用户提供的注解处理器,null意味着没有处理器提供。*/public void compile(List<JavaFileObject> sourceFileObjects,List<String> classnames,Iterable<? extends Processor> processors){if (processors != null && processors.iterator().hasNext())explicitAnnotationProcessingRequested = true;// 由于JavaCompiler只能使用一次,如果以前使用过,则抛出异常if (hasBeenUsed)throw new AssertionError("attempt to reuse JavaCompiler");hasBeenUsed = true;// forcibly set the equivalent of -Xlint:-options, so that no further// warnings about command line options are generated from this point onoptions.put(XLINT_CUSTOM.text + "-" + LintCategory.OPTIONS.option, "true");options.remove(XLINT_CUSTOM.text + LintCategory.OPTIONS.option);start_msec = now();try {//检查是否要处理注解initProcessAnnotations(processors);// (1)这些方法必须是链式调用以避免内存泄漏delegateCompiler =processAnnotations(enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),classnames);// (2)分析和生成字节码delegateCompiler.compile2();delegateCompiler.close();elapsed_msec = delegateCompiler.elapsed_msec;} catch (Abort ex) {if (devVerbose)ex.printStackTrace(System.err);} finally {if (procEnvImpl != null)procEnvImpl.close();}}

从上面的代码可知,编译真正处理的代码在(1)和(2)处。对代码分析,编译处理包括以下三个部分。分别为解析与填充符号表、注解处理、分析和生成字节码三个大阶段。

解析与填充符号表

解析与填充符号表,对应图一的词法分析、语法分析、抽象语法树、填充符合表几个细节处理。在解释语法树之前,我们首先要说下什么是语法树,语法树在很多语言中都有采用,如java、sql源码阅读中都用到了语法树的概念。如下的英语句子的语法树。

根据上面源码中的(1)注解中的代码,解析与填充符号表包括以下几个步骤。

delegateCompiler =processAnnotations(enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),classnames);
  1. parseFiles方法会执行parserFactory#newParser方法,在该方法内部封装了Scanner类并借助于JavaTokenizer类实现词法分析(手写的ad-hoc方式构造的词法分析器),词法分析可以简单理解为java文件中的每个空格之间的字符当作一个标记。源文件经过Unicode转义处理,通过Scanner类转化为令牌流。Parser类读取令牌流,使用TreeMaker创建语法树。而语法树通过com.sun.source.Tree及其子类的JCTree类或其子类实现的。语法树可理解为JCTree中每个节点表示一个包、类型等语法结构。每个树最后传递给Enter类,为遇到的所有定义的符号传入符号字面量。这必须在解析树之前做好,因为可能引用这些符号。该阶段输出的是“待办”列表,其中包含需要分析并生成文件的树。而Parser#parseCompilationUnit方法用于语法分析。
  2. enterTrees方法主要用于填充符号表。主要由Enter类实现。Enter包含很多阶段,要编译的类通过队列从一个阶段传到下一个阶段。

  • 在第一个阶段,所有的类符号都进入到Enter的范围之内,树中其他类的成员变量都严格降序排列。类符号被赋予一个MemberEnter对象作为"完成者"。除此之外,如果任何package-info.java文件被找到,并且包含包注解。树节点的顶层将会为该文件添加到“代办”列表。

  • 将符号输入到符号表。com.sun.tools.javac.comp.Enter,每个编译单元的抽象语法树的顶局节点都先被放到待处理列表中,逐个处理列表中的节点,所有类符号被输入到外围作用域的符号表中,若找到package-info.java,将其顶局树节点加入到待处理列表中,确定类的参数(对泛型类型而言)、超类型和接口,根据需要添加默认构造器,将类中出现的符号输入到类自身的符号表中,分析和校验代码中的注解(annotation)。
    添加的默认构造器如下。

  • 在第二阶段,类使用MemberEnter.complete()来完成。类是按需完成的,但是未按照此方式完成的类最终都会通过处理未完成的队列来完成。完成需要:(1)决定类的变量、超类和接口。(2)将类中定义的所有符号输入,但是在第一阶段已经完成的符号变量除外。(2)依赖于(1)中的类及其所有超类和封闭类已经完成。这就是为什么在(1)之后,我们将类放入到一个半完成的队列中。只有当我们对一个类及其所有超类和内部类执行了(1)之后,我们才继续执行(2)。

  • 输入所有的符号后,在这些符号上遇到的所有注解将会分析和验证。

    第一阶段是组织被遍历所有编译的语法树,而第二阶段是按需的,类的成员在第一次访问类的内容时输入,这是通过在编译类的类符号中使用completer对象来实现的,编译类调用对应类的树的MemberEnter阶段。

注解处理

注解是JDK1.5中引入的,对于注解的处理可以理解为编译器的一组插件,根据注解解析结果对抽象语法树进行修改,如lombok。方法processAnnotations是注解处理的入口,当由注解需要处理时,则由JavacProcessingEnvironment#doProcessing方法创建一个JavaCompiler对象来完成。从概念上来讲,注解处理是编译之前的一个初步步骤。这个初步动作由一系列的循环组成(如图2)。每个循环用于解析和输入源文件,然后确定和调用适当的注解处理器。在首次循环之后,如果被调用的任何注解处理器生成任何需要作为最后编译一部分的新原文件或类时,将需要执行后面的循环。最后,当所有必要的循环完成,执行实际编译。

在实际中,调用任何注解处理器的需要可能要等到要编译的文件被解析并且包含的声明被确定之后才能知道。因此,为了避免在不执行注解处理的情况下不必要地解析和输入源文件,JavacProcessingEnvironment对概念模型的执行有点不同,但是仍满足注解处理器作为一个整体在实际编译前执行。

当class文件被编译,并且已经解析和填充符号后。JavacProcessingEnvironment将会被调用。该类决定被编译的文件哪些注解需要被加载或被调用。通常,如果在整个编译过程中出现任何错误,该过程则在下一个合适的点停止编译。但是,如果在符号解析阶段出现丢失符号,则会抛出异常,因为定义这些符号可能作为注解处理器的结果。

如果要运行注释处理器,将在单独的类加载器中加载并运行它们。

当注解处理器运行时,JavacProcessingEnvironment决定是否需要另外一轮注解处理。如果需要,将会创建一个新的对象JavaCompiler。读取上步骤新生成的源文件进行解析。并且重新使用之前的语法树进行解析。所有的这些树都被输入到这个新编译器实例的符号表中,并且根据需要调用注解处理器。然后重复直到所有的注解编译完成。

最后,JavacProcessingEnvironment返回JavaCompiler对象用于编译剩下的部分。这个对象是用于解析和输入初始文件集的原始实例,或者是JavacProcessingEnvironment创建的用于开始最后一轮编译的最新实例。

下面以lombok为例说明

  1. 注解处理之前。


2. 注解处理后

分析和生成字节码

当命令行中指定的所有文件被解析并输入到编译器的符号表中,并且注解也已经处理,JavaCompiler能处理分析的语法树,以生成相应的class文件。由delegateCompiler.compile2()方法进入。

   /*** 注释处理之后的阶段:属性、解语法糖,最后是代码生成。*/private void compile2() {try {switch (compilePolicy) {case ATTR_ONLY://只需解析数据的属性attribute(todo);break;case CHECK_ONLY://用于属性和解析树的流分析检查flow(attribute(todo));break;case SIMPLE://流分析、语法糖处理、生成字节码generate(desugar(flow(attribute(todo))));break;case BY_FILE: {Queue<Queue<Env<AttrContext>>> q = todo.groupByFile();while (!q.isEmpty() && !shouldStop(CompileState.ATTR)) {generate(desugar(flow(attribute(q.remove()))));}}break;case BY_TODO:while (!todo.isEmpty())generate(desugar(flow(attribute(todo.remove()))));break;default:Assert.error("unknown compile policy");}} catch (Abort ex) {if (devVerbose)ex.printStackTrace(System.err);}if (verbose) {elapsed_msec = elapsed(start_msec);log.printVerbose("total", Long.toString(elapsed_msec));}reportDeferredDiagnostics();if (!log.hasDiagnosticListener()) {printCount("error", errorCount());printCount("warn", warningCount());}}

当分析树时,可以找到对成功编译所需的类的引用,但是这些类没有显示指定用于编译。根据编译选项,将在源路径和类路径中搜索此类的类定义。如果能在类文件中找到定义,将自动分析、输入源文件并将其放到待办事项列表中。这些在Attr.SourceCompleter类中实现。

分析树和生成类文件的工作由一系列的观察者来处理进入了编译器代办事项列表。这些观察者没有必要分步对所有的源文件处理。事实上,内存问题会使这极不可取。唯一的要求是,“代办”列表最终会被每一个观察者处理,除非编译因为错误而提前终止。

  1. Attr和Check

    顶层类是“Attribute",使用Attr,这意味着语法树中的名称、表达式和其他元素将被解析并与相对应的类型和符号相关联。这可以通过Attr类或Check类检查到许多语义错误。

语法分析的一个步骤,将语法树中名字、表达式等元素不变量、方法、类型等联系到一起,检查变量使用前是否已声明,推导泛型方法的类型参数,检查类型匹配性,迕行常量折叠。

下面举例说明。
(1)标注前。

(2)标注后。

  1. Flow

    如果到目前没有错误,将会使用Flow进行类的流分析。流分析用于检查变量的明确分配和不可到达语句。检查所有checked exception都被捕获或抛出;检查变量的确定性赋值(1)所有局部变量在使用前必项确定性赋值;(2)有返回值的方法必须确定性返回值;检查变量的确定性不重复赋值(1)为保证final的语义。

  2. TransTypes

    将泛型类型的类转变为TransTypes类(裸类型,普通的java类型),同时插入必要的类型转换代码。

    下面给个示例。
    (1)类型转换前。

    (2)类型转化后。

  3. Lower

    语法糖使用Lower类来处理,它重写语法树,通过替换等价、简单子树来消除特定类型的子树。这将会处理内部类和嵌套类,类字面量,断言,foreach循环等。对于每个被处理的类,Lower类返回已转变类及所有转变的嵌套类和内部类的树的列表。尽管Lower通常处理顶层类,但也处理package-info.java的顶层树。对于这种树,Lower类将创建合成类来包含包的任何注解。

    削除if (false) { … }形式癿无用代码。满足下述所有条件的代码被认为是条件编译的无用代码◦if语句的条件表达式是Java语言规范定义的常量表达式◦并且常量表达式值为false则then块为无用代码;反之则else块为无用代码。

示例

(1)Lower前

(2)Lower后

  1. Gen

    Gen类用于方法代码的编译,它创建包含字节码的Code属性,通过JVM实例来执行方法。如果该步骤成功,则编译后的类由ClassWriter类写出。

一旦一个类作为类文件被写出来,它的许多语法树和生成的字节码就不再需要了。为了节省内存,对树的这些部分和符号的引用将为空,以允许垃圾收集器恢复内存。

  1. 将实例成员初始化器收集到构造器中成为();将静态成员初始化器收集为();
  2. 从抽象语法树生成字节码。(1)后序遍历语法树(如下);(2)进行最后的少量代码转换,如String的+被生成为StringBuilder操作;x++/x–在条件允许时被优化为++x/–x
  3. 从符号表生成Class文件◦生成Class文件的结构信息。生成元数据(包括常量池)

整个前端编译过程如下图所示。

以上步骤已经生成了.class文件。在运行期间,编译器将会进一步优化,即JIT优化。

JIT编译

JIT是即时编译器(Just In Time Compiler)的缩写,Hotspot中有两个即时编译器,分别为Client Compiler(C1 JIT)和Server Compiler(C2 JIT),C1和C2都是将字节码编译成本地代码,区别可以理解为C1是局部优化,而C2可以理解为专门面向服务端的。JVM有三种运行模式,分别是解释(interpreted mode)、编译模式(compiled mode)和混合模式(mixed mode)三种模式。**Java1.8中默认的解释器与其中一个JIT编译器直接配合的方式执行,即采用混合模式。**用户可以通过参数"-Xint"强制虚拟机运行在解释模式,此时编译器不工作。当然也可以使用参数"-Xcomp"强制虚拟机运行于“编译模式”。这时优先采用编译方式执行,但在某些情况下,解释器不得不介入才能执行。

编译条件

编译优化的条件主要针对热点代码,而热点代码主要有两种情况:

  1. 多次被调用的方法
  2. 多次执行的循环体

无论第一种情况还是第二种情况,都是以整个方法作为编译对象。第二种情况而不是以循环体作为编译对象。只是处理方式不同,因为第二种编译方式发生在方法执行体中,而在运行时表现为方法栈,通过替换方法栈中的部分代码为编译后的本地代码,即通过栈上替换(On Stack Replacement,OSR)的方式进行JIT编译。

很显然,无论采用哪种方法,编译器都需要识别哪些代码为热点代码。目前热点代码探测的方式有两种。

  1. 基于采样的热点探测。虚拟机启动一个检测线程周期性检查各个线程的栈顶,如果发现某个方法经常在栈顶,则认为是"热点代码"。这种方式简单但是不能精确统计某个方法的热点,且容易受线程阻塞等外界因素影响,当线程阻塞时,某个方法就会一直处于栈顶,从而不能精确统计方法的执行次数。
  2. 基于计数器的热点探测。虚拟机会为每个方法建立计数器,如果该方法超过规定的次数则认为是热点代码。

在HotSpot中采用的是第二种方式,且对同一个方法采用了两个计数器。一个是记录在某段时间内方法调用次数的计数器,当某段时间内不满足编译时,则次数会衰减一半,所以是某段时间内的相对次数。另一个是记录方法中的循环体的计数器(称为回边计数器),而这个计数器会一直往上增长,是绝对计数,当溢出时,则调整计数器的值为溢出状态。当该两个计数器超过默认的阈值,则发生JIT编译。下面表格是不同编译模式下的默认值。两个计数器都可以通过虚拟机参数进行设定。

方法调用计数器 回边计数器
1500次 13995次
C2 10000次 10700次

编译过程

默认情况下,当虚拟机中的编译线程编译完成后,才能替换到JIT编译请求。用户可以通过参数-XX:-BackgroundCompilation来禁止后台编译。

C1编译优化主要在AdvancedThresholdPolicy.cpp文件中。

编译优化技术

公共子表达式消除、方法内联、逃逸分析

参考

1、http://openjdk.java.net/groups/compiler/doc/compilation-overview/index.html

2、(重要文章)JVM c1, c2 compiler thread — high CPU consumption? https://medium.com/@RamLakshmanan/jvm-c1-c2-compiler-thread-high-cpu-consumption-b99acc604f1d

3、(javac编译操作)Compile All Java Classes in Directory Structure with javac,https://www.baeldung.com/javac-compile-classes-directory

4、(内容同3)Compile Java Files. https://www.baeldung.com/javac

5、https://www.oracle.com/java/technologies/whitepaper.html

JVM(一)一文读懂Java编译全过程相关推荐

  1. 一文读懂Java中File类、字节流、字符流、转换流

    一文读懂Java中File类.字节流.字符流.转换流 第一章 递归:File类: 1.1:概述 java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建.查找和删除等操作. ...

  2. java中date类型如何赋值_一文读懂java中的Reference和引用类型

    简介 java中有值类型也有引用类型,引用类型一般是针对于java中对象来说的,今天介绍一下java中的引用类型.java为引用类型专门定义了一个类叫做Reference.Reference是跟jav ...

  3. 一文读懂 Java 工程师学习路线!

    作者 | 三太子敖丙 来源 | 三太子敖丙(ID:NiceOffer) 在写这个文章之前,我花了点时间,自己臆想了一个电商系统,基本上算是麻雀虽小五脏俱全,我今天就用它开刀,一步步剖析,我会讲一下我们 ...

  4. 「Java基本功」一文读懂Java内部类的用法和原理

    内部类初探 一.什么是内部类? 内部类是指在一个外部类的内部再定义一个类.内部类作为外部类的一个成员,并且依附于外部类而存在的.内部类可为静态,可用protected和private修饰(而外部类只能 ...

  5. java中this_夯实Java基础系列7:一文读懂Java 代码块和执行顺序

    目录 #java中的构造方法 #构造方法简介 #构造方法实例 #例-1 #例-2 #java中的几种构造方法详解 #普通构造方法 #默认构造方法 #重载构造方法 #java子类构造方法调用父类构造方法 ...

  6. 一文读懂Java内存模型(JMM)及volatile关键字

    点赞再看,养成习惯,公众号搜一搜[一角钱技术]关注更多原创技术文章. 本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章. 前言 并发编程从操作系统底层工作的整 ...

  7. 一文读懂 Java 字符串相关知识点和常见面试题

    点击上方"黄小斜",选择"置顶或者星标" 你关注的就是我关心的! 作者:黄小斜 来源:微信公众号[程序员黄小斜] 目录 string基础 Java String ...

  8. 一文读懂 JAVA 异常处理

    JAVA 异常类型结构 Error 和 Exeption 受查异常和非受查异常 异常的抛出与捕获 直接抛出异常 封装异常并抛出 捕获异常 自定义异常 try-catch-finally try-wit ...

  9. 一文读懂Java 11的ZGC为何如此高效

    导读:GC是大部分现代语言内置的特性,Java 11 新加入的ZGC号称可以达到10ms 以下的 GC 停顿,本文作者对这一新功能进行了深入解析.同时还对还对这一新功能带来的其他可能性做了展望.ZGC ...

最新文章

  1. 浅谈无缓存I/O操作和标准I/O文件操作区别 (转载)
  2. Python入门100题 | 第007题
  3. 【Python】编程笔记3
  4. 全开源深度学习平台PaddlePaddle入手之路(二)----利用Docker在Windows10专业版环境下配置PaddlePaddle...
  5. python turtle setheading_一文掌握Python绘图库Turtle的使用
  6. System.Diagnostics.Process 执行.EXE
  7. 操作系统学习笔记-2.1.1.进程的定义、组成、组织方式、特征
  8. dj电商-需求分析-购物车模块与订单模块
  9. (35)FPGA面试技能提升篇(AD、DA、时钟芯片)
  10. 卷积神经网络训练准确率突然下降_详解卷积神经网络:手把手教你训练一个新项目...
  11. 走火入魔.NET从C/S单点登录到B/S系统的例子,SUID(System Unique Identification)
  12. 浪曦_Struts2应用开发系列_第2讲.Struts2的类型转换--出现的问题笔记
  13. 概率图模型(快速入门必备)
  14. python word转excel题库_【Python应用软件】Word表格怎么转换Excel#Word表格汇总Excel
  15. java dojo,针对 Java 开发人员的 Dojo 概念
  16. 王道程序员求职宝典 pdf
  17. 中央财经大学创新创业中心主任尚超:大数据技术在防范虚假发票中的应用
  18. 数据结构形象动态演示的网站
  19. java的像素与dpi_对屏幕的理解---分辨率,dpi,ppi,屏幕尺寸,像素 等
  20. Linux中几个你不常用,但却很有用的命令

热门文章

  1. Deepson在Jetson Nano上进行视频分析的入门
  2. 30天web实践2-timelinejs
  3. Windows11更新最新系统版本后无法播放媒体声音
  4. python自动化键盘_使用Python进行鼠标和键盘自动化?
  5. C#||坐标距离和方位角计算
  6. 全网最全的文本关键词抽取包括有监督和无监督方法
  7. 3款热门报表软件优劣势对比
  8. 对一个整数进行因式分解
  9. 10个树莓派DIY套件,总有一款是你程序员的菜!
  10. 三生制药明晟ESG评级升至BBB级,居全球生物科技行业前列