天天看点

Java 干货之深入理解String

可以证明,字符串操作是计算机程序设计中最常见的行为,尤其是在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

字面量对应起来即可。

也就是类似

int a=0; //a 的值为数值0
String str="test" //str内容为常量池中的utf8 test 

           

但是,问题就真的这么简单么?

可别忘了,

String

也是一个对象,它也同时拥有所有一个对象应该拥有的特点,也就说

String str="test"
           

其中

test

字面量不仅仅需要表示

str

指向的内容是

test

,它还应该将str指向一个对象,支持类似

str.length(),str.replace()

等一切对象访问的操作。

test

的内容写在

Class

文件中仅仅解决的是如果赋值的问题,那String对象是如何在内存中存在呢?

String创建过程

打开

java.lang.String

文件,可以看到

String

拥有不可变对象的所有特点,

final

修饰的类,

final

修饰的成员变量,因此任何看似对String内容进行操作的方法,实际上都是返回了一个新的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

    中。

如图所示:

  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");
           

下面图解分析:

需要明白的一点的是

new String("hello world")

并不是一个原子操作,可以将其分为两步,每个关键字负责不同的工作其中

new

负责生成对象,

String("hello world")

负责初始化

new

生成的对象。
  • 首先,执行

    new

    操作,在堆中分配空间,并生成一个

    String

  • 其次,将

    new

    生成的对象的引用传递给

    String("hello world")

    方法进行初始化,而此时参数中出现了

    "hello world"

    字面量,JVM会先在字符串常量池里面检查是否有

    equals("hello world")

    的引用,如果没有,就在堆中创建相应的对象,并生成一个引用指向这个对象,并将此引用存储在

    字符串常量池

  1. 再次,复制常量池

    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("字面量") 中 "字面量" 是何时进入字符串常量池的?

有个很神奇的坑,《深入理解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()方法的讲解中所举的例子?

方法区

上面图中说了,

“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

的问题:

String str1 = new StringBuilder("Hel").append("lo").toString();
System.out.println(str1.intern() == str1); 
String str = "Hello";
System.out.println(str == str1); 
           
String str1="hello";
String str2=new String("hello");
System.out.println(str2 == str1); 
           
final String str1="hell";
String str2="hello";
String str3=str1+"o";
System.out.println(str2 == str3); 
           
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("字面量") 中 "字面量" 是何时进入字符串常量池的?