JVM:早期(编译期)优化的深入理解

早期(编译期)优化

JVM的编译器可以分为三个编译器:

  1. 前端编译器:把*.java转变为*.class的过程。如Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)
  2. JIT编译器:把字节码转变为机器码的过程,如HotSpot VM的C1、C2编译器
  3. AOT编译器:静态提前编译器,直接将*.java文件编译本地机器代码的过程

本章的后续文字里,“编译期”和“编译器”都仅限于第一类编译过程

1、Javac编译器

Javac编译器本身就是一个由Java语言编写的程序

1)、Javac的源码与调试

Javac的源码存放在JDK_SRC_HOME/langtools/src/share/slasses/com/sun/tools/javac中

编译过程大致可以分为3个过程:

  • 解析与填充符号表过程
  • 插入式注解处理器的注解处理过程
  • 分析与字节码生成过程

这3个步骤之间的关系与交互顺序如下:

2)、解析与符号填充表

解析步骤由parseFiles()方法完成,解析步骤包括了词法分析和语法分析两个过程

A.词法分析与语法分析

词法分析:将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符都可以成为标记,在Javac的源码中,词法分析过程由com.sun.tools.javac.parser.Scanner类来实现。

语法分析是根据Token序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形表述方式。语法树的每一个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、接口、返回值甚至代码注释都可以是一个语法结构。语法分析过程由com.sun.tools.javac.parser.Parser类实现,这个阶段产出的抽象语法树由com.sun.tools.javac.tree.JCTree类表示,经过这个步骤之后,编译器就基本不会再对源码文件进行操作了,后续的操作都是建立在抽象语法树之上的

B.填充符号表

完成抽象语法树之后,下一步就是填充符号表的过程,即enterTrees()方法。符号表是由一组符号地址和符号信息构成的表格,类似于哈希表中K-V值对的形式。符号表中所登记的信息在编译的不同阶段都要用到。当对符号名进行地址分配时,符号表是地址分配的依据。填充过程由com.sun.tools.javac.comp.Enter类实现

3)、注解处理器

JDK1.5之后,Java提供了对注解的支持,这些注解与普通的Java代码一样,在运行期间发挥作用。 有了编译器注解处理的标准API后,我们的代码才有可能干涉编译器的行为,由于语法树中的任意元素,甚至包括代码注释都可以在插件之中访问到,所以使用插入式注解处理器在功能上有很大的发挥空间

4)、语义分析与字节码生成

语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,如进行类型审查

A.标注检查

Javac的编译过程中,语义分析过程分为标注检查以及数据及控制流分析两个步骤,分别是attribute()flow()方法

标准检查步骤检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。在标准检查步骤中,还有一个重要的动作称为常量折叠

int a = 1 + 2;

语法树上仍然能看到字面量“1”、“2”以及操作符“+”,但是在经过常量折叠以后,它们将会被折叠为字面量“3”。由于编译期间进行了常量折叠,所以在代码里面定义“a=1+2”比起直接定义“a=3”,并不会增加程序运行期哪怕仅仅一个CPU指令的运算量

标注检查步骤在Javac源码中的实现类是com.xun.tools.javac.comp.Attr和com.sun.tools.javac.comp.Check类

B.数据及控制流分析

数据及控制流分析可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题

局部变量与字段(实例变量、类变量)是有区别的,它在常量池中没有CONSTANT_Fielddref_info的符号引用,自然就没有访问标志的信息,因此,将局部变量声明为final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障。在Javac的源码中,数据及控制流分析的入口是flow()方法,具体操作由com.sun.tools.javac.comp.Flow类来完成

C.解语法糖

语法糖是指在计算机语言中添加某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用
Java是一种“低糖语言”,常用的语法糖主要是之前提到的泛型、变长参数、自动装箱/拆箱等。虚拟机运行时不支持这些语法,它们在编译期还原回简单的基础语法结构,这个过程称为解语法糖。解语法糖的过程是由desuger()方法触发的

D.字节码生成

字节码生成是Javac编译过程的最后一个阶段,由com.sun.tools.javac.jvm,Gen类来完成,字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化为字节码写入磁盘中,编译器还进行了少量代码添加和转换工作

实例构造器<init>()方法和类构造器<client>()方法就是在这个阶段添加到语法树之中的,这两个构造器的产生过程实际上是一个代码收敛的过程,编译器会把语句块(对于实例构造器而言是{}块,对于类构造器而言是static{}块)、变量初始化(实例变量和类变量)、调用父类的实例构造器等操作收敛到<init>()<client>()方法之中,并且保证一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序进行,上面所述的动态由Gen.normalizeDefs()方法来实现

完成对语法树的遍历与调整之后,就会把填充了所有所需信息的符号表交给com.sun.tools.javac.jvm.ClassWriter类,由这个类的wrtieClass()方法输出字节码,生成最终的Class文件

2、Java语法糖的味道

1)、泛型与类型擦除

泛型是JDK1.5的一项新增特性,它的本质是参数化类型的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法

Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<int>ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型

 public static void main(String[] args) {
 Map<String, String> map = new HashMap<String, String>();
 map.put("hello", "你好");
 map.put("how are you?", "吃了没?");
 System.out.println(map.get("hello"));
 System.out.println(map.get("how are you?"));
 }

把这段Java代码编译成Class文件,然后再用字节码反编译工具进行反编译后,代码如下:

 public static void main(String[] paramArrayOfString)
 {
  HashMap localHashMap = new HashMap();
  localHashMap.put("hello", "你好");
  localHashMap.put("how are you?", "吃了没?");
  System.out.println((String)localHashMap.get("hello"));
  System.out.println((String)localHashMap.get("how are you?"));
 }

当泛型遇到重载:

 public static String method(List<String> list) {
 System.out.println("invoke method(List<String> list)");
 }
 public static int method(List<Integer> list) {
 System.out.println("invoke method(List<Integer> list)");
 }

这段代码是不能被编译的,因此参数List<String>List<Integer>编译之后都被擦除了,变成了一样的原生类型List<E>,擦除动作导致这两种方法的特征签名变得一模一样

2)、自动装箱、拆箱与遍历循环

自动装箱、拆箱在编译之后被转化成了对应的包装和还原方法,遍历循环则把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因,变长参数在调用的时候变成了一个数组类型的参数

 public static void main(String[] args) {
 Integer a = 1;
 Integer b = 2;
 Integer c = 3;
 Integer d = 3;
 Integer e = 321;
 Integer f = 321;
 Long g = 3L;
 System.out.println(c == d);// true
 System.out.println(e == f);// false
 System.out.println(c == (a + b));// true
 System.out.println(c.equals(a + b));// true
 System.out.println(g == (a + b));// true
 System.out.println(g.equals(a + b));// false
 }

包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,以及它们equals()方法不处理数据转型的关系

3)、条件编译

Java语言使用条件为常量的if语句,此代码中的if语句不同于其他Java代码,它在编译阶段就会被运行,生成的字节码之中只包含条件正确的部分

 public static void main(String[] args) {
 if (true) {
  System.out.println("block 1");
 } else {
  System.out.println("block 2");
 }
 }

Java语言中条件编译的实现,也是Java语言的一颗语法糖,根据布尔常量值的真假,编译器将会把分支中不成立的代码块消除掉,这是在解语法糖阶段实现的

Java语言中还有不少的其他语言糖,如内部类、枚举类、断言语句、对枚举和字符串的switch支持、try语句中定义和关闭资源等等

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接

(0)

