String 在虚拟机中的实现
标签:Java基础

String 在虚拟机中的实现

Java 设计者堆String做了大量优化,主要表现在3个方面

  • 不变性
  • 针对常量池的优化
  • 类的final定义

String的主要使用方法有两种:

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

1. 不变性

不变性指String对象一旦生成,则不能再对它进行改变,这个特性泛化成不变模式:即一个对象的状态在对象被创建之后就不再发生变化,不变性可以提高多线程访问的性能,因为对象不可变,因此对于所有线程都是只读的,多线程访问时,即时不加同步也不会产生数据的不一致,故减小了系统开销。

由于不变性,一些看起来修改的操作,实际上都是依靠产生新的字符串实现的,比如 String.subString() String.contact()等方法,它们并没有修改原字符串,而是产生了一个新的字符串。如果要想字符串可变,可以使用StringBufferStringBuilder

2. 针对常量池的优化

针对常量池的优化的优化指的是当两个String对象拥有相同的值时,它们只引用常量池中的同一个拷贝,当一个字符串反复出现时,可以利用这一技术大幅节省空间。

package com.liuyao;

/**
 * Created By liuyao on 2018/5/5 19:58.
 */
public class StringTest {
    public static void main(String[] args) {
        String str1=new String("abc");
        String str2=new String("abc");
        
        System.out.println(str1==str2);
        System.out.println(str1==str2.intern());
        System.out.println("abc"==str2.intern());
        System.out.println(str1.intern()==str2.intern());
    }
}

JDK8 执行结果为:

扩展阅读:深入解析String#intern

3. 类的final定义

作为final类的String对象在系统中不可能有任何子类,这是对系统安全性的保护,在JDK1.5之前,使用final定义有助于帮助虚拟机寻找机会,内联所有的final方法,从而提高效率,但这种方法在JDK1.5之后,效果不明显。

4. 有关String的内存泄露

在JDK1.6中,String对象有三部分组成(value char数组,offset 偏移,count长度),字符串的的实际内容由前面的三个部分组成,有可能value数组的长度为1000,但count的长度却为1,那剩下的999个就是内存泄露,它们不会被使用,不会释放,却长期占用内存,直到字符串本身被释放。在JDK1.6中,subString 新的子串使用构造函数新的子串和原来的字符串使用的是相同的value数组,如果原来的字符串没有回收的话,这样可以解释空间,但如果原来的字符串被回收,value的其他部分就会造成空间浪费。

在JDK1.7中,String的subString方法:

public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

调用的构造函数:

    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

看18行处,可见不再复用原来的,而是复制一份。

5. 有关String常量池的位置

在虚拟机中有一块常量池的区间专门用来存放字符串常量,在JDK1.6之前,这块区间属于永久区的一部分,在JDK1.7后,这块区间被移到了堆中进行管理

package com.liuyao;

import java.util.ArrayList;

/**
 * Created By liuyao on 2018/5/5 20:47.
 */
public class StringInternOOM {
    public static void main(String[] args) {
        ArrayList<String> list=new ArrayList<>();
        int i=0;
        while (true){
            list.add(String.valueOf(i++).intern());
        }
    }
}

public native String intern();方法获得常量池中的字符串的引用,如果常量池中没有改常量字符串,该方法会将字符串加入常量池

上面程序带参数 -Xmx5m -XX:MaxPermSize=5m运行

可见常量池位置发生了变化。

注意 intern()不能保证每时每刻返回的都会是一样的,因为存在这种可能:在一次intern方法调用后,该字符串在某一个时刻被回收,再进行一个intern()调用,那么字面量相同的字符串重新加入常量池,但是引用位置已经不同了。

  • 5 min read

CONTRIBUTORS


  • 5 min read