美文网首页
【String类】 intern的前因后果

【String类】 intern的前因后果

作者: 静筱 | 来源:发表于2019-01-08 15:05 被阅读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冲突。
      拉链法在两种情况下性能下降非常快
    • 扩容

    • 保存的字符串过多,hash碰撞激烈,每个hash值存储的单链表过常。(查找时需要遍历整个单链表)
      而Hashtable不存在扩容的情况,它的大小是jvm启动时指定的。jdk6上是默认的1009,jdk7上可以通过jvm参数指定。

      总结:
      所以保存同样数量的字符串类型,jdk6上更可能遇到读取效率差的问题。而jdk7上可以通过适量增大字符串常量池大小来避免这个性能问题。
      但总的来说,intern()并不适用于大量保存字符串信息的场景 。
      而更适合可控数量字符串,并且这些字符串很高频的被重用的场景,如编码类型等。

    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

    相关文章

      网友评论

          本文标题:【String类】 intern的前因后果

          本文链接:https://www.haomeiwen.com/subject/jvaclqtx.html