【Java原理探索】「编译器专题」重塑认识Java编译器的执行过程(常量优化机制)!

简介: 【Java原理探索】「编译器专题」重塑认识Java编译器的执行过程(常量优化机制)!

问题概括


静态常量可以再编译器确定字面量,但常量并不一定在编译期就确定了, 也可以在运行时确定,所以Java针对某些情况制定了常量优化机制。



常量优化机制


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

注意如果一旦有变量参与表达式,那么就不会有编译期间的常量优化机制。


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


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



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


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


image.png

以下面这个程序为例
byte b1  = 1 + 2;
System.out.println(b1);
// 输出结果 3
复制代码



运行结果解释:


1和2都是常量,Java有常量优化机制,就是可以编译时可以明显确定常量结果,所以直接把1和2的结果赋值给b1了。(和直接赋值3是一个意思)


换一种情况看看,把右边常量改成变量
byte b1  = 3;
byte b2  = 4;
byte b3 = b1 + b2;
System.out.println(b3);  // 程序报错


image.png


程序报错了,意思说类型不匹配:无法从int转换为byte


解释原因,从两个方面:


  • 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,这里解释一下,byte取值范围为-128~127;很明显右边表达式的结果是否在左边类型所表示范围,这个就是导致此错误出现的原因。


某些场景下,取值范围大的数据类型(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的常量池常量优化机制。



这个代码正确输出结果为true!!!


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


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);
复制代码



再基于jvm对String的处理机制的基础上,得出true的结论。


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


String b = "a" + "b";
复制代码



这个String + String被正式是ok的,那么string + 基本类型呢?


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 + 基本类型是当成常量表达式直接求值来优化的。





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

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);
复制代码


输出的结果居然变成了true,看来只要是进入常量池的常量,就有可能存在常量优化机制



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


private static String getS() {
  return "b";
}
String s1 = "abc";
String s2 = "a"+getS();
System.out.println((s1 == s2));
复制代码

结果又是出人意料,竟然是false


运行原理


编译器发现s2值是要调用函数才能计算出来的,是要在运行时才能确定结果的,所以编译器就设置为运行时执行到String s3=“a” + getS();时 要重新分配内存空间,导致s2和s1是指向两个不同的内存地址,所以==比较结果为false;


看来String这个所谓的"对象",完全不可以看成一般的对象,Java对String的处理近乎于基本类型,最大限度的优化了几乎能优化的地方。





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

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
复制代码



代码分析:


Java中,String是引用类型;==是关系运算符,==比较两个引用类型时,判断的依据是:双方是否是指向了同一个内存地址。


  • (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;




总结一下


Java语言为字符串连接运算符(+)提供特殊支持,并为其他对象转换为字符串。 字符串连接是通过StringBuilder (或StringBuffer )类及其append方法实现的。 字符串转换是通过方法来实现toString(JDK1.8 api文档) 。(toString方法返回值是String,所以会返回一个String对象)。由于String的不可变性,对其进行操作的效率会大大降低,但对 “+”操作符,编译器对其进行了优化,往通俗来讲,如果编译时能直接得到最终字符串的结果就尽量获得最后的字符串,这样就免于中间创建对象的浪费了。


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



如果不能直接计算得到最终的字符串,就像上面的例子4一样,str17明显要调用函数才能计算出来的,是要在运行时才能确定结果,那肯定必须的开辟内存创建新的对象。具体就是通过黄色字体所描述的方法





相关文章
|
4天前
|
Java 程序员 容器
Java中的变量和常量:数据的‘小盒子’和‘铁盒子’有啥不一样?
在Java中,变量是一个可以随时改变的数据容器,类似于一个可以反复打开的小盒子。定义变量时需指定数据类型和名称。例如:`int age = 25;` 表示定义一个整数类型的变量 `age`,初始值为25。 常量则是不可改变的数据容器,类似于一个锁死的铁盒子,定义时使用 `final` 关键字。例如:`final int MAX_SPEED = 120;` 表示定义一个名为 `MAX_SPEED` 的常量,值为120,且不能修改。 变量和常量的主要区别在于变量的数据可以随时修改,而常量的数据一旦确定就不能改变。常量主要用于防止意外修改、提高代码可读性和便于维护。
|
5天前
|
Java 编译器
探索Java中的异常处理机制
【10月更文挑战第35天】在Java的世界中,异常是程序运行过程中不可避免的一部分。本文将通过通俗易懂的语言和生动的比喻,带你了解Java中的异常处理机制,包括异常的类型、如何捕获和处理异常,以及如何在代码中有效地利用异常处理来提升程序的健壮性。让我们一起走进Java的异常世界,学习如何优雅地面对和解决问题吧!
|
6天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
6天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
8天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
1天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
9 2
|
6天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
21 6
|
4天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
10天前
|
安全 IDE Java
Java反射Reflect机制详解
Java反射(Reflection)机制是Java语言的重要特性之一,允许程序在运行时动态地获取类的信息,并对类进行操作,如创建实例、调用方法、访问字段等。反射机制极大地提高了Java程序的灵活性和动态性,但也带来了性能和安全方面的挑战。本文将详细介绍Java反射机制的基本概念、常用操作、应用场景以及其优缺点。 ## 基本概念 ### 什么是反射 反射是一种在程序运行时动态获取类的信息,并对类进行操作的机制。通过反射,程序可以在运行时获得类的字段、方法、构造函数等信息,并可以动态调用方法、创建实例和访问字段。 ### 反射的核心类 Java反射机制主要由以下几个类和接口组成,这些类
25 2
|
14天前
|
存储 Java 开发者
成功优化!Java 基础 Docker 镜像从 674MB 缩减到 58MB 的经验分享
本文分享了如何通过 jlink 和 jdeps 工具将 Java 基础 Docker 镜像从 674MB 优化至 58MB 的经验。首先介绍了选择合适的基础镜像的重要性,然后详细讲解了使用 jlink 构建自定义 JRE 镜像的方法,并通过 jdeps 自动化模块依赖分析,最终实现了镜像的大幅缩减。此外,文章还提供了实用的 .dockerignore 文件技巧和选择安全、兼容的基础镜像的建议,帮助开发者提升镜像优化的效果。