字符串常量池

在Java体系中,有三种常量池:

  • class字节码中的常量池:存在于硬盘上。主要存放字面量和符号引用。
  • 运行时常量池:方法区的一部分。我们常说的常量池,就是指这一块区域。
  • 字符串常量池:存在于堆区。这个常量池在JVM层面就是一个StringTable,只存储对java.lang.String实例的引用,而不存储String对象的内容。一般我们说一个字符串进入了字符串常量池其实是说在这个StringTable中保存了对它的引用,反之,如果说没有在其中就是说StringTable中没有对它的引用。
  • 运行时常量池
    运行时常量池是方法区的一部分,所以也是全局贡献的,我们知道,jvm在执行某个类的时候,必须经过加载、链接(验证、准备、解析)、初始化,在第一步加载的时候需要完成:
  • 通过一个类的全限定名来获取此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个类对象,代表加载的这个类,这个对象是java.lang.Class,它作为方法区这个类的各种数据访问的入口。

类对象和普通对象是不同的,类对象是在类加载的时候完成的,是jvm创建的并且是单例的,作为这个类和外界交互的入口, 而普通的对象一般是在调用new之后创建。

上面的第二条,将class字节流代表的静态存储结构转化为方法区的运行时数据结构,其中就包含了class文件常量池进入运行时常量池的过程,这里需要强调一下不同的类共用一个运行时常量池,同时在进入运行时常量池的过程中,多个class文件中常量池相同的字符串,多个class文件中常量池中相同的字符串只会存在一份在运行时常量池,这也是一种优化。

运行时常量池的作用是存储java class文件常量池中的符号信息,运行时常量池中保存着一些class文件中描述的符号引用,同时在类的解析阶段还会将这些符号引用翻译出直接引用(直接指向实例对象的指针,内存地址),翻译出来的直接引用也是存储在运行时常量池中。

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

jdk1.7之前运行时常量池逻辑包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区的实现为永久代

jdk1.7字符串常量池从方法区拿到了堆中,治理没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代。

jdk1.8 hotspot移除了永久代用原空间(Metaspace)取而代之,这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间。

全局字符串常量池

Java中创建字符串对象的两种方式

一般有如下两种:

String s0 = "hellow";
String s1 = new String("hellow");

第一种方式声明的字面量hellow是在编译期就已经确定的,它会直接进入class文件常量池中;当运行期间在全局字符串常量池中会保存它的一个引用.实际上最终还是要在堆上创建一个”hellow”对象,这个后面会讲。

第二种方式方式使用了new String(),也就是调用了String类的构造函数,我们知道new指令是创建一个类的实例对象并完成加载初始化的,因此这个字符串对象是在运行期才能确定的,创建的字符串对象是在堆内存上。

因此此时调用System.out.println(s0 == s1);返回的肯定是flase,因此==符号比较的是两边元素的地址,s1和s0都存在于堆上,但是地址肯定不相同。

String s1 = “Hello”,到底有没有在堆中创建对象?

idea java 常量字符串过长 java中字符串常量池在哪里_idea java 常量字符串过长

idea java 常量字符串过长 java中字符串常量池在哪里_java_02

Interned String 就是全局共享的“字符串常量池”,和运行时常量池不是一个概念。但我们在代码中声明String s1 = “hello” 这句代码后,在类加载中,类的class文件的信息会被解析到内存的方法区里。

class文件里常量池的大部分数据会被加载到运行时常量池,包括String的字面量;但同时“hello”字符串的一个引用会被存到同样在“非堆”区域的“字符串常量池”中,而“hello”本体还是和所有对象一样,创建在java堆中。

当主线程开始创建s1时,虚拟机会先去字符串池中找是否还用equals(“hello”)的string,如果相等就把字符串的“hello”的引用复制给s1;如果找不到相等的字符串,就会在堆中新建了一个对象,同时把引用驻留在字符串池,再把引用赋给str。

当用字面量赋值的方法创建字符串时,无论创建多少次,只要字符串的值相同,它们所指向的都是堆中的同一个对象

字符串常量池的本质

字符串常量池时jvm所维护的一个字符串实例的引用表,在hotspot vm中,它是一个叫做stringtable的全局表,在字符串常量池中维护的时字符串实例的引用,底层c++实现就是一个hashtable。这些被维护的引用所指的字符串实例,被称作“被驻留的字符串”或“Interned string”或通常所说的“进入了字符串常量池的字符串”

String"字面量" 是何时进入字符串常量池的?
在执行ldc指令时,该指令表示int、float或String型常量从常量池推送至栈顶。

String.intern()的用法

String.intern()官方给的定义:

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

实际上,就是去拿String的内容去Stringtable里查表,如果存在,则返回引用,不存在,就把该对象的"引用"存在Stringtable表里。

String str1 = new StringBuilder("h").append("dc").toString();
        String str2 = new StringBuilder("j").append("ava").toString();
        System.out.println(str1.intern() == str1);		//true
        System.out.println(str2.intern() == str2);		//false

以上结果在1.7以上,“hdc”这个字符串实例只存在一份,存在于java堆中,当第一行语句执行完之后。已经在堆中创建一个字符串对象,并且在全局字符串常量池中保留了这个字符串的引用,那么str1.intern() 直接返回这个引用,满足str1.intern()===str1;对于引用str2,jvm中已经存在“java”这个字符串,因此第二行语句执行完以后会重新创建一个新的“java”字符串对象,而intern()会返回首次遇到的实例引用,因此它返回的是系统中的那个“java”字符串对象引用(首次),因此会返回false

JDK7 常量池被移动到 Native Heap(Java Heap,HotSpot VM中不区分native堆和Java堆),所以即使设置了持久代大小,也不会对常量池产生影响;不断while循环在当前的代码中,所有int的字符串相加还不至于撑满 Heap 区,所以不会出现异常。

java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。

/在值小于127时可以使用常量池
        Integer i1=127;
        Integer i2=127;
        System.out.println(i1==i2);//输出true

        //值大于127时,不会从常量池中取对象
        Integer i3=128;
        Integer i4=128;
        System.out.println(i3==i4);//输出false