【Java基础】字符串常量池

什么是字符串常量池

案例演示

首先提出一个问题:在下面这个代码段中有几个对象?

String s=new String("字符串")

答:2个对象。分别在字符串常量池1个,堆中有一个。
首先使用new关键字创建对象时,虚拟机会去字符串常量池中去找有没有“字符串”这个对象,如果有就不会再去创建对象,直接再堆中创建对象“字符串”。然后把堆中“字符串”的地址返回给s。
如果字符串常量池中没有就创建一个“字符串”对象,再去堆中创建对象“字符串”,并且s会指向堆中的这个对象。
在这里插入图片描述
在这里插入图片描述
这样就会有一个问题:在内存中开辟两块空间,对资源造成一定的浪费。为了解决这个问题,在创建字符串的时候做了一些优化,为字符串开辟一块空间------字符串常量池。

字符串常量池的作用

可以回想一下,在写代码的时候,我们一般是

String s="字符串"

这么定义的。
在执行这行代码时,先去字符串常量池中查找有无“字符串”对象,如果有的话,直接把该对象的地址赋值给s,如果没有的话,会去字符串常量池中创建对象:“字符串”,然后把字符串常量池中“字符串”的地址赋值给s。
在这里插入图片描述
在这里插入图片描述
这样的话,就省去了在堆中创建对象,节省了一部分的空间。

字符串常量池在内存中的位置

Java7之前

在java7之前,会把字符串常量池在永久代的区域内,主要用来存储字符串常量,也就是静态数据的一种。永久代是存储在堆的区域内的,用来存储类信息、方法信息、常量信息等静态数据。

而 Java 堆是 JVM 中存储对象实例和数组的内存区域,也就是说,永久代是 Java 堆的一个子区域。

换句话说,永久代中存储的静态数据与堆中存储的对象实例和数组是分开的,它们有不同的生命周期和分配方式。

但是,永久代和堆的大小是相互影响的,因为它们都使用了 JVM 堆内存,因此它们的大小都受到 JVM 堆大小的限制。

于是,当我们创建一个字符串常量时,它会被储存在永久代的字符串常量池中。如果我们创建一个普通字符串对象,则它将被储存在堆中。如果字符串对象的内容是一个已经存在于字符串常量池中的字符串常量,那么这个对象会指向已经存在的字符串常量,而不是重新创建一个新的字符串对象。

Java7之前,除去永久代,剩下的就是堆
在这里插入图片描述

Java7

永久代的大小是有限制的,而且不能确定一个程序需要多少永久代,如果该程序使用了大量的变量、方法,永久代可能就会被占满,会出现内存溢出的异常。所以为了解决该问题,在java7的时候就把字符串常量池移动到堆的区域内。
在这里插入图片描述

java8

Java8时,永久代被元空间取代,但是功能和永久代是一样的。只不过永久代是一块本机内存区域,和JVM内存区域是分开的。

元空间优点:

1、它不会导致 OutOfMemoryError 错误,因为元空间的大小可以动态调整。
2、元空间使用本机内存,而不是 JVM 堆内存,这可以避免堆内存的碎片化问题。
3、元空间中的垃圾收集与堆中的垃圾收集是分离的,这可以避免应用程序在运行过程中因为进行类加载和卸载而频繁地触发 Full GC。
在这里插入图片描述

String.intern()方法

intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

Java 7 的时候,字符串常量池从永久代中移动到了堆中,虽然此时永久代还没有完全被移除。Java 8 的时候,永久代被彻底移除。

这个变化也直接影响了 String.intern() 方法在执行时的策略,Java 7 之前,执行 String.intern() 方法的时候,不管对象在堆中是否已经创建,字符串常量池中仍然会创建一个内容完全相同的新对象; Java 7 之后呢,由于字符串常量池放在了堆中,执行 String.intern() 方法的时候,如果对象在堆中已经创建了,字符串常量池中就不需要再创建新的对象了,而是直接保存堆中对象的引用,也就节省了一部分的内存空间。

String s1 = new String("字符串1") + new String("字符串2")
String s2 = s1.intern();
System.out.println(s1 == s2);

执行这段代码时:String(“字符串1”)会在字符串常量池中创建对象,字符串1
String(”字符串2“)会在字符串常量池中创建独享,字符串2
执行new String(“字符串1”),会在堆上创建字符串对象”字符串1“
执行new String(“字符串2”),会在堆上创建字符串对象”字符串2“
执行:new String(“字符串1”) + new String(“字符串2”),会创建StringBuilder对象,并将字符串1和字符串2的内容追加其中,然后调用StringBuilder对象的toString()方法,将其转换为一个新的字符串对象,内容为”字符1字符串2“,这个新的字符串存储在堆上

也就是说,当编译器遇到 + 号这个操作符的时候,会将 new String(“二哥”) + new String(“三妹”) 这行代码编译为以下代码:

new StringBuilder().append("字符串1").append("字符串2).toString();

不过需要注意的是,尽管 intern 可以确保所有具有相同内容的字符串共享相同的内存空间,但也不要烂用 intern,因为任何的缓存池都是有大小限制的,不能无缘无故就占用了相对稀缺的缓存空间,导致其他字符串没有坑位可占。

另外,字符串常量池本质上是一个固定大小的 StringTable,如果放进去的字符串过多,就会造成严重的哈希冲突,从而导致链表变长,链表变长也就意味着字符串常量池的性能会大幅下降,因为要一个一个找是需要花费时间的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谷艳爽faye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值