写点什么

再议 String- 字符串常量池与 String

  • 2022 年 5 月 15 日
  • 本文字数:2866 字

    阅读完需:约 9 分钟

0. Background




在 JAVA 语言中有 8 中基本类型和一种比较特殊的类型 String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个 JAVA 系统级别提供的缓存。


8 种基本类型的常量池都是系统协调的,String 类型的常量池比较特殊。它的主要使用方法有两种:


  • 直接使用双引号声明出来的 String 对象会直接存储在常量池中。

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


1. 常量池



1.1 常量池是什么?


JVM 常量池主要分为 Class 文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池

1.1.0 方法区

方法区的作用是存储 Java 类的结构信息,当创建对象后,对象的类型信息存储在方法区中,实例数据存放在堆中。类型信息是定义在 Java 代码中的常量、静态变量、以及类中声明的各种方法,方法字段等;实例数据则是在 Java 中创建的对象实例以及他们的值。


该区域进行内存回收的主要目的是对常量池的回收和对内存数据的卸载;一般说这个区域的内存回收率比起 Java 堆低得多。

1.1.1 Class 文件常量池

class 文件是一组以字节为单位的二进制数据流,在 Java 代码的编译期间,我们编写的 Java 文件就被编译为.class 文件格式的二进制数据存放在磁盘中,其中就包括 class 文件常量池。


class 文件常量池主要存放两大常量:字面量和符号引用


字面量:字面量接近 java 语言层面的常量概念


  • 文本字符串,也就是我们经 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 常申明的:public String s = "abc";中的"abc"

  • 用 final 修饰的成员变量,包括静态变量、实例变量和局部变量:public final static int f = 0x101;,final int temp = 3;

  • 而对于基本类型数据(甚至是方法中的局部变量),如 int value = 1 常量池中只保留了他的的字段描述符 int 和字段的名称 value,他们的字面量不会存在于常量池。


符号引用:符号引用主要设涉及编译原理方面的概念


  • 类和接口的全限定名,也就是 java/lang/String;这样,将类名中原来的".“替换为”/"得到的,主要用于在运行时解析得到类的直接引用

  • 字段的名称和描述符,字段也就是类或者接口中声明的变量,包括类级别变量和实例级的变量

  • 方法中的名称和描述符,也即参数类型+返回值

1.1.2 运行时常量池

当 Java 文件被编译成 class 文件之后,会生成上面的 class 文件常量池,JVM 在执行某个类的时候,必须经过加载、链接(验证、准备、解析)、初始化的步鄹,运行时常量池则是在 JVM 将类加载到内存后,就会将 class 常量池中的内容存放到运行时常量池中,也就是 class 常量池被加载到内存之后的版本,是方法区的一部分。


在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就 StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。


运行时常量池相对于 class 常量池一大特征就是具有动态性,Java 规范并不要求常量只能在运行时才产生,也就是说运行时常量池的内容并不全部来自 class 常量池,在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是 String.intern()。

1.1.3 字符串常量池

在 JDK6.0 及之前版本,字符串常量池存放在方法区中,在 JDK7.0 版本以后,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。在 HotSpot VM 里实现的 string pool 功能的是一个 StringTable 类,它是一个 Hash 表,默认值大小长度是 1009;这个 StringTable 在每个 HotSpot VM 的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了 StringTable 上。


在 JDK6.0 中,StringTable 的长度是固定的,长度就是 1009,因此如果放入 String Pool 中的 String 非常多,就会造成 hash 冲突,导致链表过长,当调用 String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;在 JDK7.0 中,StringTable 的长度可以通过参数指定。


字符串常量池设计思想:


  • 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能

  • JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化

  • 为字符串开辟一个字符串常量池,类似于缓存区

  • 创建字符串常量时,首先查看字符串常量池是否存在该字符串

  • 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中

  • 实现的基础

  • 实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享

  • 运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收


2. String.intern()与字符串常量池




/**?


*?Returns?a?canonical?representation?for?the?string?object.?


*?<p>?


*?A?pool?of?strings,?initially?empty,?is?maintained?privately?by?the?


*?class?<code>String</code>.?


*?<p>?


*?When?the?intern?method?is?invoked,?if?the?pool?already?contains?a?


*?string?equal?to?this?<code>String</code>?object?as?determined?by?


*?the?{@link?#equals(Object)}?method,?then?the?string?from?the?pool?is?


*?returned.?Otherwise,?this?<code>String</code>?object?is?added?to?the?


*?pool?and?a?reference?to?this?<code>String</code>?object?is?returned.?


*?<p>?


*?It?follows?that?for?any?two?strings?<code>s</code>?and?<code>t</code>,?


*?<code>s.intern() == t.intern()</code>?is?<code>true</code>?


*?if?and?only?if?<code>s.equals(t)</code>?is?<code>true</code>.?


*?<p>?


*?All?literal?strings?and?string-valued?constant?expressions?are?


*?interned.?String?literals?are?defined?in?p?3.10.5?of?the?


*?<cite>The?Java™?Language?Specification</cite>.?


*?


*?@return??a?string?that?has?the?same?contents?as?this?string,?but?is?


*??????????guaranteed?to?be?from?a?pool?of?unique?strings.?


*/??


public?native?String?intern();????


字符串常量池的位置也是随着 jdk 版本的不同而位置不同。在 jdk6 中,常量池的位置在永久代(方法区)中,此时常量池中存储的是对象。在 jdk7 中,常量池的位置在堆中,此时,常量池存储的就是引用了。


在 jdk8 中,永久代(方法区)被元空间取代了。这里就引出了一个很常见很经典的问题,看下面这段代码。


@Test


public?void?test(){


String?s?=?new?String("2");


s.intern();


String?s2?=?"2";


System.out.println(s?==?s2);


String?s3?=?new?String("3")?+?new?String("3");


s3.intern();


String?s4?=?"33";


System.out.println(s3?==?s4);


}


//jdk6


//false


//false


//jdk7


//false


//true


这段代码在 jdk6 中输出是 false false,但是在 jdk7 中输出的是 false true。我们通过图来一行行解释。

JDK1.6


  • String s = new String("2");创建了两个对象,一个在堆中的 StringObject 对象,一个是在常量池中的“2”对象。

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
再议String-字符串常量池与String_程序员_爱好编程进阶_InfoQ写作社区