老马的JVM笔记(六)----编译器优化

转眼一本书记了一半了,看了个囫囵吞枣,但也不算全无收获。只是过于硬核,希望可以有实用价值。

这一章其实是告诉你编译器怎么优化,不是教你怎么优化自己的代码。

6.1 早期优化--Javac编译器

Java的编译期有:前期编译器,把.java编译成.class文件(javac编译器);后期运行期编译器,JIT编译器(Just In Time Compiler),用于将class文件中的字节码转变成机器码(C1,C2编译器);静态提前编译器,AOT(Ahead Of Time Compiler),可以直接把.java编译成机器码(GNU Compiler for the JAVA)。本章主要针对前期编译器。(早期优化)

1.解析与填充符号表

词法、语法分析:将语句字节流解析成标记(token)的集合,token为语句中能拆解出的最小单位(大概就是能被space分隔开的单位)。语法分析会将token序列构造成抽象语法树(AST)。

填充符号表:符号表是一组装有符号地址和符号信息的表格,类似键值对表。每个阶段都会用到符号表,javac编译器可以填写。

2.注解处理器

读取Annotation。

3.语义分析与字节码生成

标注检查:检查变量在使用时是否被声明,变量和变量值的类型是否匹配等。编译期间就会进行变量折叠。

数据及控制流分析:分析上下文逻辑是否顺畅。局部变量使用时是否已被赋值,每个方法是否有返回语句...

解语法糖:语法糖,就是一些可以帮忙偷懒的语句。翻译成正常语句。

字节码生成:把前面生成的东西转化成字节码,写入磁盘中。

语法糖

1.泛型与类型擦除

把类型模糊成一种,类似List<E>,本质上都是Object,硬转,不算是硬核泛型。而且是Java自己把类型擦掉了,就是说List<Integer>和List<String>在编译期属于一种东西,会引起混乱,尤其在重载时,分不清。

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

// 自动装箱
List<Integer> list = [1,2,3,4];
// 遍历循环与拆箱
for(int i: list){
    System.out.println(i);
}

3.条件编译

条件为常量时编译器会自己判断代码可达性。

if(true){
    System.out.println("true");
}else {
    // unreachable
    System.out.println("false");
}

while(false){
    // unreachable
    System.out.println("false");
}

6.2 晚期(运行期)优化---JIT编译器

6.2.1 热点代码

即时编译器不是虚拟机必需组件,却是衡量虚拟机性能的重要组件。Java使用编译器将代码预编译成字节码,再用字节码解释器运行。

在运行过程中,被多次调用的方法,被多次执行的循环体会被标记为“热点代码”。虽然循环体就在方法里,但java自有办法看出来一个方法中的多次循环。具体怎么算多?

1.基于采样的热点探测(sample based hot spot detection):周期性检查每个线程的栈顶,如果该方法经常出现在栈顶则为热点代码,然而怎么算经常?这是不不准确的。

2.基于计数器的热点探测(counter based hot spot detectoin):具体就是用阈值。使用计数器统计方法的执行次数,超过阈值就是热点,那怎么取阈值?就在程序员选择了。计数器分两种,方法调用计数器(invocation counter)与回边计数器(back edge counter),阈值是两个计数器之和。方法调用计数器统计方法被调用的次数,如果方法未被编译,则+1,直到两计数器之和达到阈值,交给编译器编译该方法。如果一定时间(也是自设参数)为达到阈值,将该方法的方法调用计数器减半,称为衰减(counter decay),这段时间叫半衰期(counter half life time)。回边计数器用于统计循环体,每次循环结束后跳记为一次回边。回边计数器没有衰减机制。同样,两计数器和超过阈值,交给编译器编译。

6.2.2 编译过程

具体编译器的编译过程暂且跳过。  就是字节码->高级中间代码(High-level intermediate representation)->低级中间代码(low-level intermediate representation),最后扫描成既期待吗。

6.2.3 编译优化

Java虚拟机的优化措施都集中在了JIT中,所以编译的代码肯定比解释器运行快。编译器有很多方法优化代码,是JVM给程序员的福利。程序员想优化自己的代码还要去看别的书,所以这部分一直在跳过。但也看看JVM的优化方式,给自己找找灵感,也给JVM省省事。

内联(Method Inlining):方法的内联可以省去调用方法的成本,因为调用方法就要建立栈帧;另外可以为其他优化做基础。内联是编译器优化的最佳工具。简单说就是把方法中的简单语句直接拽到调用者这里,省去了调用的过程。多说无益,写个例子一层层扒去没用的代码就完了。

static class B{
    int value;
    final int get(){
        return value;
    }
}

public void foo(){
    y = b.get();
    z = b.get();
    sum = y + z;
}

第一次优化,省去get()的调用。

public void foo(){
    y = b.value;
    z = b.value;
    sum = y + z;
}

y,z等值,没必要再调用一次value。

public void foo(){
    y = b.value;
    z = y;
    sum = y + z;
}

还要给z开辟内存空间,没必要。

public void foo(){
    y = b.value;
    sum = y + y;
}

最后就非常简单了,但这是编译器做的事,你能这么写代码吗?不可能。

1.公共子表达式消除

一个表达式如果已经求出来值赋给一个变量,那这个表达式就再也不需要被计算了。这个编译器可以做到,但程序员能做还是不要经常一个表达式写好几次,应该是这个意思。

2.数组边界检查消除

不是每次查找数组元素都一定要检查边界值,如果已知index只会在[0,array.length)范围内,就不会超过边界值。但偷懒一定会有代价。

3.方法内联

方法内联不只是简单的把方法内的代码复制过来,因为涉及到多态时选择会成为问题。具体怎么内联,就交给JVM开发人员了。

“类型继承关系分析”(Class Hierarchy Analysis),用于确定目前加载出来的:类中,是否存在子类,子类中是否存在抽象类;接口中,是否有多于一种实现。这样在内联时,如果没有遇到虚方法,直接联;如果是虚方法,在CHA中查,为了防止查不到,还设计了“逃生门”用于后退。还是很有才的。

4.逃逸分析

逃逸分析用于分析对象的动态作用域。什么是逃逸?对象在方法中被创建后,被其他线程或外部方法访问到,称为逃逸。逃逸不是一件好事,代码要低耦合。如果可以证明对象不能逃逸,就可以针对其做出一些优化:

·栈上分配:虽然对象理应被分配到堆上,但如果不逃逸,可以在栈上分配,这样方法结束,栈销毁,对象也销毁。

·同步消除: 没有逃逸,就不会被装进同步区。

·标量替换:如果该对象不会逃逸,可以将其拆分成若干标量(标量指Java中最小单位,int,char等)。

6.2.4 Java与C/C++编译器对比

说Java速度慢,曾经主要因为解释器的速度慢。现在的区别主要出于两者的编译器速度,Java使用即时编译器,C++使用静态编译器。

Java不行的地方:

1.即时编译器需要与运行抢时间,势必增长运行时间。无论怎么优化,都会在运行时感到延迟。

2.Java是类型安全语言,在运行时需要频繁检查变量空指针、数组越界、类型继承等问题,像一个操心的老母亲。

3.因为面向对象,所以多态,所以虚方法很多。在多态方面,频率远高于C++,所以在编译时需要时间,例如上文提到的内联。

4.Java支持动态扩展,因此在运行中会改变现有的类继承关系,而因为动态,所以不能看到实时全貌,所以变化时会有调整。

5.Java的对象都在堆上分配(非逃逸的栈上对象不提),只有局部变量在栈上分配。而C/C++可以在堆上栈上分配,栈上的对象易于回收。主要在C++的对象都是程序员主动回收,成败在个人。

Java行的地方:

所有速度不行的原因,都是Java行的地方。时间不是白花的。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值