字符串常量池 运行时常量池_常量池中的字符串是对象吗

字符串常量池 运行时常量池_常量池中的字符串是对象吗详细介绍了字符串常量池以及其产生的相关问题,并对String类相关操作和String类中的intern()方法进行了详细解析。

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

字符串常量池 StringTable

概述

  • 常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String s = “java”这种申明方式;当然也可扩充,执行器执行器产生的常量也会放入常量池,故认为常量池是JVM的一块特殊的内存空间。
  • 因为在Java中创建一个对象是一个很重的活,并且需要不断进行垃圾回收,所以像是String Table这样的缓冲池可以有效缓解这些问题。(很多包装类都有缓冲空间,Integer 默认缓存 -128 ~ 127 区间的值,Long 和 Short 也是缓存了这个区间的值,Byte 只能表示 -127 ~ 128 范围的值,全部缓存了,Character 缓存了 0 ~ 127 的值。Float 和 Double 没有缓存的意义,因为这两种类型表示小数,可能性倍增,所以不适合应用缓存池的概念)
  • 字符串常量池String Table的数据结构是一个哈希表,但是这个哈希表与Java集合中的哈希表不用,无法进行扩容操作,并且字符串种类复杂,很可能发生哈希碰撞现象,一旦字符串在哈希表中形成了链表等数据结构,就会使字符串常量池的性能下降,所以字符串常量池中需要加入垃圾回收机制。

字符串常量池在JVM中的位置变化:

  • jdk6及之前在方法区中,但是在jdk6中已经有向对堆中迁移的趋势。
  • jdk7是JVM内存区域发生了变化,将方法区放到了直接内存中,而字符串常量池放到了堆空间当中。

关于String以及StringBuffer、StringBuilder的相关信息可以参考博主的另一篇文章:

Java String、StringBuilder、StringBuffer类解析

String的内存分配

String的特点:

  • String实现了Serializable接口,表示String是可序列化的
  • 实现了Comparable接口
  • 实现了CharSequence(字符序列接口)
  • String类中用于存储字符的数组value[]是final类型的
  • String代表不可变的字符序列,具有不可变性。
  • 被final修饰,无法被继承

String与字符串常量池

  1. 在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。

  2. 常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。

    • 直接使用双引号声明出来的String对象会直接存储在常量池中。比如:String info="atguigu.com";
    • 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。这个后面重点谈
  3. Java 6及以前,字符串常量池存放在永久代

  4. Java 7中 Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内

  • 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
  • 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String.intern()。
  1. Java8元空间,字符串常量在堆中。

在这里插入图片描述

在这里插入图片描述

String基本操作中的细节

示例一

public class StringTest { 
   
    public static void main(String[] args) { 
   
            System.out.println();//2152
            System.out.println("1");//2153
            System.out.println("2");
            System.out.println("3");
            System.out.println("4");
            System.out.println("5");
            System.out.println("6");
            System.out.println("7");
            System.out.println("8");
            System.out.println("9");
            System.out.println("10");//2163
            //如下的字符串"1" 到 "10"不会再次加载
            System.out.println("1");//2163
            System.out.println("2");//2163
            System.out.println("3");
            System.out.println("4");
            System.out.println("5");
            System.out.println("6");
            System.out.println("7");
            System.out.println("8");
            System.out.println("9");
            System.out.println("10");//2163
	}

在单元测试方法test中我们打上断点调试:
在这里插入图片描述

调试区域中勾选该选项可以查看String类在内存中的详情:

在这里插入图片描述

进行调试我们会发现,在输出10个String后,再次输出相同的字符串,而字符串常量池中没有在创建新的字符串常量对象:

在这里插入图片描述

示例二

//官方示例代码
class Memory { 
   
    public static void main(String[] args) { 
   //line 1
        int i = 1;//line 2
        Object obj = new Object();//line 3
        Memory mem = new Memory();//line 4
        mem.foo(obj);//line 5
    }//line 9

