java中的String

来源:互联网 发布:linux fuser 编辑:程序博客网 时间:2024/04/30 09:09

转载自:http://blog.csdn.net/xieyuooo/article/details/6859160

 

   谈及String与StringBuffer和StringBuilder时,我们知道StringBuilder性能最高. String是不可变的字符串,而StringBuffer和StringBuilder是可变的字符串对象,而StringBuffer是在进行内容修改时(即char数组修改)会进行线程同步操作,在同步过程中存在征用加锁和访问对象的过程,开销较大,在方法内定义的局部变量中没有必要同步,因为就是当前线程使用,所以StringBuilder为一个非同步的可变字符串对象。

   根据String内部的定义,应该有以下内容:一个char数组指针指向一个数组对象(数组对象也是一个对象,和普通对象最大的区别需要一个位置来记录数组的长度)、offset、count、hash、serialVersionUID(这个不用计算在对象的大小中,因为在JVM启动时就会被装入到方法区中)。其次,还有对象对其的过程,而String的内容为char数组引用,指向的数组对象的内部的内容,也就是一个String相当于就包含了两个对象,两个对象都有头部,以及对其方式,数组头部会多一个保存数组长度的区域,头部还会存储对象加锁状态、唯一标识、方法区指针、GC中的Mark标志等等相应的内容,如果头部存储空间不够就会在外部开辟一个空间来存储,内部用一个指针指向那块空间;另外对象会按照8byte对其方法进行对其,即对象大小不是8byte的倍数,将会填充,方便寻址。

   1: public class StringTest {
   2:     public static void main(String[] args) {
   3:         String a = "abc";
   4:         String b = "def";
   5:         
   6:         String c = a + b;
   7:         String d = "abc" + "def";
   8:         
   9:         String e = new String("abc");
  10:         
  11:         System.out.println(a == e);
  12:         System.out.println(a.equals(e));
  13:         System.out.println(a == "abc");
  14:         System.out.println(a == e.intern());
  15:         System.out.println(c == "abcdef");
  16:         System.out.println(d == "abcdef");
  17:     }
  18: }

