对于Java中的String对象,个人觉得每个程序员都会思考过、学习过、研究过这个对象,因为他是面试官们的最爱。如:String s = new String("abc");,创建了几个对象。这种问题反复出现在程序员面试的过程中。下面我们对应着一些代码片段以及其的执行结果,来深入分析Java的String对象。

  首先我们要注意的是String对象的处理在JDK6和JDK7中的处理是不同的。下面通过代码来分析String的三个主要方面知识。

1.常量池的存在

先看看下面代码以及执行结果:



public static void main(String[] args) {
    new StringInternTest().testStringPool();
}

/**
 * 测试常量池的存在
 */
public void testStringPool() {
    String a = new String("abc");
    String b = "abc";
    System.out.println(a == b);
}

执行结果:false



其实这点相信很多Java学习者都已经了解了。Java对于一些基本类型的值维护着一个常量池,如String,int等。同时int还有着自己的缓存。

正是由于字符串常量池的存在才使得上面的代码执行结果为false。

首先分析变量b的处理流程,由于b是通过直接指定字符串值的方式创建的,所以先在栈中创建一个变量b,然后再去常量池中查找有没有"abc"这个字符串存在,不存在则创建一个值为“abc”的对象存放到常量池,然后变量b指向该对象,存在的话则直接指向即可。

下面来分析变量a,a是通过new关键字声明的字符串变量,java中只要遇到new那么jvm就会进行内存分配。所以变量a指向一个地址引用,该引用指向一个堆内存中分配的内存块,该内存块存放着一个String对象,到这里相信大家都是没有任何疑问的。再后面问题就来了。

当分配一块内存存放一个String对象,那么这个对象的值“abc”怎么存放的呢。从网上搜集很多文章都解释说“abc”的值同变量b的创建方式一样,也是在常量池保存着一个值为“abc”的对象,然后将一个变量指向该对象,即b-->堆中的一个对象-->常量池中的一个对象,而a-->常量池中的一个对象。所以a==b为false。个人感觉不是太对,但是也找不出正确的结论,或者可以很有力的证据证明该解释是错误的,不过我可以有一种方法从侧面证明该解释的不合理性,该方法会在下面一段介绍到。在此我希望有同样疑问的人可以一起探讨,或者有人能够给出权威解答。

2.常量池的位置

对于JVM中常量池的位置,JDK6和JDK7是不同的。对于JVM的内存异常错误--OOM其实是分类型的。如java.lang.OutOfMemoryError: Java heap space,java.lang.OutOfMemoryError: PermGen space,还有其他的,但是这里只需要用到这两个即可。下面我们来看一段代码以及在JDK6和7下的执行结果。



代码一:
/**
 * 测试常量池的位置
 */
public void testPoolLocation() {
    List<String> lists = new ArrayList<String>();
    for (int i = 0; i < 1000000; i++) {
        System.out.println(i);
        lists.add(new String("" + i).intern());
    }
}



注:intern()方法下面会介绍



代码二:
/**
 * 测试常量池的位置
 */
public void testPoolLocation2() {
    List<String> lists = new ArrayList<String>();
    for (int i = 0; i < 1000000; i++) {
        System.out.println(i);
        lists.add(new String("" + i));
    }
}



在执行代码一二时我们需要调整下JVM的内存参数:-Xms16M -Xmx16M -XX:PermSize=6M -XX:MaxPermSize=6M

执行堆内存为16M,非堆内存为6M,这样的目的是让程序更快的抛出内存异常。

先在JDK6在执行代码一,看结果:



123094
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)
    at org.vicky.study.StringInternTest.testPoolLocation(StringInternTest.java:33)
    at org.vicky.study.StringInternTest.main(StringInternTest.java:14)



从结果可以看出,程序创建了123094个字符串然后抛出了内存异常,注意下这里的内存异常是PermGen space

再来看看代码二的执行结果:



297868
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:2760)
    at java.util.Arrays.copyOf(Arrays.java:2734)
    at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
    at java.util.ArrayList.add(ArrayList.java:351)
    at org.vicky.study.StringInternTest.testPoolLocation2(StringInternTest.java:44)
    at org.vicky.study.StringInternTest.main(StringInternTest.java:14)



从结果可以看出,代码二创建了297868个字符串然后抛出了内存异常,注意这里的异常是发生在heap space,不同于代码一是发生在PermGen space。而两段代码的不同之处仅仅是代码一在创建字符串时调用了intern方法。

对于intern方法做个简单解释,具体可以参考API。intern主要作用是将判断字符串常量池中是否存在一个等于该字符串对象的对象(通过equals()方法判断),存在则返回常量池中的对象,不存在则将该对象放入常量池。

另外代码一创建仅12W个字符串,而不进行intern()操作的代码二却能够创建30W个字符串,所以对于代码一来说引起对内异常的操作是intern()而且创建过多的字符串对象。而intern是将字符串对象放入常量池,所以最终的内存异常是由于常量池所在的内存块发生了OOM,由此我们可以断定JDK6的常量池是在PermGen中。

下面再来看看JDK7的执行结果:

代码一:



300836
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.lang.StringBuilder.toString(StringBuilder.java:405)
    at org.vicky.study.StringInternTest.testPoolLocation(StringInternTest.java:33)
    at org.vicky.study.StringInternTest.main(StringInternTest.java:14)



代码二:



300742
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.lang.StringBuilder.toString(StringBuilder.java:405)
    at org.vicky.study.StringInternTest.testPoolLocation2(StringInternTest.java:44)
    at org.vicky.study.StringInternTest.main(StringInternTest.java:14)



从两个结果来看,结果是一样的,内存异常都发生在heap space,说明JDK7将字符串常量池搬到了堆内存。

这里还有另外一个结果:代码一和二创建的字符串数量相差很小,说明执行intern()方法和不执行intern()方法每次消耗的内存大致相同,为什么会这样呢,根据intern()方法的API文档,intern会确保常量池存放一个相同的字符串,那据此说明代码一每次都会存在两个对象,所以代码一可创建的字符串数量应该是代码二的一般才对,但是结果并非如此。原因是JDK7的intern方法会将变量直接指向常量池中的对象,而不指向原来的对象,所以GC会将原来的对象回收掉。

上面提到过如何侧面证明new String("abc")不会将"abc"存到常量池。现在我们来分析JDK6下的代码一和代码二的执行结果,如果说new String("abc")会将"abc"存入常量池,那么代码一中的intern()方法岂不是没有意义,因为每次new一个字符串时这个字符串都会自动放入常量池,但是一和二的执行结果完全是不同的,所以我认为new String("abc")不会将"abc"存入常量池。不知道这样理解对还是不对,但是也找不到更好地文章。

3.intern方法的使用

Stirng的intern()方法是将字符串对象显示的放入常量池的方法。

Stirng的intern()方法的使用需要小心,因为常量池是一个hashtable,即在常量池查找字符串时是根据hashcode去查找的,但是JDK的常量池大小是固定的,所以当常量池里的字符串过多就会发生hash碰撞,导致查找效率降低。

最后其实还是没搞清楚new String()的逻辑以及intern()方法的意义。该篇文章可作为一个思路,不可作为一个结论。我也会继续对其进行研究,希望能够深入的了解String常量池。欢迎一起探讨。