dengchengchao

可以证明,字符串操作是计算机程序设计中最常见的行为,尤其是在Java大展拳脚的Web系统中更是如此。
---《Thinking in Java》


提到Java中的String,总是有说不完的知识点,它对于刚接触Java的人来说,有太多太多的值得研究的东西了,可是为什么Java中的String这么独特呢?今天我们来一探究竟。


基本数据类型

众所周知Java有八大基本数据类型,那么基本数据类型与对象有什么异同呢?

  • 基本数据类型不是对象
  • 基本数据类型能直接存储变量对应的值在堆栈中,存取更加高效
  • 使用方便,不用new创建,每次表示都不用新建一个对象

字面量与赋值

什么叫字面值呢?考虑下面代码:

int a=3;
double d=3.32;
long l=102322332245L;

其中,3、3.32、102322332245L便叫做字面值。3默认是int类型,3.32默认是double类型,102322332245默认也是int类型,所以必须加一个L来将它修改为long类型,否则编译器就会报错,字面量可以直接在计算机中表示。

基本数据类型便可以直接通过字面值进行赋值


String与基本数据类型

话说了这么多,这和String有什么关系呢?正如本文最开始所说,因为Java需要常常与字符串打交道,因此Java的设计者想要将String类型在使用上性能上尽量像基本数据类型一样。
也就是

int i=0;
String str="test";

那么问题来了,基本数据类型之所以叫基本数据类型,是因为这种类型可以直接在计算机中直接被表示,比如int i=0;中,0作为字面值是可以直接被表示出来"0x00",而test作为字符串如何直接被表示呢?


常量池

JVM的解决方案便是Class文件常量池。Class常量池中主要用于存放两大类常量: 字面量符号引用量,其中字面量便包括了文本字符串。

也就是当我们在类中写下String str="test"的时候,Class文件中,就会出现test这个字面量。

String类型的特殊性在于它只需要一个utf8表示的内容即可。
这样便解决了String直接赋值的问题,只要在JVM中,将str与test字面量对应起来即可。

但是,问题就真的这么简单么?
可别忘了,String也是一个对象,它也同时拥有所有一个对象应该拥有的特点,也就说

String str="test"
将```test```的内容写在``` Class```文件中仅仅解决的是如果赋值的问题,那String对象是如何在内存中存在呢?

----
#### String创建过程

打开```java.lang.String```文件,可以看到```String```拥有不可变对象的所有特点,``` final```修饰的类,``` final```修饰的成员变量,因此任何看似对String内容进行操作的方法,实际上都是返回了一个新的String对象。
正因为```String```这样的特点,我们可以建立一个对String的对象的缓存池:```String Pool```,用来缓存所有第一次出现的```String```对象。 

> JVM规范中只要求了```String Pool```,但并没有要求如何实现,在```Hot Spot JVM```中,是通过类似一个```HashSet<String>```实现,里面存储是当前已存储的String对象的**引用**:

String str="test";

首先虚拟机会在```String Pool```中查找是否有```equals("test")```的String,如果有,就把字符串常量池里面对```"test"```对象的引用赋值给str。如果不存在,就在**堆**中新建一个"test"对象,并将引用驻留在字符串常量池(```String Pool```)中,同时将该引用复制给str。

可以看到,Java在这里是使用的String缓存对象来解决“字面值”这个问题的。**也就是说,"test"所对应的字面值其实是一个在字符串常量池的String对象**这样做只要出现过一次的String对象,第二次就不再会被创建,节约了很大一笔开销,便解决了String类似基本数据类型的性能问题。

----
#### 深入理解String

明白了String的前因后果,现在来梳理关于String的细节问题。

String str="test"

包含了3个“值”:

-  ```"test"```字面量,表示String对象所存储的**内容**,编译后存放在Class字节码中,运行时存放在```Class```对象中,而```Class```对象存储在JVM的方法区中
-  ```test```对象,存储在堆中
-  ```test```对象对应的引用,存储在```String Pool```中。

如图所示:

