【String类】 intern的前因后果

2019-01-08  本文已影响0人  静筱

引言

String字符串最为最高频的类型,它的操作性能会直接影响整个程序的性能。
Java中八种基本类型的包装类的大部分以及特殊类型String都实现了常量池技术,它们是Byte、Short、Integer、Long、Character、Boolean,另外两种浮点数类型的包装类(Float、Double)则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127时才可使用对象池

常量池会保证:

  1. 一样的值只会在内存中保存一份
  2. 读取常量池的值更快速(主要指jdk6上)
  3. 避免重复创建对象的时间消耗

背景:String常量池

jdk 6中字符串常量池位于内存分区的方法区内, 而jdk 7中字符串常量池从方法区移至堆中。
字符串常量池本质上是一个Hashtable<String>,默认大小为1009。jdk 7 中支持使用-XX:StringTableSize参数来指定字符串常量池对应的HashTable的大小。

字符串放入常量池的时机

编译期:java代码中通过双引号直接声明的字符串(如:String s = "test"), 或者静态编译优化后的常量(如 String s = "test1"+"test2"), 或者JIT优化时产生的常量字符串都会在编译期放入字符串常量池。

运行期:运行期无法将字符串新增至字符串常量池,除非使用String.intern()方法。

intern源码(基于jdk8)

    /**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; 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();

注意

  1. 当常量池没有本字符串时,新增字符串到常量池,并返回常量池中新字符串对象的引用。
  2. 当常量池有本字符串时,直接返回常量池中字符串对象。

为什么要用intern

  1. 避免同一对象在内存中保存多份 ==> 节省内存空间

jdk7上常量池内存空间的变化及intern变化

jdk7在字符串常量池上的主要变化是,将其由方法区移至堆中。
相应的其String.intern方法的行为也有如下变化 :

jdk6的字符串常量池保存的是字符串对象,而jdk7中字符串常量池可以保存对于堆中原有字符串对象的** 引用** 。

实例说明

下面通过两组例子实际检验下对以上知识点的理解, 请分别写下jdk6和jdk7上你的答案,再查看正确答案

实例1

import sun.misc.Unsafe;

import java.lang.reflect.Field;

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

        try {
            String s = new String("1");
            String t = s.intern();
            long addressS = addressOf(s);
            long addressT = addressOf(t);
            String s2 = "1";
            long addressS2 = addressOf(s2);
            System.out.println("s: "+addressS);
            System.out.println("s.intern: "+addressT);
            System.out.println("s2 : "+addressS2);


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

            String s3 = new String("1") + new String("1");
            String t3 = s3.intern();
            String s4 = "11";

            long addressS3 = addressOf(s3);
            long addressT3 = addressOf(t3);
            long addressS4 = addressOf(s4);

            System.out.println("s3: "+addressS3);
            System.out.println("s3.intern: "+addressT3);
            System.out.println("s4: "+addressS4);

            System.out.println(s3 == s4);
            System.out.println(t3 == s4);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
    }

    private static Unsafe unsafe;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static long addressOf(Object o) throws Exception {

        Object[] array = new Object[] { o };

        long baseOffset = unsafe.arrayBaseOffset(Object[].class);
        int addressSize = unsafe.addressSize();
        long objectAddress;
        switch (addressSize) {
            case 4:
                objectAddress = unsafe.getInt(array, baseOffset);
                break;
            case 8:
                objectAddress = unsafe.getLong(array, baseOffset);
                break;
            default:
                throw new Error("unsupported address size: " + addressSize);
        }
        return (objectAddress);
    }
}

写好你的答案了么?来比较一下正确答案吧。

jdk6:
s: 4267994917   // String s = new String("1"); 编译期会向常量池中放入"1",运行期在堆中生成新的对象,引用常量池中的"1". s指向堆中新对象的地址

s.intern: 4284831160 //会检查“1”是否在常量池,如果不在会把“1”放入常量池,注意这里并未改变s的指向。与s = s.intern()操作不同。

s2 : 4284831160 //String s2 = "1"; s2指向常量池中的“1” 

false // s==s2? 不正确,s指向堆中新对象,s2指向常量池中的“1”。

true  // s.intern==s2? 正确,s.intern()不论“1”是否是常量池中己存在,返回的都是“1”在常量池中的对象地址,与s2一致。

s3: 4267995168 // String s3 = new String("1") + new String("1"); 编译期会将“1”放入常量池,静态编译时会把此语句转换为StringBuilder.append操作。 运行时在堆中生成两个临时对象和一个新的字符串对象,但是最终s3指向的是堆中新对象的地址。此时常量池中并没有“11”这个字符串。

s3.intern: 4284831191 //检查“11”是否存在于常量池,不存在则将其放入常量池,此时s3.intern()返回的是常量池中此对象的地址,而s3本身还是指向堆中新对象的地址。

s4: 4284831191 // String s4 = "11"; 指向常量池中“11”对象。与s3.intern()一致。

false // s3==s4? s3指向堆中新对象地址,s4指向常量池中地址。

true  //s3.intern()==s4? s3.intern()返回的是常量池中字符串所在地址,s4也是返回常量池中字符串所在地址,因此一致。
jdk7:
s: 4071646725  //  String s = new String("1"); 编译期会向常量池中放入"1",运行期在堆中生成新的对象,引用常量池中的"1". s指向堆中新对象的地址

s.intern: 4071646728 //会检查“1”是否在常量池,如果不在会把“1”放入常量池,注意这里并未改变s的指向。与s = s.intern()操作不同。

s2 : 4071646728 // String s2 = "1"; s2指向常量池中的“1” 

false //与jdk6相同 

true  //与jdk6相同 

s3: 4071647013 //String s3 = new String("1") + new String("1"); 编译期会将“1”放入常量池,静态编译时会把此语句转换为StringBuilder.append操作。 运行时在堆中生成两个临时对象和一个新的字符串对象,但是最终s3指向的是堆中新对象的地址。此时常量池中并没有“11”这个字符串。

s3.intern: 4071647013 //检查常量池中是否有“11”这个字符串,此时没有,则在常量池中新增对象,但是“11”这个字符串己经在堆中存在了,所以此处常量池保存的是对堆中己有“11”对象的引用。s3.intern()返回的也是这个堆中己有对象的地址。

s4: 4071647013 //String s4 = "11"; 指向常量池中“11”对象。与s3.intern()一致。都是堆中己有"11"对象地址.

true //s3,s3.intern,s4都指向堆中“11”字符串对象的地址。
true

回顾知识点

  1. jdk7中字符串常量池不再只保持字符串对象,如果该字符串在堆中己经存在,则可以在常量池中保存对堆内己有对象的引用。
  2. new出来的字符串的intern方法返回的地址,与直接用双引号声明的字符串所对应的地址,永远是一致的,无论是jdk7还是jdk6. 参见上例s.intern==s2, s3.intern==s4

实例2

本例与上例的主要不同在于把intern操作的顺序后移:

  public static void main(String[] args) {

        try {
            String s = new String("1");
            String s2 = "1";
            String t = s.intern(); 
            long addressS = addressOf(s);
            long addressT = addressOf(t);
            long addressS2 = addressOf(s2);

            System.out.println("s: "+addressS);
            System.out.println("s.intern: "+addressT);
            System.out.println("s2 : "+addressS2);

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

            String s3 = new String("1") + new String("1");
            String s4 = "11";
            String t3 = s3.intern();

            long addressS3 = addressOf(s3);
            long addressT3 = addressOf(t3);
            long addressS4 = addressOf(s4);

            System.out.println("s3: "+addressS3);
            System.out.println("s3.intern: "+addressT3);
            System.out.println("s4: "+addressS4);

            System.out.println(s3 == s4);
            System.out.println(t3 == s4);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
    }

写好你的答案了么?来比较一下正确答案吧。

jdk6:
s: 4071561516
s2 : 4071561519
s.intern: 4071561519
false
true //至此与上例一致,不过多解释
s3: 4071561804
s4: 4071561810
s3.intern: 4071561810
false
true

调整intern调用次序在jdk6上并不影响结果。

jdk7:
s: 4071561516
s2 : 4071561519
s.intern: 4071561519
false
true //至此与上例结果一致,不过多解释
s3: 4071561804 //String s3 = new String("1") + new String("1"); 编译期会将“1”放入常量池,静态编译时会把此语句转换为StringBuilder.append操作。 运行时在堆中生成两个临时对象和一个新的字符串对象,但是最终s3指向的是堆中新对象的地址。此时常量池中并没有“11”这个字符串?不对,因为整个java程序是一起编译的,编译期执行String s4="11"时,会把“11”放到常量池里。

s4: 4071561810 //String s4 = "11"; 检查常量池,没有“11”字符串,新增。此时s4指向常量池中的“11”。

s3.intern: 4071561810 //运行时执行,先去常量池找“11”,找到了,所以返回常量池中“11”的地址。

false // s3==s4? s3指向堆中新对象,s4指向常量池,因此不一致。
true  // s3.intern()==s4 ? s3.intern指向常量池,与s4一致。

intern可能导致的问题
intern的设计本质上是以时间换空间的优化方法,因为在执行intern过程中首先要检查全局HashTable<String>,多线程的情况下涉及锁等资源的消耗。

而intern在使用时要特别注意以下两个问题:

  1. 性能问题:
    常量池底层是靠Hashtable实现,hashtable中不可避免的一个问题就是hash值冲突的问题。而常量池所依赖的Hashtable跟Hashmap一样,是靠每个hash值对应一个单链表的方式(即为拉链法)来解决hash冲突。
    拉链法在两种情况下性能下降非常快
  1. 内存问题:
    jdk 6上使用intern可能遇到OutOfMemoryError: PermGen space 的问题。
    这是因为java 6上字符串常量池在PermGen, 也就是永久代中(注意其实这种方法并不准确,在jvm spec上规定的这个区域叫方法区,只是HotSpot这种特定的虚拟机上选择把GC分代收集扩展到了方法区,或者说使用永久代来实现方法区而且。其他虚拟机如BEA JRockit/IBM J9等并不存在永久代的概念)。永久代主要用于存储静态的类信息和方法信息,静态方法和静态变量,以及final标注的常量信息等。

    PermGen的大小是相对固定的(JVM参数-XX:PermSize和-XX:MaxPermSize),而且这个内存空间虽然也会被GC管控,但是一个类要被回收掉,条件非常苛刻。

    所以在jdk 6上不建议使用intern方法。

参考资源

https://blog.csdn.net/mxd446814583/article/details/79599752

https://www.jianshu.com/p/449672f6aae0

上一篇下一篇

猜你喜欢

热点阅读