Java String在JVM内存中的分布

2023年 8月 15日 82.3k 0

String str = "test"

用一段代码来举例:为一个String对象赋值一个字符串字面量“test”:

image.png

在启动项目时,在JVM类加载过程的“加载”阶段,当扫描到字节码文件中的“test”字面量时,JVM会把这个字面量存放在运行时常量池中。

然后在“解析”阶段,JVM会根据运行时常量池中的这个字符串字面量,在堆中创建出对应的String类对象,然后再把它的引用值存入到字符串常量池中。

截图.png

当JVM在执行如上的testForString()方法时,会将testForString()方法入栈,然后在对应的栈帧中存上局部变量String str1。

其中,str1变量被赋值的是字符串常量池中内容为“test”的String类对象的引用值。

image.png

现在举例下面的代码,我们定义两个变量str1和str2,并且都为它们赋值同一个字面量“test”,通过str1==str2这段代码来看看这两个变量是不是都指向的是同一个内存空间。

image.png

image.png

可以看到输出结果为true,这说明str1和str2指向的是同一个内存空间。

我们来图解一下此时JVM内存的数据分布:

image.png

可以看到,此时变量str1和str2被赋值的都是同一个内存空间的引用地址。

String str = new String()

对String对象的赋值还有一种情况,那就是: new String()。

我们来看看下面这段代码:

image.png

输出看一下结果:

image.png

可以注意到,str1和str2指向的都是同一个内存的空间,而str3却单独指向了一个别的内存空间。

来找找原因。

看如下代码:

image.png

在执行new关键词时,JVM会在堆中创建一个类型为String的对象,然后执行这个对象的构造方法。

image.png

image.png

从java.lang.String#String(java.lang.String)方法中可以看到,形参的value字段被赋值给了这个String类对象的value字段。

那么我们可以知道,此时JVM的内存数据分布大概是这个样子:

截图.png

当构造函数执行完毕后,这个对象就已构建完毕,然后JVM就会把这个新对象的引用赋值给变量str3:

截图.png

所以从这里我们可以得知,str3变量实际上引用的是堆中的一个新被创建的String类对象,所以这也就解释了为什么变量str3和str2指向的不是同一个内存空间的原因。

这是因为,str3指向的是堆中的新对象,而str2则是指向的字符串常量池中的引用地址。

它们的共同点,也就只有value字段引用的是同一个内存的空间。

“+”运算

对String对象的赋值还可以使用“+”运算。

在讲解之前,我们需要先要了解JVM对于字符串的“+”运算会做哪些处理。

JVM中,字符串的“+”运算有2个规则:

  • 如果“+”字符串运算的操作数不是在编译期间就被确定的,那么JVM就会自动把“+”替换成StringBuilder的append和toString方法。
  • 如果两个操作数在编译期间都已经被完全确定,比如操作数都是String字面量,那么JVM就不会把“+”转换为StringBuilder的append()方法,而是会直接将这2个字面量连接起来,然后根据连接的结果字面量再在堆中创建一个新的String对象,最后再将这个引用地址存到字符串常量池里。
  • 现在我们举例如下代码:

    image.png

    输出结果:

    image.png

    可以看到,变量str4的值是“testtest”,而str3的值也是“testtest”,可结果这2个变量指向的却不是同一个内存空间。

    这是为什么呢?

    由于字符串“+”运算的规则,对于代码String str4 = str1 + str2,由于操作数str1和str2并不是在编译期间就被确定的,所以JVM就会自动把“+”替换成StringBuilder的append和toString方法。

    也就是说,代码:

    String str4 = str1 + str2;
    

    被替换成了:

    String str4 = new StringBuilder().append(str1).append(str2).toString();
    

    所以代码编译后,实际上源代码的内容大概变成了下面这个样子:

    image.png

    所以可以知道,str1 + str2运算实际上会为变量str4在内存堆中新建一个StringBuilder类型的对象,然后通过这个对象的toString()方法,在内存堆中再新建一个String类对象,最后让str4引用这个对象。

    这就是为什么str3和str4不是指向的同一个内存空间的原因。

    因为str3被赋值的是字符串常量池中所存放的String类对象的引用值,而str4则是直接引用的堆中的一个新的String类对象:

    截图.png

    注意,Java编译器中还有一个优化特性:常量折叠。

    “常量折叠”的作用就是在代码编译期间将用到final变量的地方替换成这个变量对应的实际值。

    比如如下这个代码:

    image.png

    那么在项目编译时,原代码:

    image.png

    就变成了:

    image.png

    现在我们来执行一下上面的代码,看看此时的结果会是怎样的:

    image.png

    可以看到str3和str4指向的是同一个内存空间。

    这是为什么呢?

    对于代码String str3 = "test" + "test",由于这2个操作数在编译期间就被确定为"test",所以JVM就不会把“+”转换为StringBuilder的append()方法,而是会直接将这2个字面量连接起来,然后再把对应的引用地址保存到字符串常量池里。

    所以此时的内存数据布局大概如下所示:

    截图.png

    可以看到这个时候字符串常量池中已经有了“testtest”内容的String对象的引用了,所以当执行String str4 = "testtest"代码时,就可以直接把常量池中的这个引用拿来直接赋值给str4。

    截图.png

    这就是为什么str3和str4指向的是同一个内存空间的原因。

    .intern()

    String变量还可以使用java.lang.String#intern方法类来赋值。

    我们来看看这个方法的源码:

    image.png

    官方文档解释:当intern方法被调用时,如果此时常量池中已经包含有和String类对象同样文本内容的String类对象的引用,那么就会返回常量池中的这个引用,否则,就会先在常量池中存入这个文本内容的String类对象的引用值,然后返回它的引用。

    总的一句话就是:如果常量池中已经有了这种文本内容的String,那么就直接用,如果没有,那么就先在常量池中创建一个,然后在使用。反正最后引用的一定是常量池中的String。

    我们以下面代码举例:

    image.png

    输出结果:

    image.png
    可以看到,str1和str2指向的不是同一个内存空间,而str2和str3则是指向的同一个内存空间。

    我们来解释一下为什么会导致这种结果。

    我们知道,在类加载过程结束后,JVM内存数据布局大概会是如下所示:

    截图.png

    可以看到,此时字符串常量池中已经存放有了对“test”内容的String类对象的引用。

    然后JVM开始准备执行方法。

    将方法入栈,先执行这段代码:

    image.png

    此时JVM会在堆内存中新建一个String对象,并让str1变量引用这个对象的引用:

    截图.png

    在赋值完变量str1后,JVM接着执行代码:

    image.png

    str2会直接引用常量池中的“test”内容String类对象的引用值:

    截图.png

    然后,接着执行代码:

    image.png

    由于执行intern方法,所以JVM会先去常量池中查找有没有和str1同样文本内容的String类对象的引用。就像查表一样,通过文本内容来查找是否能够匹配到具有相同文本内容的记录。

    由于在类加载的时候,常量池中就已经有了对“test”内容的String类对象的引用,所以此时intern方法就会直接返回常量池中的这个引用值,然后赋值给变量str3:

    截图.png

    所以这就是为什么变量str2和str3指向的是同一个内存空间的原因。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论