String,StringBuffer,StringBuilder的区别

常用的jdk的组件的源码分析之:String,StringBuffer,StringBuilder

String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)

String和StringBuffer的主要性能区别其实在于 String是不可变的对象, 因此在每次对String类型进行改变的时候其实都等同于生成了一个新的 String对象,然后将指针指向新的String对象,所以经常改变内容的字符串最好不要用String,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后,JVM的GC就会开始工作,那速度是一定会相当慢的。

那么String为什么要是不可变的呢?

String类不可变的好处

  1. 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现,String interning是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串。因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
  2. 如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
  3. 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
  4. 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。
  5. 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。也同时指出一个理念,千万不要把可变类型作为HashMap和HashSet的键值

在java中如何设计不可变

  1. 对于属性不提供设值的方法
  2. 所有的属性定义为private final
  3. 类声明为final不允许继承
  4. return deep cloned objects with copied content for all mutable fields in class

翻看string的源码,可以看到string的本质是个char数组,并且使用final关键字修饰。但是char数组用final修饰只能让数组的引用地址不变,array数组还是可变的,主要是SUN的工程师没有暴露内部成员字段,所以String不可变主要在底层实现,而不是在final。

String的内存存储

一般而言,Java 对象在虚拟机的结构如下:

  • 对象头(object header):8 个字节
  • Java 原始类型数据:如 int, float, char 等类型的数据,各类型数据占内存。
    • boolean 1
    • byte
    • char 2
    • short
    • int 4
    • long 8
  • 引用(reference):4 个字节
  • 填充符(padding)

然而,一个 Java 对象实际还会占用些额外的空间,如:对象的 class 信息、ID、在虚拟机中的状态。在 Oracle JDK 的 Hotspot 虚拟机中,一个普通的对象需要额外 8 个字节。

String对象的声明

1
2
3
4
private final char value[]; 
private final int offset;
private final int count;
private int hash;

那么因该如何计算该 String 所占的空间?

首先计算一个空的 char 数组所占空间,在 Java 里数组也是对象,因而数组也有对象头,故一个数组所占的空间为对象头所占的空间加上数组长度,即 8 + 4 = 12 字节 , 经过填充后为 16 字节。

那么一个空 String 所占空间为:

对象头(8 字节)+ char 数组(16 字节)+ 3 个 int(3 × 4 = 12 字节)+1 个 char 数组的引用 (4 字节 ) = 40 字节。

因此一个实际的 String 所占空间的计算公式如下:

1
8*( ( 8+2*n+4+12)+7 ) / 8 = 8*(int) ( ( ( (n) *2 )+43) /8 )

在java中对String对象特殊对待,所以在heap上分为两块,一块是String constant pool存储java字符串常量,另一块存储普通对象和字符串对象,主要区别:

1
2
String a = "abc";
String b = new String("acb")

第一种jvm会先去查找constant pool是否存在此常量,不存在就在constant pool上进行创建,第二种是在堆上创建对象,并且不会加入到constant pool上,因此可能会带来字符串重复占用内存的问题。可以调用String.intern()加入到String constant pool中,其实是JVM heap 中 PermGen 相应的区域。

jdk1.6和1.7还有所不同,jdk1.7的常量池是在堆中的

StringBuffer

StringBuffer和String不同,每次修改都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。而在某些特别情况下, String 对象的字符串拼接其实是被JVM解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String效率是远要比 StringBuffer 快的:

1
2
String S1 = “This is only a” + “ simple” + “ test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);

你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本一点都不占优势。其实这是 JVM 的一个把戏,在 JVM 眼里,这个

1
2
3
String S1 = “This is only a” + “ simple” + “test”;
其实就是:
String S1 = “This is only a simple test”;

当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如:

1
2
3
4
String S2 = “This is only a”;
String S3 = “ simple”;
String S4 = “ test”;
String S1 = S2 +S3 + S4;

这时候 JVM 会规规矩矩的按照原来的方式去做

https://www.ibm.com/developerworks/cn/java/j-lo-optmizestring/index.html

https://blog.csdn.net/qq_36357995/article/details/79985538

https://segmentfault.com/a/1190000004261063

谢谢支持