![image](https://raw.githubusercontent.com/dengchengchao/Images/master/3%E4%B8%AAstring.jpg)


其中

1. 一定注意str所指向的对象是存放在**堆**中的,网上大多数说的不明白,更有误导```String Pool```中存储的是对象的说法。Java 对象,排除逃逸分析的优化,所有对象都是存储在堆中的。

2. ```String Pool```位于JVM 的```None Heap```中,并且```String Pool```中的引用持有对堆中对应String对象的引用,因此不必担心堆中的String对象是被GC回收。

3. 网上很多文章还会说```test```字面值是存在```Perm Gen```中,但是这样并不准确,永生代(“Perm Gen”)只是Sun JDK的一个实现细节而已,Java语言规范和Java虚拟机规范都没有规定必须有“Permanent Generation”这么一块空间,甚至没规定要用什么GC算法——不用分代式GC算法哪儿来的“永生代”? HotSpot的PermGen是用来实现Java虚拟机规范中的“方法区”(method area)的。

4. 前面说过,Java想将String向基本数据类型靠近,还能体现在对```final String```对象的处理,对于```final String```,如果使用仅仅是字面值的作用,而并没有涉及到对象操作的话(使用对象访问操作符"."),编译器会直接将对应的值替换为相应字面值。举例:
 
    
    ```
    final String str="hello";
    String helloWorld=str+"world";
    ```
    编译器会直接优化:
    ```
    String helloWorld="helloworld";
    ```
 
    
    ```
    final String str="hello";
    String hello=String.valueOf(str); 
    ```
    编译器会直接优化
    ```
    String hello=String.valueOf("hello"); 
    ```
    如果没有编译器的优化,就会涉及到操作数压栈出栈等操作,但是经过优化后的String,可以发现并不会有astore/aload等指令的出现.

-----
#### new String()
其实```new String```没什么好说的,```new String()```表示将```String```完全作为一个对象来看,放弃它的基本数据类型性质,也与```String Pool```没有任何关系,但是```String```包含的```intern()```方法能将它与```String Pool```关联起来。

- jdk 1.7之前,```intern()```表示若```String Pool```中不存在该字符串,则 在堆中新建一个与调用```intern()```对象的字面值相同的对象,并在```String Pool```中保存该对象的引用,同时返回该对象,若存在则直接返回。
- jdk 1.7及1.7 之后,```intern()```表示将调用```intern()```对象的引用直接复制一份到```String Pool```中。

网上很多讨论涉及到几个对象

String str=new String("hello world");

下面图解分析:

![image](https://raw.githubusercontent.com/dengchengchao/Images/master/newString.jpg)

> 需要明白的一点的是```new String("hello world")```并不是一个原子操作,可以将其分为两步,每个关键字负责不同的工作其中```new```负责生成对象,```String("hello world")```负责初始化```new```
生成的对象。

- 首先,执行```new```操作,在**堆**中分配空间,并生成一个```String```对象。
- 其次,将```new```生成的对象的引用传递给```String("hello world")```方法进行初始化,而此时参数中出现了```"hello world"```字面量,JVM会先在**字符串常量池**里面检查是否有```equals("hello world")```的引用,如果没有,就在**堆**中创建相应的对象,并生成一个引用指向这个对象,并将此引用存储在```字符串常量池```中。

2. 再次,复制常量池```hello world```指向的**字面量**对象传递给``` new String("hello world")```进行初始化。

> 第二点中提到了复制,其实最主要的就是复制```String```对象中```value```所指向的地址,也就是将方法区中的```"hello world"```的索引复制给新的对象,这也是为什么上图中,两个对象都指向方法区中同一个位置

下面的```String str=new String("hello world")```进行反编译的结果:
   0: new           #2                  // class java/lang/String
   3: dup
   4: ldc           #3                  // String hello world
   6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
   9: astore_1
  10: return
大概的指令应该都能看到,解释一下:

- new 执行new 操作,在堆中分配内存
- dup 将new 操作生成的对象压栈
- ldc 将String型常量值从常量池中推送至栈顶
- invokespecial 调用```new String()```并传入new 出来的对象了ldc的String值

> ldc指令是什么东西?
简单地说,它用于将int、float或String型常量值从常量池中推送至栈顶,在这里也能看到,JVM是将```String```和八大基本数据类型统一处理的。    
> ldc 还隐藏了一个操作:也就是"hello world"的resolve操作,也就是检测“hello world”是否已经在常量池中存在的操作。
>传送门详见:[Java 中new String("字面量") 中 "字面量" 是何时进入字符串常量池的?](https://www.zhihu.com/question/55994121)

-----

有个很神奇的坑,《深入理解JVM》中曾经提到过这个问题,不过周志明老师是拿的"java"作为举例:
    代码如下(jdk 1.7)
    ```
    
    public class RuntimeConstantPoolOOM {
        public static void main(String[] args) {
            String str1 = new StringBuilder("计算机").append("软件").toString();
            System.out.println(str1.intern() == str1);
    
            String str2 = new StringBuilder("ja").append("va").toString();
            System.out.println(str2.intern() == str2);
        }
    }
    ```
    结果 
    true
    false
    
不明白为什么"java"字符串会在执行StringBuilder.toString()之前出现过?   
其实是因为:Java 标准库在JVM启动的时候加载的某些类已经包含了```java```字面量。

传送门:[如何理解《深入理解java虚拟机》第二版中对String.intern()方法的讲解中所举的例子?](https://www.zhihu.com/question/51102308)

-----
#### 方法区
上面图中说了,```“hello wold”```对象的```value```的值是放在方法区中的。如何证明呢?
这里我们可以使用反射来干一些坏事。   
虽然```String```类是一个不可变对象,对外并没有提供如何修改```value```的方法,但是反射可以。
    String str1=new String("abcd");
    String str2="abcd";
    String str3="abcd";
    Field valueField = String.class.getDeclaredField("value");
    valueField.setAccessible(true);//设置访问权限
    char[] value = (char[]) valueField.get(str2);
    value[0] = '0';
    value[1] = '1';
    value[2] = '2';
    value[3] = '3';

    System.out.println(str1);
    System.out.println(str2);
    System.out.println(str3);
    String str4="abcd";
    System.out.println(str4);
    System.out.println("abcd");
可以试一试,输出结果都是```0123```,因为在编译的时候生成```Class```对象的时候,```str1,str2,str3,str4```都是指向的```Class```文件中同一个位置,而在运行的时候这个```Class```对象的值被修改后,所有和```abcd```有关的对象的```value```都会被改变。   
相信理解了这一个例子的同学,能够对```String```有一个更加深刻的理解

-----
#### 检验
说了这么多,你真的懂了么?来看看经常出现的一些关于```String```的问题:

1.

String str1 = new StringBuilder("Hel").append("lo").toString();
System.out.println(str1.intern() == str1);
String str = "Hello";
System.out.println(str == str1);

2.

String str1="hello";
String str2=new String("hello");
System.out.println(str2 == str1);

3.

final String str1="hell";
String str2="hello";
String str3=str1+"o";
System.out.println(str2 == str3);

4.

String str1="hell";
String str2="hello";
String str3=str1+"o";
System.out.println(str2 == str3);
```

参考文章:
《深入理解JVM》 第二版
java用这样的方式生成字符串:String str = "Hello",到底有没有在堆中创建对象?
R大:请别再拿“String s = new String("xyz");创建了多少个String实例”来面试了吧
Java 中new String("字面量") 中 "字面量" 是何时进入字符串常量池的?

相关文章: