Java技术指南「编译器专题」重塑认识Java编译器的执行过程(常量优化机制)!

Posted 李浩宇Alex

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java技术指南「编译器专题」重塑认识Java编译器的执行过程(常量优化机制)!相关的知识,希望对你有一定的参考价值。

问题概括

常量优化机制

  1. 给一个变量赋值,如果等于号的右边是常量的表达式并且没有一个变量,那么就会在编译阶段计算该表达式的结果。
  2. 然后判断该表达式的结果是否在左边类型所表示范围内。
  3. 如果在,那么就赋值成功,如果不在,那么就赋值失败。

结合问题,我们就可以大致猜出,如果常量能在编译期确定就会有优化,不能的话就不存在。

下面我们来详细讲解一下这个机制,Java中的常量池常量优化机制主要是两方面

就是对于byte/short/char三种类型的常量优化机制

先贴出一张Java八大数据类型大小范围表以供参考:

以下面这个程序为例
byte b1  = 1 + 2;
System.out.println(b1);
// 输出结果 3
运行结果解释:
换一种情况看看,把右边常量改成变量
byte b1  = 3;
byte b2  = 4;
byte b3 = b1 + b2;
System.out.println(b3);  // 程序报错

解释原因,从两个方面:
  • byte 与 byte (或者 short char ) 进行运算的时候会提升int 两个int 类型相加的结果也是int 类型

  • b1 和 b2 是两个变量,变量存储的是变化,在编译的时候无法判断里面的值,相加有可能会超出byte的取值这就是为什么一旦有变量参与表达式,那么就不会有编译期间的常量优化机制。

在这里我们试着把变量添加final改回常量,看看又有什么结果

final byte b1  = 1;
final byte b2  = 2;
byte b3 = b1 + b2;
System.out.println(b3); 

发现程序可以正常运行,输出结果为3,所以可知常量优化机制一定是针对常量的。

接下来我们再看另外一个程序
byte b1  = 127 + 2;   
System.out.println(b4);

某些场景下,取值范围大的数据类型(int)可以直接赋值给取值范围小的(byte、shor、char),而且只能特定int赋值给byte/short/char,其他基本数据类型不行,如下图。

int num1 = 10;
final int num2 = 10;
byte var1 = num1 + 20;  // 存在变量,编译报错
byte var2 = num2 + 20; // 编译通过

这个也是常量优化机制的一部分

所以我们这里总结一下byte/short/char三种类型的常量优化机制

  • 先判断值是否是常量, 然后再看值是否在该数据类型的取值范围内
  • 只有byte, short, char 可以使用常量优化机制,转换成int类型(这个你换成其他基本数据类型就不适应了)来个程序测试一下,下面这个就是单纯把之前的byte改成了int型,发现并不像之前报错,反而正常运行,输出结果3,所以就说明了只有byte, short, char 可以使用常量优化机制
int a = 1;
int b = 2;
int c = a + b;
System.out.println(c);
拓展一下(易错点):
byte var = 10;
var = var + 20; // 编译报错,运算中存在变量
var += 20; // 等效于: var = (short) (var + 20); 没有走常量优化机制,而是进行了类型转换

就是对于编译器对String类型优化(这个是重点难点)

String s1 = "abc";
String s2 = "a"+"b"+"c";
System.out.println(s1 == s2);
  • 这个输出的结果是多少呢?有人就会认为 “a” + “b”+“c"会生成新的对象"abc”,但是这个对象和String s2 = "abc"不同,(a == b)是比较对象引用,因此不相等,结果为false。

  • 如果你是这样想的话,那恭喜你对java的String有一定了解,但是你不清楚Java的常量池常量优化机制。

那么到底为什么呢,下面就来解释一下原因:

String s2 = “a” + “b”+“c”;编译器将这个"a" + “b”+“c"作为常量表达式,在编译时进行优化,直接取表达式结果"abc”,这里没有创建新的对象,而是从JVM字符串常量池中获取之前已经存在的"abc"对象。因此a,b具有对同一个string对象的引用,两个引用相等,结果true。

意思是说先通过优化,代码简化为

String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);

下面进一步探讨,什么样的String + 表达式会被编译器当成常量表达式?

String b = "a" + "b";
String a = "a1";
String b = "a" + 1;
System.out.println((a == b));  //result = true
String a = "atrue";
String b = "a" + true;
System.out.println((a == b));  //result = true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println((a == b));  //result = true

既然常量弄完了,我们换成变量来试试

String s1 = "ab";
String s2 = "abc";
String s3 = s1 + "c";
System.out.println(s3 == s2);
输出的结果是false
运行原理
  • String s3 = s1+“c”;这一句话,是在StringBuffer缓冲区中进行创建一个StringBuffer对象,将两者相加。

  • 但是对s3进行赋值时不能够直接将缓冲区的对象地址取来而是用toString方法变成另外的堆内存,然后赋值给s3,所以,s3和s2的地址值已经不同了,所以输出false。

