`
qicen
  • 浏览: 46664 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
文章分类
社区版块
存档分类
最新评论

你真的了解java.lang.String吗?

阅读更多
你真的了解java.lang.String吗?

一直觉得自己的Java基础技术还算可以,自从看了一些大牛的博客后,发现自己对Java的理解还只是皮毛。之前看过JVM的一些知识以及java.lang.String的源码,以为自己对它很了解,但真的是这样吗?在这之前我对字符串的认识一直停留在很基础的认识上:
    1、String对象是不可变的,一旦创建内容将不可能被修改。
    2、JVM中存在一个字符串常量池,String.intern()方法可以把运行时产生的字符串放入常量池中。
    3、对于以下代码有:s1和s2指向同一个对象,s3和s1指向不同的对象但值相等。
        String s1 = "Hello World";
        String s2 = "Hello World";
        String s3 = new String("Hello World");


Java字符串真的是不可变的吗?
今天在stackoverflow上看到这样的一个问题,Java字符串真的是不可变的吗?并贴出了一段修改字符串的代码。
   
    String s1 = "Hello World";  
    String s2 = "Hello World";  
    String s3 = s1.substring(6);  
    System.out.println(s1); // Hello World  
    System.out.println(s2); // Hello World  
    System.out.println(s3); // World  

    Field field = String.class.getDeclaredField("value");  
    field.setAccessible(true);  
    char[] value = (char[])field.get(s1);  
    value[6] = 'J';  
    value[7] = 'a';  
    value[8] = 'v';  
    value[9] = 'a';  
    value[10] = '!';  

    System.out.println(s1); // Hello Java!  
    System.out.println(s2); // Hello Java!  
    System.out.println(s3); // World

看到代码后,我恍然大悟。虽然看过String的源码,知道String的内部是一个final的char数组,也知道final数组的值可以修改,可以通过反射机制去修改实例的一些变量值,但从来没有想到过字符串的值可以通过这种方式修改。

接着看了下面的一些回答,了解到最后一行代码System.out.println(s3); // World在java 7u6版本之前会输出Java!对比了一下java 6u45和java 7u45的String的源码,发现在java 6u45版本中substring产生的新字符串和原字符串共用的一个char数组,只是构造字符串时修改了offset和count的值。而在java 7u45的版本的源码中并不存在offset和count这两个常量,在substring的时候是从原char数组中拷贝出来了this.value = Arrays.copyOfRange(value, offset, offset+count)。这样的话在java 7u6之前,调用字符串的substring的时候,产生的字符串和原字符串实际是共用了同一个char数组,可以节约内存空间。

下图是java 7u6之前版本中s1、s2、s3字符串在JVM的存储结构。所以通过反射直接修改char数组的值是可以修改字符串的。



对问题的回答:String对象是不可变的,但这仅意味着你无法通过调用它的公有方法来改变它的值。这个问题也揭示了为什么反射技术在某些场景下非常实用,但在大多数情况下,你应该避免使用它。

那么在java 7u6版本为什么要做这样的修改呢?
虽然共用一个char数组可以节约内存空间,但这样会带来内存泄漏的问题。假如你有一个很长的母字符串,使用substring想从其中得到一个有用的子字符串,然后你只使用这个子字符串。这是你的母字符串就会成为垃圾被JVM回收掉,但是由于母字符串的char数组一直被字符串使用无法回收,整个母字符串的内容一致存在内存中,导致内存泄漏。

关于字符串常量池
在这之前,我一直在想Java的字符串常量池是存在什么地方,Heap?PermGen?并且写了一些测试代码证实了字符串常量池是存储在PermGen。但看到大牛的文章后,发现我的认识还是那么的狭小,下面是大牛对字符串常量池和String.intern()的总结:
  • 不要在Java 6及以前的版本使用String.intern()将字符串放入常量池,因为这是的字符串常量池是存储在固定大小的内存区(PermGen)
  • Java 7和8中将字符串常量池存储在堆内存中。
  • Java 7和8中可以通过JVM参数-XX:StringTableSize来控制字符串常量池的大小。
  • -XX:StringTableSize的默认值在Java 7中是1009,在Java 8中大概是25~50K
  • 在多线程情况下可以随意使用Stirng.intern()方法。8个写线程只比1个写线程的负载多了17%,1个写7个读线程只比一个线程的负载多了9%
  • 字符串常量池不是线程隔离的
  • 虽然Java 7+对String.intern()做了很多优化,但是它还是花费CPU资源。在简单的例子中没有调用String.intern()方法的程序比调用了的程序要快3.5倍。所以不需要对所有的字符串调用String.intern()方法,只需要对经常被使用的字符串(例如:省、市等)调用String.intern()方法加入常量池。


参考文献:
http://java-performance.info/changes-to-string-java-1-7-0_06/
http://stackoverflow.com/questions/20945049/is-a-java-string-really-immutable
http://java-performance.info/string-intern-in-java-6-7-8/
http://java-performance.info/string-intern-java-6-7-8-multithreaded-access/
http://java-performance.info/string-intern-java-7-8-part-3/
  • 大小: 11.9 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics