String类型与运行时常量池

Posted by RussXia on February 8, 2018

String类型与运行时常量池

String不属于8种基本数据类型,它是一个对象,但是它却有其他对象所没有的一些特性。

对于同属于String类型的对象,下面的代码的运行结果很显而易见,而造成这个现象的原因就是因为运行时常量池的存在。

(下文所有代码均在Hotspot虚拟机上编译执行,文中所出现的jvm,指的也是Hotspot)

String str1 = "Hello" ;
String str2 = new String("Hello") ;
System.out.println(str1.equals(str2)); // true;
System.out.println(str1 == str2); //false。

Java中的常量池(静态常量池与运行时常量池)

静态常量池: 在生成编译文件的时候(.class文件),magic number,类/接口的信息,常量等信息已被确定下来了。

*.class文件中的常量池便是我们平常说的静态常量池,在虚拟机加载class文件时,会从常量池中获取

其符号引用,在类的创建时或运行时解析,翻译到具体的内存地址之中。

(具体可以参考深入理解Java虚拟机的第六章部分)

运行时常量池: 运行时常量池位于堆内存中,是线程共享的。

  • 在JVM运行时,class文件中的常量池会被载入到内存中,并保存在方法区中。
  • 在JVM运行时,通过代码生成的常量也会被放入运行时常量池中(intern方法)

字符串常量池 :(也叫全局字符串池、string pool、string literal pool)。 字符串常量池在每个VM中只有一份,他在内存中的位置如图,红色箭头所指向的区域 Interned Strings

HotSpot中方法区的变迁

  • 在jdk1.6及之前,HotSpot使用Perm Gen来实现方法区,主要是出于分代gc的角度考虑。
  • 在jdk1.7时,运行时常量池被迁移到java堆上。
  • 在jdk1.8时,移除了Perm Gen,用Metaspace代替。

移除PermGen的主要原因包括以下几个:

  • 字符串存储在PermGen,容易出现性能问题和内存溢出
  • 类及方法信息难以确定其大小,因此永久代内存大小的指定比较困难,太小容易造成永久代溢出,太大容易造成老年代溢出。
  • 永久代的GC效率低下
  • 为Hotspot何JRockit合二为一作铺垫(JRockit没有永久代)

public native String intern() 的使用

public String intern()

返回字符串对象的规范化表示形式。 一个初始为空的字符串池,它由类 String 私有地维护。当调用 intern 方法时,如果池已经包含一个等于此String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。 否则,将此 String 对象添加到池中,并返回此 String 对象的引用。
它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。 所有字面值字符串和字符串赋值常量表达式都使用 intern 方法进行操作。字符串字面值在 Java Language Specification 的 §3.10.5 定义。 — 返回:一个字符串,内容与此字符串相同,但一定取自具有唯一字符串的池。

上面是这个方法的官方说明文档。通俗地讲,就是先检查当前字符串常量池中是否存在(equals方法确定),

如果存在,直接返回池中的字符串,如果不存在,将当前String对象添加到字符串常量池中,并返回当前对象的引用。

Java代码:

    package com.xzy.jvmlearning;

    /**
    * @author RuzzZZ
    * @since 17/11/2017 4:27 PM
    */
    public class RuntimeConstantPoolAdress {

      public static void main(String[] args) {
          StringBuilder sb = new StringBuilder();
          String str1 = sb.append("ja").append("va").toString();
          String str2 = sb.append("Hello").append("World").toString();
          System.out.println(str1.intern() == str1);
          System.out.println(str2.intern() == str2);
      }
    }

输出结果(在jdk1.8.0_111环境下运行结果):

false
true

因为字符串java是被提前加载到字符串常量池的,所以str1.intern()返回的是字符串常量池的引用,所以str1.intern() == str1false

字符串HelloWorld不在常量池中。所以str2.intern()会将HelloWorld加入到字符串常量池中,并返回str2的引用地址。所以str2.intern() == str2true

常见误区

字符串常量池是什么

OpneJDK中,使用的是c++实现的StringTable。其内部和HashMap类似,只是不能扩容,默认大小为1009。

如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。

JDK1.6中,这个长度是固定的; JDK1.7中可以通过: -XX:StringTableSize=99991参数指定这个StringTable的大小。

字符串常量池里放的是什么

  • 在JDK6.0及之前版本中,String Pool里放的都是字符串常量
  • 在JDK7.0中,由于String#intern()发生了改变,String Pool中也可以存放放于堆内的字符串对象的引用。

因此,在JDK版本>=1.7以上时,以下两段代码输出不一致。就是因为String Pool可以存放堆中字符串对象的引用。具体的分析可以参见美团技术团队技术分享

String s1 = new String("1");
s1.intern();
String s2 = "1";
System.out.println(s1 == s2);   //false

String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);   //true
String s1 = new String("1");
s1.intern();
String s2 = "1";
System.out.println(s1 == s2);   //false

String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);   //false