    private void foo(Object param) { 
   //line 6
        String str = param.toString();//line 7
        System.out.println(str);
    }//line 8
}

在这里插入图片描述

字符串拼接中的细节

  • 常量与常量的拼接结果在常量池,原理是编译期优化
  • 常量池中不会存在相同内容的变量
  • 拼接前后,只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
  • 如果拼接的结果调用intern()方法,根据该字符串是否在常量池中存在,分为:
    • 如果存在,则返回字符串在常量池中的地址
    • 如果字符串常量池中不存在该字符串,则在常量池中创建一份,并返回此对象的地址

示例1: 常量之间的拼接会进行编译期优化

@Test
public void test() { 
   
    String s1 = "a" + "b" + "c";
    String s2 = "abc";
    System.out.println(s1 == s2); // true
}

在这里插入图片描述

@Test
public void test1() { 
   
    String s = "aabb";
    final String s1 = "aa";
    final String s2 = "bb";
    String s3 = s1 + s2;
    System.out.println(s == s3); // true
}

在这里插入图片描述

示例2:变量与常量、变量与变量拼接

@Test
public void test2() { 
   
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";

    String s4 = s1 + s2;

    System.out.println(s3 == s4); // false
}

此时的结果为false,这是因为在拼接过程中实现拼接功能的实际是StringBuilder对象,先创建出一个StringBuilder对象,然后调用StringBuilder中的append方法,最后调用toString方法将其转化成一个String类型的对象。所以最后s4的地址是一个String类的对象,而s3是字符串常量池当中的引用,最终结果为false。

在这里插入图片描述

intern() 方法

说明

intern()是一种手动将字符串加入常量池中的方法,其优点是执行速度非常快,直接使用==进行比较要比使用equals()方法快很多;内存占用少。但是intern()方法每次操作都需要与常量池中的数据进行比较,查看常量池中是否存在等值数据,所以其主要适用于有限值,并且这些有限值会被重复利用的场景,这样可以减少内存消耗,同时在进行比较操作时减少时耗,提高程序性能。

  • String中的intern()方法是一个native方法

    public native String intern();
    
  • 字符串常量池池最初是空的,由String类私有地维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串内容相等的字符串,则返回池中的字符串地址。否则,该字符串对象将被添加到池中,并返回对该字符串对象的地址。

  • 如果不是用双引号声明的String对象,可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

new Stirng()的细节说明

下面的代码中一共创建了几个对象呢?

@Test
public void test4() { 
   
    String s = new String("hello");
}

来看一下字节码指令当中的信息:

在这里插入图片描述

先是创建了一个String类型的对象,然后引入了常量池中的”hello”,最后执行了Stirng的构造器。所以一共有两个对象产生。

new Stirng(“xxx”) + new String(“xxx”) 的细节说明

@Test
public void test5() { 
   
    String s = new String("Hello") + new String("World");
}

字节码指令当中的细节:
在这里插入图片描述

实际上先是创建了一个StringBuilder类的对象,然后调用了StringBuilder的构造器,再从常量池中引入”Hello”,创建出一个String类的对象,调用StringBuilder中的append方法将”Hello”加入,之后同样,引入”World”,然后创建一个String类的对象,再次appen方法,最后调用StringBuilder中的toString方法。

为什么打印结果输出false呢?

public void test6() { 
   
    String s = new String("Hello") + new String("World");
    String s2 = "HelloWorld";
    System.out.println(s == s2);// false
}

这是因为StringBuilder中的toString()方法:

实际上调用了String类的构造法新建了一个String,而在这个String中只是将原来的char[]中的内容进行了复制,然后将复制的引用返回。所以toString()返回的是一个String类的对象引用,而不是常量池中的引用,所以最后结果是false。

@Override
public String toString() { 
   
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

// 
public String(char value[], int offset, int count) { 
   
    if (offset < 0) { 
   
        throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) { 
   
        if (count < 0) { 
   
            throw new StringIndexOutOfBoundsException(count);
        }
        if (offset <= value.length) { 
   
            this.value = "".value;
            return;
        }
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) { 
   
        throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

关于intern() 方法的面试题

打印结果是什么呢?为什么是这样的结果呢?

public class StringTest { 
   
    public static void main(String[] args) { 
   
        // 问题一:
        String s = new String("1");
        String s1 = s.intern();// 调用此方法之前,字符串常量池中已经存在了"1",所以返回"1"在常量池当中的引用
        String s2 = "1";
        System.out.println(s == s2);// jdk6:false jdk7/8:false
        System.out.println(s1 == s2); // true

        // 问题二:
        String s3 = new String("1") + new String("1");
        s3.intern();

        String s4 = "11";// s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
        System.out.println(s3 == s4);// jdk6:false jdk7/8:true
    }
}

问题一在注释中以及说明,所以重点来看问题二。

首先要明白实际在内存中的细节,才能知道为什么在jdk6中是false,而jdk6之后是true

先来看jdk6中的分析:

  1. 两个new String()的相加的操作实际上是创建了一个StringBuilder对象进行append操作,最后调用toStirng方法返回一个String类型对象的引用,将其赋给了s3。
  2. 在调用了intern方法后将”11″加入到常量池中,再此之前常量池是没有”11″的,该方法返回的结果是常量池中的引用
  3. 而s4直接就是字符串常量池中的引用
  4. 最后进行比较,s3是String类型对象引用,s4是常量池中的直接引用,所以结果是false。

在这里插入图片描述

再来看jdk7/8中的分析:

  1. 同样两个new String()的相加的操作实际上是创建了一个StringBuilder对象进行append操作,最后调用toStirng方法返回一个String类型对象的引用,将其赋给了s3。
  2. 但是调用intern方法时会对其进行优化,发现在堆区域中已经有了”11″这个内容,于是就堆区中的String类型对象的引用在方法区中保存。
  3. 因为对字符串常量池进行了优化,所以 s3的值也是在堆中的String类型对象的引用值。
  4. 最后两者地址值相同,结果为true

在这里插入图片描述

拓展:

public class StringTest { 
   
    public static void main(String[] args) { 
   
        //执行完下一行代码以后,字符串常量池中,是否存在"11"呢?
        String s3 = new String("1") + new String("1");//new String("11")
        //在字符串常量池中生成对象"11",代码顺序换一下,实打实的在字符串常量池里有一个"11"对象
        String s4 = "11";
        String s5 = s3.intern();

        // s3 是堆中的 "ab" ,s4 是字符串常量池中的 "ab"
        System.out.println(s3 == s4);//false

        // s5 是从字符串常量池中取回来的引用,当然和 s4 相等
        System.out.println(s5 == s4);//true
    }
}

StringTable中的垃圾回收

代码部分:

/** * String的垃圾回收测试: * 加入参数: * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails */
public class StringGCTest { 
   
    public static void main(String[] args) { 
   
        for (int j = 0; j < 100000; j++) { 
   
            String.valueOf(j).intern();
        }
    }
}

结果输出:
在这里插入图片描述

G1 中的 String 去重操作

openjdk文档:http://openjdk.java.net/jeps/192

官方文档中内容节选:

许多大型 Java 应用程序目前都存在内存瓶颈。测量表明,在这些类型的应用程序中,大约 25% 的 Java 堆实时数据集被String对象消耗。此外,这些对象中大约有一半String是重复的,其中重复的意思 string1.equals(string2)是正确的。在堆上拥有重复String的对象本质上只是浪费内存。本项目将在 G1 垃圾收集器中实现自动连续String重复数据删除,避免内存浪费,减少内存占用。

对大量 Java 应用程序(大小)进行的测量显示如下:

  • 对象占用的活动堆数据集的平均百分比String= 25%
  • 重复对象占用的活动堆数据集的平均百分比String= 13.5%
  • 平均String长度 = 45 个字符

鉴于我们只对字符数组进行重复数据删除,我们仍将承担String对象(对象头、字段和填充)的开销。此开销取决于平台/配置,在 24 和 32 字节之间变化。但是,考虑到平均String长度为 45 个字符(90 个字节 + 数组标头),仍然有很大的优势。

考虑到上述情况,实际预期收益最终会 减少10% 左右的堆。请注意,此数字是根据广泛的应用计算得出的平均值。特定应用程序的堆减少量可能上下变化很大。

实现:

  1. 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。
  2. 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
  3. 使用一个Hashtable来记录所有的被String对象使用的不重复的char数组。当去重的时候,会查这个Hashtable,来看堆上是否已经存在一个一模一样的char数组。
  4. 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
  5. 如果查找失败,char数组会被插入到Hashtable,这样以后的时候就可以共享这个数组了。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/164782.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • GitHub 近两万 Star,无需编码,可一键生成前后端代码,这个开源项目有点强!

    点击上方“全栈程序员社区”,星标公众号 重磅干货,第一时间送达 github地址:https://github.com/zhangdaiscott/jeecg-boot 项目介绍:…

  • Python代码实现Excel转JSON

    Python代码实现Excel转JSON题记项目需求需要用到Excel转JSON,第一时间想到的就是尘封了将近一年的python,一直在JavaJava,python早忘光了,想立刻开始动手却又不敢,最后确认,用python来完成操作Excel有得天独厚的优势,只能硬着头皮上了。短短的代码,做了将近四个小时,中间复习了一下字典和列表,同时也因为其中遇到了一些奇奇怪怪的问题,凌晨一点多躺下,一身轻松。主要技术python3.8.6+字典/列表的运用+对Excel操作的库pandas其中python对Excel操作的库其实有很多,像我

  • springmvc的执行流程详解[通俗易懂]

    1.什么是MVCMVC是ModelViewController的缩写,它是一个设计模式 2.springmvc执行流程详细介绍 第一步:发起请求到前端控制器(DispatcherServlet)第二步:前端控制器请求HandlerMapping查找Handler        可以根据xml配置、注解进行查找第三步:处理器映射器Handle

  • 图片批量重命名编号不要括号c语言_文件批量重命名001开始

    图片批量重命名编号不要括号c语言_文件批量重命名001开始很多人会采用传统的方法来实现,不过得到的文件名称是这样的:文件名+(编号),这样批量重命名后的文件名有括号,很多人不喜欢,所以网上很多人在网上搜索图片批量重命名不要括号的方法。如果你采用传统的方法对图片进行批量重命名操作,那么得到的图片名称中肯定会包含括号,很多小伙伴嫌这些括号很难看,不利于图片的后续查看和使用。所以今天小编就来教大家如何实现吧,我们需要借助一个批量重命名工具来帮助我们,有了这个方法之后我们就无需手动一个一个的进行修改了,批量重命名能帮助我们节省很多的时间。步骤3,进行重命名设置。…

  • ActiveMQ 从零开始 学习日志(一)

    ActiveMQ 从零开始 学习日志(一)ActiveMQ 从零开始 学习日志(一)

  • stm32的unique ID全球唯一码[通俗易懂]

    stm32的unique ID全球唯一码[通俗易懂]我经常把STM32的全球唯一码作为网卡的MAC地址,但有一天我发现我发现,我的2个板子的MAC地址一样,造成只能有一个ping通。我查看这2个板子的单片机的UNIQUEID,发现非常接近。uniqueid只有前4个字节不一样,而我用的MAC地址是uniqueid的后6个字节,这就造成生成的MAC地址一模一样,可能是这2个片子是同一批买的,同一批生产的,ID号…

    2022年10月29日

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号