Java字符串常量池、运行时常量池 (史上最全,持续更新)

文章很长,而且持续更新,建议收藏起来,慢慢读 总目录 博客园版 为您奉上珍贵的学习资源 :

  • 免费赠送 经典图书:《Java高并发核心编程(卷1)》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

  • 免费赠送 经典图书:《Java高并发核心编程(卷2)》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

  • 免费赠送 经典图书:《Netty Zookeeper Redis 高并发实战》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

  • 免费赠送 经典图书:《SpringCloud Nginx高并发核心编程》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

  • 免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取

  • 推荐:尼恩Java面试宝典(持续更新 + 史上最全 + 面试必备)具体详情,请点击此链接

    尼恩Java面试宝典,33个最新pdf,含2000多页不断更新、持续迭代 具体详情,请点击此链接

    在这里插入图片描述


    社群问题:JVM 常量池和字符串常量池,不是同一个池吧?

    在这里插入图片描述

    字符串常量池

    在这里插入图片描述

    JDK1.8之前,Hotspot虚拟机中,字符串常量池存放于方法区的实现永久代(Perm Space)中,JDK1.8开始,字符串常量池移到了堆中。
    字符常量池本质上是一张哈希表,key为由字符串内容及其长度生成的哈希值,value为字符串对象在堆中的地址。

    常量池与运行时常量池

    public class T01_ConstantPool {
        public static void main(String[] args) {
      
        }
    }
    
    

    运行上面的程序后,可在项目目录下找到T01_ConstatntPool.class文件,我们将目录切换到class文件所在目录,运行命令javap -v T01_ConstantPool.class,在运行结果中可以看到类似下方的Constant pool信息。

    Constant pool:
       #1 = Methodref          #3.#17         // java/lang/Object."<init>":()V
       #2 = Class              #18            // T01_Pool
       #3 = Class              #19            // java/lang/Object
       #4 = Utf8               <init>
       #5 = Utf8               ()V
       #6 = Utf8               Code
       #7 = Utf8               LineNumberTable
       #8 = Utf8               LocalVariableTable
       #9 = Utf8               this
      #10 = Utf8               T01_ConstantPool;
      #11 = Utf8               main
      #12 = Utf8               ([Ljava/lang/String;)V
      #13 = Utf8               args
      #14 = Utf8               [Ljava/lang/String;
      #15 = Utf8               SourceFile
      #16 = Utf8               T01_ConstantPool.java
      #17 = NameAndType        #4:#5          // "<init>":()V
      #18 = Utf8               T01_Pool
      #19 = Utf8               java/lang/Object
    
    

    这个Constant pool就是常量池,编译时每个类都会产生类似的产量池,我们可以看到,每个常量指向的都是一个符号引用(类/方法/属性的全限定名)。
    到了运行时期,Constant pool常量池中符号引用会被转化为直接引用(类/方法/属性在内存中的真实地址),从而Constant pool常量池就转变为了运行时常量池。运行时常量池位于方法区(永久代/元空间)。

    intern方法的作用

    intern()方法的功能定义:

    (1)如果当前字符串内容存在于字符串常量池(即equals()方法为true,也就是内容一样),那直接返回此字符串在常量池的引用;

    (2)如果当前字符串不在字符串常量池中,那么在常量池创建一个引用并指向堆中已存在的字符串,然后返回常量池中的引用。

    简单说intern方法就是判断并将字符串是否存在于字符串常量池,如果不存在则创建,存在则返回。

    字符串常量池

    在HotSpot中实现字符串常量池功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009。在每个HotSpot虚拟机的实例中只有一份,被所有的类共享。字符串常量由一个个字符组成,放在了StringTable上。

    JDK6及之前版本,字符串常量池是放在Perm Gen区(方法区)中。StringTable的长度是固定的,长度是1009,当String字符串过多时会造成hash冲突,导致链表过长,性能大幅度下降。此时字符串常量池里面放的全部是字符串常量(字面值)。

    由于永久代的空间有限且固定,JDK6的存储模式很容易造成OutOfMemoryError。

    而JDK7时正在着手去永久代的工作,因此字符串常量池被放在了堆中。此时,即使堆的大小也是固定的,但对于应用调优工作,只需要调整堆大小就行了。

    在JDK7中字符串常量池不仅仅可以存放字符串常量,还可以存放字符串的引用。也就是说,堆中的字符串的引用可以作为常量池的值而存在。

    字符串池化流程分析

    在了解了上面的基础理论,我们下面以图文相结合的形式来逐步演示字符串池化的流程和分类。以下实例以JDK8版本为基础来进行分析讲解。

    当我们通过双引号声明一个字符串:

    String wechat = "程序新视界";
    
    
    

    此时,双引号内的字符串会被直接存储在字符串常量池中。
    img

    关于上面的存储结构,我们已经在之前文章中提到,不再过多解释。下面如果我们再声明同样的字符串看看会有什么样的变化。

    String wechat = "程序新视界";
    String wechat1 = "程序新视界";
    
    
    

    上述代码中声明wechat1时,会发现常量池中已经存在了对应的字符串,则不会再重新创建,只是把对应的引用返回给wechat1。对应结构图如下:

    [img

    此时,如果直接用双等号比较wechat和wechat1肯定是相等的,因为它们的引用和字面值都是相同的。

    上面是直接双引号赋值的情况,那么如果通过new的形式创建字符串对应的流程又是如何呢?前面文章已经讲到这分两种情况:常量池存在对应的值和不存在对应的值。

    String wechat2 = new String("程序新视界");
    
    
    

    如果存在对应的值,此时会先在堆中创建一个针对wechat2变量的对象引用,然后将这个对象引用指向字符串常量池中已经存在的常量。

    img

    此时直接使用双等号比较wechat和wechat2变量肯定是不相等的,而通过equals方法进行对比字面值则是相等的。

    另外一种情况就是通过new创建时,字符串常量池中并不存在对应的常量。这种情况会现在字符串常量池中创建一个字符串常量,然后再在堆中创建一个字符串,持有常量池中对应字符串的引用。并把堆中对象的地址返回给wechat2。最终效果图依旧如上图。

    在此时,如果不是直接new字符串赋值,而是通过+号操作,情况就有所不同。

    String s1 = "程序";
    String wechat3 = new String(s1 + "新视界");
    
    
    

    上述代码s1会存入常量池,而wechat3的值则由于JVM编译时采用了StringBuilder进行加号的拼接,只会在堆中创建一个String对象,并不会在常量池中存储对应的字符串。

    img

    此时的情况已经涉及到我们面试题中创建字符串的情况了。那么,下面我们就通过intern方法进行池化操作,看看字符串常量池的具体变化。

    还以上面的代码为例,此时wechat、wechat1、wechat2三个变量和wechat3直接用双等号比较肯定是不相等的。下面对wechat3进行intern池化处理。

    String s1 = "程序";
    String wechat3 = new String(s1 + "新视界");
    wechat3 = wechat3.intern();
    
    
    

    此时会发现wechat、wechat1两个变量与wechat3的值相等了。由于wechat和wechat1其实是一个,这里只以wechat和wechat3的比较为例来分析一下这个流程。

    在没有调用intern方法之前内存的状态是下图(忽略掉s1部分)这样的:

    img

    看上图它们的值不相等也就不奇怪了。下面对wechat3进行池化处理,并把池化的结果赋值给wechat3,就是上面的代码。内存结构会发生如下变化:

    img

    此时,再判断对应的两个值,因为引用和字面值全部相同,因此便相等了。具体intern的判断规则我们上面已经知道,如果常量池中存在对应的值,则直接返回引用。

    那还有另外一种情况,就是常量池中不存在对应的值会是如何处理的呢?先看如下代码:

    String s2 = "关注";
    String wechat4 = new String(s2 + "公众号");
    wechat4 = wechat4.intern();
    
    1234
    

    在调用intern之前的操作我们前面已经说过,会在堆中创建一个String对象,而常量池中并不会存储一份,与wechat3的图一样。

    公众号");
    wechat4 = wechat4.intern();

    在调用intern之前的操作我们前面已经说过,会在堆中创建一个String对象,而常量池中并不会存储一份,与wechat3的图一样。
    

    参考文献

    https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.2

    https://blog.csdn.net/m0_60747279/article/details/119540072

    https://blog.csdn.net/weixin_49798750/article/details/120815612

    https://blog.csdn.net/m0_60747279/article/details/119540072

    https://blog.csdn.net/HelenAndLi/article/details/122193451

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值