这里我们还可以拓展一下,把s1前面加final修饰符修改为常量看看

final String s1 = "ab";
String s2 = "abc";
String s3 = s1 + "c";
System.out.println(s2 == s3);

再往里走一点,观察下面程序

private static String getS() {
    return "b";
}

String s1 = "abc";
String s2 = "a"+getS();
System.out.println((s1 == s2));
运行原理

我们来举个例子总结一下上面所有内容

public static void main(String[] arge) {

        //1
        String str1 = new String("1234");
        String str2 = new String("1234");
        System.out.println("new String()==:" + (str1 == str2));

        //2
        String str3 = "1234";
        String str4 = "1234";
        System.out.println("常量字符串==:" + (str3 == str4));

        //3
        String str5 = "1234";
        String str6 = "12" + "34";
        System.out.println("常量表达式==:" + (str5 == str6));

        //4
        String str7 = "1234";
        String str8 = "12";
        String str9 = str8 + "34";
        System.out.println("字符串和变量相加的表达式==:" + (str7 == str9));

        //5
        final String val = "34";
        String str10 = "1234";
        String str11 = "12" + val;
        System.out.println("字符串和常量相加的表达式==:" + (str10 == str11));

        //6
        String str12 = "1234";
        String str13 = "12" + 34;
        System.out.println("字符串和数字相加的表达式==:" + (str12 == str13));

        //7
        String str14 = "12true";
        String str15 = "12" + true;
        System.out.println("字符串和Boolen相加表达式==:" + (str14 == str15));

        //8
        String str16 = "1234";
        String str17 = "12" + getVal();
        System.out.println("字符串和函数得来的常量相加表达式==:" + (str16 == str17));
    }
    private static String getVal()
    {
        return "34";
    }

运行输出:

new String()==:false
常量字符串==:true
常量表达式==:true
字符串和变量相加的表达式==:false
字符串和常量相加的表达式==:true
字符串和数字相加的表达式==:true
字符串和Boolen相加表达式==:true
字符串和函数得来的常量相加表达式==:false

代码分析:

  • (1)String为引用类型,str1和str2为新实例化出来的对象,分别指向不同的内存地址。而==对于引用类型判断,是判断的是引用地址,所以例子1结果为false。

  • (2)对于第二个例子,编译器编译代码时,会将”1234”当做一个常量,并保存在JVM的常量池中,然后编译String str3=”1234”;时,将常量的指针赋值给str3,在编译String str4=”1234”;时,编译器查找常量池里有没有值相同的常量,如果有就将存在的常量赋给str4,这样结果就是str3和str4都指向了常量池中的常量的地址,所以==比较结果为true;

  • (3)第三个例子,编译时编译器发现能够计算出”12”+”34”的值,它是个常量,就按照第二个例子一样处理,最终str5和str6都指向了同一个内存地址。所以==比较结果为true;

  • (4)第四个例子,常量优化只针对常量,String str9 = str8 + “34”;这一句话,str9的值在运行时才能确定结果,是在StringBuffer缓冲区中进行创建一个StringBuffer对象,将两者相加。但是对s3进行赋值时不能够直接将缓冲区的对象地址取来而是用toString方法变成另外的堆内存,然后赋值给s3,所以,s3和s2的地址值已经不同了,所以输出false。

  • (5)第五个例子、第六个例子和第七个例子,类似第三个例子,编译时编译器发现能够计算出值,就尽量计算出来,所以==比较结果为true;

  • (6)第八个例子中,编译器发现str17值是要调用函数才能计算出来的,是要在运行时才能确定结果的,所以编译器就设置为运行时执行到String str17=“12” + getVal();时 要重新分配内存空间,导致str13和str1是指向两个不同的内存地址,所以==比较结果为false;

总结一下

String str = "a" + "b" + "c";  // 直接等价于 str = "abc"; 
 // 这个就解释了上面为true的所有情况

以上是关于Java技术指南「编译器专题」重塑认识Java编译器的执行过程(常量优化机制)!的主要内容,如果未能解决你的问题,请参考以下文章

Java技术专题「编译器专题」重塑认识Java编译器的执行过程(消除数组边界检查+公共子表达式)

JVM技术专题针对于ASM库生成和修改class文件开发指南 「 入门篇」

Java技术专题「编译器专题」深入分析探究“静态编译器”(JAVAIDEAECJ编译器)是否可以实现代码优化?

Java技术指南「编译器专题」深入分析探究“静态编译器”(JAVAIDEAECJ编译器)

JVM技术专题重塑你对类加载机制的认识「分析篇」

JVM技术专题 字节码指令集调用执行流程分析「语法分析篇」