JVM系列8-前端编译与优化

简介: JVM系列8-前端编译与优化

1. 概述

所谓”编译“,通俗来讲就是把我们写的代码“翻译“成机器可以读懂的机器码。而编译器就是做这个翻译工作的。

Java 技术中的编译器可以分为如下三类:

  • 前端编译器:把 *.java 文件转变为 *.class 文件的过程。比如 JDK 的 Javac。
  • 即时编译器:Just In Time Compiler,常称 JIT 编译器,在「运行期」把字节码转变为本地机器码的过程。比如 HotSpot VM 的 C1、C2 编译器,Graal 编译器。
  • 提前编译器:Ahead Of Time Compiler,常称 AOT 编译器,直接把程序编译成与目标机器指令集相关的二进制代码的过程。比如 JDK 的 Jaotc,GNU Compiler for the Java。

其中后面两类都属于后端编译器。

本文主要分析前端编译器 Javac 的相关内容,后文再介绍后端编译器。

2. Javac 编译器

Javac 的编译过程大致可以分为 1 个准备过程和 3 个处理过程:

  1. 准备过程:初始化插入式注解处理器。
  2. 解析与填充符号表过程
  1. 词法、语法分析:将源码中的字符流转变为标记集合,构造抽象语法树
  2. 填充符号表:产生符号地址和符号信息
  1. 插入式注解处理器的注解处理过程
  2. 分析与字节码生成过程
  1. 标注检查:对语法的静态信息进行检查
  2. 数据流及控制流分析:对程序的动态运行过程进行检查
  3. 解语法糖:将简化代码编写的语法糖还原为原来的样子
  4. 字节码生成:将前面各个步骤所生成的信息转化为字节码

我们可以把上述处理过程对应到代码中, Javac编译动作的入口是 com.sun.tools.javac.main.JavaCompiler类, 上述3个过程的代码逻辑集中在这个类的compile()和compile2() 方法里, 其中主体代码如下图所示, 整个编译过程主要的处理由图中标注的8个方法来完成

2.1 解析与填充符号表

2.1.1 词法、语法分析

  • 词法分析

将源码中的字符流转变为标记(Token)集合的过程。关键字、变量名、运算符等都可作为标记。比如下面一行代码:

int a = b + 2;

在字符流中,关键字 int 由三个字符组成,但它是一个独立的标记,不可再分。

该过程有点类似“分词”的过程。虽然这些代码我们一眼就能认出来,但编译器要逐个分析过之后才能知道。

  • 语法分析

根据上面的标记序列构造抽象语法树的过程。

抽象语法树(Abstract Syntax Tree,AST)是一种用来描述程序代码语法结构的树形表示方法,每个节点都代表程序代码中的一个语法结构(Syntax Construct),比如包、类型、修饰符等。

通俗来讲,词法分析就是对源码文件做分词,语法分析就是检查源码文件是否符合 Java 语法。

2.1.2 填充符号表

符号表(Symbol Table)是一种数据结构,它由一组符号地址和符号信息组成(类似“键-值”对的形式)。

符号由抽象类 com.sun.tools.javac.code.Symbol 表示,Symbol 类有多种扩展类型的符号,比如 ClassSymbol 表示类、MethodSymbol 表示方法等。

符号表记录的信息在编译的不同阶段都要用到,如:

  • 用于语义检查和产生中间代码;
  • 在目标代码生成阶段,符号表是对符号名进行地址分配的依据。

这个阶段主要是根据上一步生成的抽象语法树列表完成符号填充,返回填充了类中所有符号的抽象语法树列表。

2.2 注解处理器

JDK 5 提供了注解(Annotations)支持,JDK 6 提供了“插入式注解处理器”,可以在「编译期」对代码中的特定注解进行处理,从而影响前端编译器的工作过程。

比如效率工具 Lombok 就是在这个阶段进行处理的。示例代码:

import lombok.Getter;
@Getter
public class Person { 
    private String name; 
    private Integer age;
}

该代码编译后:

public class Person {    
    private String name;    
    private Integer age;    
    public Person() {    
    }    
    public String getName() {        
        return this.name;    
    }    
    public Integer getAge() {        
        return this.age;    
    }
}

其中两个 getter 方法就是 @Getter 注解在这个阶段产生的效果(具体实现原理网上可以找到相关内容)。

2.3 语义分析与字节码生成

抽象语法树能表示一个结构正确的源程序,但无法保证语义是否符合逻辑。

而语义分析就对语法正确的源程序结合上下文进行相关性质的检查(类型检查、控制流检查等)。比如:

int a = 1;
boolean b = false;

// 这样赋值显然是错误的
// 但在语法上是没问题的,这个错误是在语义分析时检查的
int c = a + b;

Javac 在编译过程中,语义分析过程可分为标注检查和数据及控制流分析两个步骤。

2.3.1 标注检查

检查内容:变量使用前是否已被声明、变量与赋值之间的数据类型是否匹配等。

  • 常量折叠

该过程中,还会进行一个常量折叠(Constant Folding)的代码优化。

比如,我们在代码中定义如下:

int a = 1 + 2;

在抽象语法树上仍能看到字面量 "1"、"2" 和操作符 "+",但经过常量折叠优化后,在语法树上将会被标注为 "3"。

2.3.2 数据及控制流分析

主要检查内容:

  • 局部变量使用前是否赋值
  • 方法的每条路径是否有返回值
  • 受检查异常是否被正确处理等

2.3.3 解语法糖

语法糖(Syntactic Sugar):也称糖衣语法,指的是在计算机语言中添加某种语法,该语法对语言的编译结果和功能并没有实际影响,却能更方便程序猿使用该语言。

Java 中常见的语法糖有泛型、变长参数、自动装箱拆箱等。

JVM 其实并不支持这些语法,它们在编译阶段要被还原成原始的基础语法结构。该过程就称为解语法糖。

2.3.4 字节码生成

Javac 编译过程的最后一个阶段。主要是把前面各个步骤生成的信息转换为字节码指令写入磁盘中。

此外,编译器还进行了少量的代码添加和转换工作。比如实例构造器 <init>() 和类构造器 <clinit>() 方法就是在这个阶段被添加到语法树的。

还有一些代码替换工作,例如将字符串的 "+" 操作替换为 StringBuilder(JDK 5 及以后)或 StringBuffer(JDK 5 之前) 的 append() 操作。

3. Java 语法糖

3.1 泛型

泛型这个概念大家应该都不陌生,Java 是从 5.0 开始支持泛型的。

由于历史原因,Java 使用的是“类型擦除式泛型(Type Erasure Generics)”,也就是泛型只会在源码中存在,编译后的字节码文件中,全部泛型会被替换为原先的裸类型(Raw Type)。

因此,在运行期间 List<String>List<Integer> 其实是同一个类型。例如:

public class GenericTest { 
  public static void main(String[] args) {  
    List<Integer> l1 = new ArrayList<>();  
    l1.add(1);  
    List<String> l2 = new ArrayList<>();  
    l2.add("2"); 
  }
}

经编译器擦除类型后:

public class GenericTest {    
  public GenericTest() {    
  } 
     
  public static void main(String[] var0) {       
  // 原先的泛型都没了       
   ArrayList var1 = new ArrayList();        
  var1.add(1);        
  ArrayList var2 = new ArrayList();        
  var2.add("2");    
  }
}

类型擦除是有缺点的,比如:

  1. 由于类型擦除,会将泛型的类型转为 Object,但是 int、long 等原始数据类型无法与 Object 互转,这就导致了泛型不能支持原始数据类型。进而引起了使用包装类(Integer、Long 等)带来的拆箱、装箱问题。
  2. 运行期无法获取泛型信息。

3.2 自动装箱、拆箱与遍历

  • 遍历代码示例
public class GenericTest { 
  public static void main(String[] args) {  
    List<String> list = Arrays.asList("hello", "world");  
    for (String s : list) {   
      System.out.println(s);  
    } 
  }
}

反编译版本 1:

public class GenericTest {    
  public GenericTest() {    
  }    
  public static void main(String[] args) {        
    List<String> list = Arrays.asList("hello", "world");       
    // 使用了迭代器 Iterator 遍历        
    Iterator var2 = list.iterator();        
    while(var2.hasNext()) {            
      String s = (String)var2.next();            
      System.out.println(s);        
    }    
  }
}

反编译版本 2:

public class GenericTest {  
  public static void main(String[] args) {    
    // 创建一个数组    
    List<String> list = Arrays.asList(new String[]{ "hello", "world" });    
    for (String s : list)      
    System.out.println(s);   
  }
}

不同的反编译器得出的结果可能有所不同,这里找了两个版本对比分析。

从上面两个版本的反编译结果可以看出:Arrays.asList() 方法其实创建了一个数组,而增强 for 循环实际调用了迭代器 Iterator。

  • 自动拆装箱代码示例
public class GenericTest { 
  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);  
    System.out.println(e == f);  
    System.out.println(c == (a + b));  
    System.out.println(c.equals(a + b));  
    System.out.println(g == (a + b));  
    System.out.println(g.equals(a + b)); 
  }
}

类似代码估计大家都见过,毕竟有些面试题就喜欢这么搞,这些语句的输出结果是什么呢?

我们先看反编译后的代码:

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

可以看到,编译器对上述代码做了自动拆装箱的操作。其中值得注意的是:

  1. 包装类的 "==" 运算不遇到算术运算时,不会自动拆箱。
  2. equals() 方法不会处理数据转型。

此外,还有个值得玩味的地方:为何 c==d 是 true、而 e==f 是 false 呢?似乎也是个考点。

这就要查看 Integer 类的 valueOf() 方法的源码了:

static final int low = -128;
public static Integer valueOf(int i) {    
  if (i >= IntegerCache.low && i <= IntegerCache.high)        
            return IntegerCache.cache[i + (-IntegerCache.low)];    
  return new Integer(i);
}
private static class IntegerCache {    
  static final int low = -128;    
  static final int high;    
  static final Integer cache[];    
  static {        
  // high value may be configured by property        
  int h = 127;        
  String integerCacheHighPropValue =sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");        
  if (integerCacheHighPropValue != null) {            
      try {                
      int i = parseInt(integerCacheHighPropValue);
      i = Math.max(i, 127);                
      // Maximum array size is Integer.MAX_VALUE              
        h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
      } catch( NumberFormatException nfe) { 
            // If the property cannot be parsed into an int, ignore it.     
             }       
}       
  high = h;        
  cache = new Integer[(high - low) + 1];        
  int j = low;        
  for(int k = 0; k < cache.length; k++)           
      cache[k] = new Integer(j++);   
     
  // range [-128, 127] must be interned (JLS7 5.1.7)       
         assert IntegerCache.high >= 127;    

  }  
      
  private IntegerCache() {}
}

可以看到 Integer 内部使用了缓存 IntegerCache:其最小值为 -128,最大值默认是 127。因此,[-128, 127] 范围内的数字都会直接从缓存获取。

而且,该缓存的最大值是可以修改的,可以使用如下 VM 参数将其修改为 500:

-XX:AutoBoxCacheMax=500复制代码

增加该参数后,上述 e==f 也是 true 了。


目录
打赏
0
0
0
0
93
分享
相关文章
如何优化前端性能以提高加载速度
前端性能优化对提升网站加载速度至关重要,直接影响用户体验、SEO排名和转化率。本文介绍了优化前端加载速度的关键技巧,包括最小化HTTP请求、使用CDN、优化图片、利用浏览器缓存、压缩文件和实现懒加载。通过这些方法,可以显著减少页面加载时间,提高网站的整体性能和用户满意度。
JVM实战—10.MAT的使用和JVM优化总结
本文详细探讨了JVM内存管理与性能优化的关键问题。首先分析了线上大促活动引发的老年代内存泄漏及频繁FGC问题,通过MAT工具定位到本地缓存未正确处理的原因,并提出使用Ehcache等框架解决。接着讨论了百万级数据误处理导致的频繁FGC案例,深入剖析String.split()方法在特定JDK版本下的内存消耗问题,并给出多线程并发处理大数据量的优化建议。文章还总结了JVM运行原理、GC机制以及YGC和FGC的触发条件,明确了正常系统GC频率指标。最后提供了JVM性能优化的整体思路,包括新系统开发时的参数预估、压测后的调整策略以及线上系统的监控方法,同时列举了常见的FGC原因及对应解决方案。
129 79
JVM实战—10.MAT的使用和JVM优化总结
前端 CSS 优化:提升页面美学与性能
前端CSS优化旨在提升页面美学与性能。通过简化选择器(如避免复杂后代选择器、减少通用选择器使用)、合并样式表、合理组织媒体查询,可减少浏览器计算成本和HTTP请求。利用硬件加速和优化动画帧率,确保动画流畅。定期清理冗余代码并使用缩写属性,进一步精简代码。这些策略不仅加快页面加载和渲染速度,还提升了视觉效果,为用户带来更优质的浏览体验。
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
670 166
聊聊JVM如何优化
JVM的优化是一个复杂而细致的过程,涉及内存管理、垃圾回收、即时编译、线程调度等多个方面。通过合理配置JVM参数、选择合适的垃圾回收器、优化线程调度和使用专业的监控工具,可以大幅提升Java应用的性能和稳定性。掌握这些优化技巧,能够帮助开发者在高并发、高负载的生产环境中保持系统的高效运行。
88 13
|
3月前
|
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
98 0
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
Vue 性能革命:揭秘前端优化的终极技巧;Vue优化技巧,解决Vue项目卡顿问题
Vue在处理少量数据和有限dom的情况下技术已经非常成熟了,但现在随着AI时代的到来,海量数据场景会越来越多,Vue优化技巧也是必备技能。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
婚恋交友系统平台 相亲交友平台系统 婚恋交友系统APP 婚恋系统源码 婚恋交友平台开发流程 婚恋交友系统架构设计 婚恋交友系统前端/后端开发 婚恋交友系统匹配推荐算法优化
婚恋交友系统平台通过线上互动帮助单身男女找到合适伴侣,提供用户注册、个人资料填写、匹配推荐、实时聊天、社区互动等功能。开发流程包括需求分析、技术选型、系统架构设计、功能实现、测试优化和上线运维。匹配推荐算法优化是核心,通过用户行为数据分析和机器学习提高匹配准确性。
256 3
探索前端性能优化:关键策略与代码实例
本文深入探讨前端性能优化的关键策略,结合实际代码示例,帮助开发者提升网页加载速度和用户体验,涵盖资源压缩、懒加载、缓存机制等技术。

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等