解析Java中String对象在JVM中的内存位置

String str = “test”

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

在这里插入图片描述

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

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

在这里插入图片描述

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

String str = new String()

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

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

在这里插入图片描述

输出看一下结果:

在这里插入图片描述

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

来找找原因。

看如下代码:

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

可以知道,此时JVM的内存分布大概是这个样子:

在这里插入图片描述

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

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

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

“+”运算

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

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

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

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

现在我们举例如下代码:

在这里插入图片描述

输出结果:

在这里插入图片描述

可以看到,变量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();

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

在这里插入图片描述

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

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

因为str3引用的是字符串常量池的String类对象,而str4则是引用的堆中的一个新String类对象:

在这里插入图片描述

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

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

比如如下这个代码:

在这里插入图片描述

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

在这里插入图片描述

就变成了:

在这里插入图片描述

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

在这里插入图片描述

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

这是为什么呢?

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

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

在这里插入图片描述

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

在这里插入图片描述

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

.intern()

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

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

在这里插入图片描述

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

我们以下面代码举例:

在这里插入图片描述

输出结果:

在这里插入图片描述

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

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

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

在这里插入图片描述

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

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

然后,接着执行代码:

在这里插入图片描述

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

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

在这里插入图片描述

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

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值