结果如下:

   1: false
   2: true
   3: true
   4: true
   5: false
   6: true

   首先解释前面4个结果,再解释最后2个结果;首先String a = "abc"这样的申请,会将对象放入常量池中,也就是放在Perm Geration中的,而String e = new String("abc")这个对象是放在Eden空间的,所以当使用a == e发生地址对比,两者肯定结果是不一样的;而当发生a == "abc"两个地址是一样的,都是指向常量池的对应对象的首地址;而equals是对比值不用多说,肯定是一样的;a == e.intern()为什么也是true呢,就是当intern()这个方法发生时,它会在常量池中寻找和e这个字符串等值的字符串(匹配的方法为equals),如果没有发现则在常量池申请一个一样的字符串对象,并将对象首地址返回,如果发现了则直接返回首地址;而a是常量池中的对象,所以e在常量池中就能找到的地址就是a的首地址。

   后面两个结果一个是a指向常量池的“abc”,b指向常量池中的“def”,c是通过a和b相加,两个都是常量池对象;而d是直接等价于“abc”+“def”按照道理说,两个也是常量池对象,为什么两个对象和常量池的“abcdef”比较的结果不一样呢?首先:常量池的String+常量池String结果还在常量池,这句话是不正确的,为什么,首先将代码修改成这样:

   1: public class StringTest {
   2:     public static void main(String[] args) {
   3:         String a = "abc";
   4:         String b = "def";
   5:         String c = a + b;
   6:     }
   7: }

   首先这里会使用了一个指针指向一个常量池中的对象内容为“abc”,而另一个指针指向“def”,然后通过new申请了一个StringBuilder(jdk 1.5以前是StringBuffer),然后调用这个StringBuilder的初始化方法;然后分别做了两次append操作,然后最后做一个toString()操作;可见String的+在编译后会被编译为StringBuilder来运行,这里做了一个new StringBuilder的操作,并且做了一个toString的操作,凡是new出来的对象绝对不会放在常量池中;toString会发生一次内容拷贝,但是也不会在常量池中,所以在这里常量池String+常量池String放在了堆中;而下面这个情况呢:

   1: public class StringTest {
   2:     public static void main(String[] args) {
   3:         String d = "abc" + "def";
   4:     }
   5: }

   因为当发生“abc” + “def”在同一行发生时,JVM在编译时就认为这个加号是没有用处的,编译的时候就直接变成成

   String d = "abcdef";

   再例如:

   1: final String a = "a";  
   2: final String b = "ab";  
   3: String c = a + b;

   在编译时候,c部分会被编译为:String c = "aab";但是如果a或b有任意一个不是final的,都会new一个新的对象出来;其次再补充下,如果a和b,是某个方法返回回来的,不论方法中是final类型的还是常量什么的,都不会被在编译时将数据编译到常量池,因为编译器并不会跟踪到方法体里面去看你做了什么,其次只要是变量就是可变的,即使你认为代码是不可变的,但是运行时是可以被切入的。

   就是这么简单,运行时自然直接就在常量池中是一个对象了,而不需要每次访问到这里做一个加法操作,有引用的时候,JVM不确定你要拿引用去做什么,所以它并不会直接将你的字符串进行编译时的合并(其实在某些情况下JVM可以适当考虑合并,但是JVM可能是考虑到编译时优化的算法复杂性,所以这些优化可能会放在运行时的JIT来完成,但JIT优化这部分java代码是有一些前提条件的)

   所以并不是常量池String+常量池String结果还在常量池,而是编译时JVM就认为他们没有必要做,直接合并了,就像JVM做if(true)和if(false)的优化一样的道理,而前者如果是引用给出来的常量池对象,JVM在拼接过程中是通过申请StringBuilder来完成的,也就是它的结果就像普通对象一样放在堆当中的。


   那么String为什么不可变,因为+操作是新申请了对象;+到底做了什么,是申请了一个StringBuilder来做append操作,然后再toString成一个新的对象;如果不是new出来的字符串或者是通过.intern()得到的字符串,则是常量池中的对象;常量池中的字符串和常量池中的字符串拼接,他们的结果不一定还在常量池,如果还在常量池只有一种可能性就是编译时就合并了,因为运行时new出来的StringBuilder是不可能放在常量池中的,我们绝大部分字符串拼接都是有引用的,而不是直接两个常量串来做的。

   既然String拼接是通过StringBuilder来完成的,那么为什么String的+和StringBuilder会有那么大的差距呢?这是一个值得考虑的问题,如果String的+操作和StringBuilder是一样的操作,那么我们的StringBuilder就没有多大存在的必要了,因为apend太多字符串是一件非常恶心的事情。

   首先你会发现,如果在同一条代码中(不一定是同一行代码,因为java代码可以相互包装嵌套,指对于成来讲基本的一条代码),如String a = a + b + c;这条代码算是同一行,而System.out.println(a + b + c + String.format(d , "[%s]"));对于d就会单独处理后,再和a + b+ c处理,然后再调用System中的静态成员out对象中的println方法;

   对于同一条代码中,如果发生这种加法操作(不是编译时合并的),那么通过javap命令分析时会发现,结果回将其申请一个StringBuilder然后进行append,不论多少个字符串都会append,然后最后toString()操作,为什么性能差距会那么大(在循环次数越多的时候差距会越来越大),用多行和循环测试,在使用String做+操作时,如果是多条代码或者在循环中做的话,每条代码都会做一个新的new StringBuilder,然后最后会toString一下,也就是当两个字符串相加时,会“最少”多申请一个StringBuilder然后再转换为一个String(虽然是将StringBuilder中内容拷贝到一个新的String中,但是空间是两块),所以浪费空间比较快,而且如果字符串越长,循环的过程中就会逐步进入old,而且old中的东西也会越来越多,导致了疯狂的GC,最后会疯狂的Full GC,再多的内存也会很快达到Full GC,只要你做循环;其实在常规应用中,一般只需要做几行的字符串叠加也无所谓,如果能写成一行就写成一行,如果非要写成多行还想要性能的话,就用StringBuilder吧;其实快并不是在多少申请了对象,因为java申请对象的速度非常快速,不存在说因为多申请了两个对象就会导致什么大的问题,大的问题是因为这些临时空间所产生的垃圾,最终导致了疯狂的GC,上述两种情况在做多次循环的过程中本地使用代码:-XX:+PrintGCDetails来运行,会发现,使用String做加法,刚开始会疯狂的YGC,过一段后会疯狂的FullGC,最后内存溢出,而使用StringBuilder几乎不会做GC,要做应该是做YGC,如果发生FGC一般说明这个字符串已经快把OLD区域撑满了,也就说马上要内存溢出了,而前者临时对象也应该去掉的,但是它会比StringBuilder叠加次数更少的时候,发生内存溢出,那是因为对象比较大的时候,临时对象已经在old区域,而前一个临时对象正好是要作为后一个对象的拷贝,所以在后面那个对象还没有拷贝成功前,前面那个对象的空间还不能被释放,那么很明显,old区域的利用率一般到一半的时候就溢出了。


   最后补充一个话题,其实StringBuilder也有一些问题,就是在动态扩容的过程中,每次增加2倍的空间,并不是在原有空间上做类似的C语言的realloc操作,而是新申请一个2倍大小的空间,将这些内容再拷贝过去;StringBuilder之所以可以动态增加是因为一个预先分配的char长度,如果没有满可以继续在后面添加内容,如果满了就申请一个2倍的空间,然后将前面的拷贝过去;不难说出两个问题,所谓的动态扩容只是逻辑上的实现,而并非真正的动态扩容,这也有它的内存安全性考虑,而String是多长,数组的长度就多长;另一个可以看出的问题就是动态扩容的过程中同样会产生各种各样的垃圾对象,其实在循环的过程中,看得往往还没有那么明显,在多线程访问多个随机方法,每个随机方法内部都会去做一些apend,而且都大于10的时候,临时对象就多了;不过还好,它的临时对象只是char数组,而不是String对象,前面说了,String对象相当于两个对象,前面那个对象的大小也是很大的;但是如果需要考虑这样的细节,那么请在编写StringBuilder的时候,预先写好它可能的最大长度,尤其是被反复调用的代码,如StringBuilder builder = new StringBuilder(2048);一般的小对象没有必要这样做,而且一次申请对象如果过大可能很容易进入old区域,甚至于直接进入old区域,这是我们不想看到的。

   目前的Hotspot还未解决这个问题,但是JRockit已经有一种解决方案了,它的解决方案很好的一种方法,就是在编译时它就能决定在这个局部方法内部你会发生多少次的append操作,那么它的StringBuilder内部做的就不是char数组,而是一个String[],预先分配数组的长度就是和append次数一样大小的数组,每做一次append就像数组下标增加1,并且放在对应的数组位置,并记录下总体的长度,待这个对象发生toString操作时,此时再申请一个这个长度一样大小的char[]空间,将数据拷贝进去,就解决了所有的临时对象的问题,对于在增加了一次间接访问和toString时候发生的逐个拷贝这些开销都是可以接受的(只要append的次数不是特别的多,一般append的次数也不可能特别多,所以利用循环测试出来的性能区别这个时候也是不靠谱的)

   

   最后,所谓的String拼接和StringBuilder下的使用,只要不是太大的字符串或者太多次数的拼接或者高并发访问的代码段做了2行代码以上的拼接,String做加法几乎和StringBuilder区别不大;太大的字符串产生的太大的临时空间,太多的拼接次数是产生太多的临时空间,同一条代码中作String的拼接(不论拼接次数)和使用StringBuilder做append效果一致,只是每次append结果在这行发生完成后会发生toString操作,而默认申请的StringBuilder大小默认为10,如果超过限制则翻倍,这也算是一个限制。

0 0