相关推荐

  • 详解JVM 运行时内存使用情况监控

    java 语言, 开发者不能直接控制程序运行内存, 对象的创建都是由类加载器一步步解析, 执行与生成与内存区域中的; 并且jvm有自己的垃圾回收器对内存区域管理, 回收; 但是我们已经可以通过一些工具来在程序运行时查看对应的jvm内存使用情况, 帮助更好的分析与优化我们的代码; 注: 查看系统里java进程信息 // 查看当前机器上所有运行的java进程名称与pid(进程编号) jps -l // 显示指定的jvm进程所有的属性设置和配置参数 jinfo pid 1 . jmap : 内存占用情

  • 详解Java内存管理中的JVM垃圾回收

    一.概述 相比起C和C++的自己回收内存,JAVA要方便得多,因为JVM会为我们自动分配内存以及回收内存. 在之前的JVM 之内存管理 中,我们介绍了JVM内存管理的几个区域,其中程序计数器以及虚拟机栈是线程私有的,随线程而灭,故而它是不用考虑垃圾回收的,因为线程结束其内存空间即释放. 而JAVA堆和方法区则不一样,JAVA堆和方法区时存放的是对象的实例信息以及对象的其他信息,这部分是垃圾回收的主要地点. 二.JAVA堆垃圾回收 垃圾回收主要考虑的问题有两个:一个是效率问题,一个是空间碎片问题.

  • 详解JVM的内存对象介绍[创建和访问]

    作为java程序员对应Object应该是非常熟悉的,但是对于对象在JVM中的一些情况并不是很清楚,所以本文就来记录下对象在JVM中的一些内容 对象的创建 java程序中创建对象的常用方式是: Object obj = new Object(); 该行代码的执行过程如下: 从图中我们可以发现对象创建的步骤如下 执行new执行 检查这个指令参数是否能够在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否已经被加载,解析和初始化. 如果该类没有被加载则先执行类的加载操作 如果该类已经被

  • JVM:晚期(运行期)优化的深入理解

    晚期(运行期)优化 在部分的商用虚拟机中,Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为热点代码.为了提高热点代码的执行效率,在运行时,虚拟机会将这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器 本章提到的编译器.即时编译器都是指HotSpot虚拟机内的即时编译器,虚拟机也是特质HotSpot虚拟机 1.HotSpot虚拟机内的即时编译器 1).解释器与编译器 当程序需要迅速启动和执行的

  • JVM:早期(编译期)优化的深入理解

    早期(编译期)优化 JVM的编译器可以分为三个编译器: 前端编译器:把*.java转变为*.class的过程.如Sun的Javac.Eclipse JDT中的增量式编译器(ECJ) JIT编译器:把字节码转变为机器码的过程,如HotSpot VM的C1.C2编译器 AOT编译器:静态提前编译器,直接将*.java文件编译本地机器代码的过程 本章的后续文字里,"编译期"和"编译器"都仅限于第一类编译过程 1.Javac编译器 Javac编译器本身就是一个由Java语言

  • go select编译期的优化处理逻辑使用场景分析

    前言 select作为Go chan通信的重要监听工具,有着很广泛的使用场景.select的使用主要是搭配通信case使用,表面上看,只是简单的select及case搭配,实际上根据case的数量及类型,在编译时select会进行优化处理,根据不同的情况调用不同的底层逻辑. select的编译处理 select编译时的核心处理逻辑如下: func walkselectcases(cases *Nodes) []*Node { ncas := cases.Len() sellineno := li

  • 手把手教你实现Android编译期注解

    详细阐述了实现一个Android编译期注解sdk的步骤以及注意事项,并简要分析了运行时注解以及字节码技术在生成代码上与编译期注解的不同与优劣 一.编译期注解在开发中的重要性 从早期令人惊艳的ButterKnife,到后来的以ARouter为首的各种路由框架,再到现在谷歌大力推行的Jetpack组件,越来越多的第三方框架都在使用编译期注解这门技术,可以说不管你是想要深入研究这些第三方框架的原理 还是要成为一个Android高级开发工程师,编译期注解都是你不得不好好掌握的一门基础技术. 本文从基础的

  • 浅谈异常结构图、编译期异常和运行期异常的区别

    异常处理一般有2种方式,要么捕获异常try-catch,要么抛出异常throws 如果一个方法后面抛出一个运行时期异常(throws RuntimeException),调用者无须处理 如果一个方法后面抛出一个编译时期异常,调用者必须处理,或者抛出或者try-catch: 运行时期的异常一般都不处理,一般是程序逻辑上的错误,比如分母为0作为除数了... 注意如果在try里面出现了异常后,try下面的语句就不会执行,回去寻找catch匹配异常处理会,接下来的语句会处理的(也就是在try-catch

  • Android 利用 APT 技术在编译期生成代码

    APT(Annotation Processing Tool 的简称),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入.现在有很多主流库都用上了 APT,比如 Dagger2, ButterKnife, EventBus3 等,我们要紧跟潮流,与时俱进呐! (ง •̀_•́)ง 下面通过一个简单的 View 注入项目 ViewFinder 来介绍 APT 相关内容,简单实现了类似于ButterKnife 中的两种注解 @BindView 和 @OnClick . 项目

  • java自旋锁和JVM对锁的优化详解

    目录 背景 好处 AtomicLong的实现 getAndIncrement方法 实验 缺点 适用场景 JVM对锁做了哪些优化? 自适应的自旋锁 锁消除 锁粗化 偏向锁/ 轻量级锁/ 重量级锁 锁升级 背景 先上图 由此可见,非自旋锁如果拿不到锁会把线程阻塞,直到被唤醒: 自旋锁拿不到锁会一直尝试 为什么要这样? 好处 阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大. 在很多场景下,可能我们的同步代码块的内容并不多,所以

  • C++编译期循环获取变量类型详情

    目录 一.问题 二.解决方案 1.定义类型 2.定义属性集 3. 获取类型索引 4. 编译期循环 总结 一.问题 假设现在有一些属性以及这些属性对应的数值类型,比如: "gender" --> char "age" --> int "height" --> float "IQ" ---> int "name" --> std::string "weight"

  • webpack优化的深入理解

    前言 由于前端的快速发展,相关工具的发展速度也是相当迅猛,各大框架例如vue,react都有自己优秀的脚手架工具来帮助我们快速启动一个新项目,也正式因为这个原因,我们对于脚手架中最关键的一环webpack相关的优化知之甚少,脚手架基本上已经为我们做好了相关的开发准备,但是当我们想要做一些定制化的优化操作时,对webpack的优化也需要有一定的了解,否则无从下手,接下来就让我们进入webpack的优化世界 构建速度提升 loader提升 loader是webpack中最重要的特性,由于webpac

  • JVM中ClassLoader类加载器的深入理解

    JVM的体系结构图 先来看一下JVM的体系结构,如下图: JVM的位置 JVM的位置,如下图: JVM是运行在操作系统之上的,与硬件没有直接的交互,但是可以调用底层的硬件,用JIN(Java本地接口调用底层硬件) JVM结构图中的class files文件 class files文件,是保存在我们电脑本地的字节码文件,.java文件经过编译之后,就会生成一个.class文件,这个文件就是class files所对应的字节码文件,如下图: JVM结构图中的类加载器ClassLoader的解释 类加

随机推荐