jvm(10)-早期(编译期)优化

【0】README

0.1)本文部分文字描述转自 “深入理解jvm”,旨在学习  早期(编译期)优化 的基础知识;

0.2)本文部分文字描述转自: http://www.cnblogs.com/zhouyuqin/p/5223180.html

 

【1】概述

1)java中的编译期是一段不确定的操作过程(process),可能是:
  • process1)指一个前端编译器把 *.java 文件转变为 *.class 文件的过程;
  • process2)指虚拟机的后端运行期编译器(JIT编译器,just in time compiler)把字节码转变为机器码的过程;
  • process3)指使用静态提前编译器(AOT编译器,Ahead Of Time Compiler)直接把*.java  文件编译成本地机器码的过程;
2)下面是编译过程中有代表性的编译器:
  • 2.1)前端编译器:Sun 的javac,Eclipse JDT 中的增量式编译器(ECJ);
  • 2.2)JIT编译器:HotSpot VM 的C1, C2 编译器;
  • 2.3)AOT编译器: GNU Compiler for the Java(GCJ),Excelsior JET;
Attention)本文提到的编译器是前端编译器;

【2】 Javac 编译器
【2.1】 Javac 的源码和调试
1)从 Sun Javac 的代码来看,编译过程大致可以分为3个过程,分别是:(干货——编译过程大致分为3个过程)
  • step1)解析和填充符号表过程;
  • step2)插入式注解处理器的注解处理过程;
  • step3)分析与字节码生成过程;
2)以上3个步骤之间的关系与交互顺序如下图所示:

2.1)Javac 编译动作的入口: 是 com.sun.tools.javac.main.JavaCompiler 类,上述3个过程的代码逻辑集中在这个类的 compile() 和 compile2() 方法中,主体代码如下图所示,整个编译最关键的处理就由以下8个方法来完成:

【2.2】解析与填充符号表
1)解析符号表的步骤(词法、语法分析+填充符号表)
1.1)词法、语法分析(干货——引入词法、语法分析)
  • 1.1.1)词法分析:是将源代码的字符流转变为 标记集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符都可以成为标记;如“int a = b+2” 包含了6个标记,分别是 int, a, =, b, +, 2 ;
  • 1.1.2)语法分析:是根据Token 序列构造抽象语法树的过程,抽象语法树(abstract syntax tree==AST)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构,如包,类型,修饰符,运算符,接口,返回值等;
1.2)填充符号表
  • 1.2.1)符号表定义:符号表是一组符号地址和符号信息构成的表格,读者可以把它想象成哈希表中键值对的形式;
  • 1.2.2)完成抽象语法树之后,下一步就是填充符号表的过程,即enterTrees()方法。符号表是由一组符号地址和符号信息构成的表格,类似于哈希表中K-V值对的形式。符号表中所登记的信息在编译的不同阶段都要用到。当对符号名进行地址分配时,符号表是地址分配的依据。填充过程由com.sun.tools.javac.comp.Enter类实现。
【2.3】注解处理器

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

【2.4】语义分析与字节码生成

0)语法分析之后,编译器获得了程序代码的抽象语法树表示,语法树能表示一个结构正确的源代码抽象。而语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,如进行类型审查。在Javac编译过程中,语法分析过程分为标注检查以及数据及控制流分析两个步骤,分别对应着attribute()和flow()方法完成。

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

标注检查步骤在Javac源码中的实现类是com.xun.tools.javac.comp.Attr和com.sun.tools.javac.comp.Check类。
2)数据及控制流分析 : 数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以查出诸如程序员局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。编译期的数据及控制流分析与类加载时的数据及数据流分析的目的基本上是一致的,但校验范围有所区别,有一些校验项只有在编译期或者运行期才能进行。如将局部变量声明为final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障,在Javac的源码中,数据及控制流分析的入口是flow()方法,具体操作由com.sun.tools.javac.comp.Flow类来完成。 (干货——将局部变量声明为final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障, 看个荔枝如下)
jvm(10)-早期(编译期)优化_第1张图片
3)解语法糖 (干货——语法糖就是一种编写代码的语法)
  • 3.1)语法糖:是指在计算机语言中添加某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。 
  • 3.2)Java是一种“低糖语言”:常用的语法糖主要是之前提到的泛型、变长参数、自动装箱/拆箱等。虚拟机运行时不支持这些语法,它们在编译期还原回简单的基础语法结构,这个过程称为解语法糖。解语法糖的过程是由desuger()方法触发的。(干货——引入解语法糖的概念)
4)字节码生成 :
  • 4.1)字节码生成是Javac编译过程的最后一个阶段:由com.sun.tools.javac.jvm.Gen类来完成,字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化为字节码写入磁盘中,编译器还进行了少量代码添加和转换工作。 
  • 4.2)完成对语法树的遍历与调整之后:就会把填充了所有所需信息的符号表交给com.sun.tools.javac.jvm.ClassWriter类,由这个类的wrtieClass()方法输出字节码,生成最终的Class文件。
【3】 java语法糖
【3.1】泛型和类型擦除
1) 泛型是JDK1.5新增的特性,它的本质是 参数化类型 的应用,也就是说所操作的数据类型被指定为一个参数,这种参数类型可以用于类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。  

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

2.1)看个荔枝

public class GenericEraseTest {
	public static void main1(String[] args) { // 泛型擦除前的荔枝
		Map<String, String> map = new HashMap<>();
		map.put("hello", "你好");
		map.put("how are you?", "吃了没?");
		System.out.println(map.get("hello"));
		System.out.println(map.get("how are you?"));
	}
	
	public static void main(String[] args) { // 泛型擦除后的荔枝
		Map map = new HashMap();
		map.put("hello", "你好");
		map.put("how are you?", "吃了没?");
		System.out.println(map.get("hello"));
		System.out.println(map.get("how are you?"));
	}
}

3)故当List<Integer>和List<String>作为参数时,擦除使得两者的特征签名变得一模一样,有时可能导致拥有该两个方法参数的方法无法重载。


Attention)值得注意的是:当出现上述的情况的时候,如果返回值不一样的话,该两个方法是可以存在于一个Class文件中的,总结一下,两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是合法地,可以共存于一个Class文件中。

4)擦除法所谓的擦除:仅仅是对方的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。(干货——擦除法的执行本质)

【3.2】自动装箱、拆箱与遍历循环

1)自动装箱、自动拆箱与遍历循环这些语法糖:无论实现上还是思想上都不能和上文介绍的泛型相比,两者的难度和深度都有很大差距;但是它们是java 中使用得最多的语法糖;

1.1)看个荔枝:

public class SyntacticSugar {
	// 自动装箱,拆箱与遍历循环
	public static void main1(String[] args) {
		List<Integer> list = Arrays.asList(1, 2, 3, 4);
		// 如果在jdk1.7中,还有另外一颗语法糖 
		// 能让上面这句代码进一步简写成 List<Integer> list = [1, 2, 3, 4];
		int sum = 0;
		for (int i : list) {
			sum += i;
		}
		System.out.println(sum);
	}
	// 自动装箱,拆箱与遍历循环编译之后
	public static void main(String[] args) {
		List list = Arrays.asList(new Integer[] {
			Integer.valueOf(1),
			Integer.valueOf(2),
			Integer.valueOf(3),
			Integer.valueOf(4),
			Integer.valueOf(5)
		});
		int sum = 0;
	}
}

1.2)再看个荔枝,自动装箱的陷阱

public class AutoPackage
{
	public static void main(String[] args)
	{
		Integer a = 1;
		Integer b = 2;
		Integer c = 3;
		Integer d = 3;
		Integer e = 312;
		Integer f = 312;
		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
	}
}

对以上代码的分析(Analysis):鉴于包装类的 == 运算在不遇到算术运算的情况下不会自动拆箱,以及它们equals()方法不处理数据转型的关系,作者建议在实际编码中应该尽量避免这样使用自动装箱和拆箱;

【3.3】条件编译 

1)java语言也可以进行条件编译,方法就是使用条件为常量的if语句;

// 通过if语句实现条件编译
public class ContitionalCompile {
	public static void main(String[] args) {
		if(true) {
			System.out.println("true");
		} else {
			System.out.println("false");
		}
	}
}

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

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


你可能感兴趣的:(jvm(10)-早期(编译期)优化)