96ace1fa56ef097300fb165e0f479b2d
Java - 字符串

看了极客时间的字符串文章,明显不够过瘾,我开始了长期思考关于 Java 字符串的各种问题,深入 JVM 理解 String 在 JDK 不同版本的变化

[TOC]

一、常用 API

  • StringBuffer

    • length()

    返回 StringBuffer 的长度,也就是当前字符串数组的大小

    • charAt(int i)

    返回 StringBuffer 第 i 个字符

    • append(String s)

    在 StringBuffer 末尾插入字符串 s

    • setCharAt(int i, char c)

    将第 i 个字符更改为 c

    • toString()

    将当前对象转换为字符串

  • String

    • capacity()

    返回当前容量

    • charAt(int index)

    返回指定索引处的 char 值

    • length()

    返回长度(字符数)

二、三种字符串对象

  • String

最大特点是 immutable,故不可拼接,每一次的拼接其实是产生新的对象,天生的线程安全

  • StringBuffer

可拼接,线程安全,线程安全导致性能不高,适合多线程

  • StringBuilder

可拼接,线程不安全,线程不安全所以性能高,适合单线程

三、String 的创建方式

  1. 通过直接赋值

例如,String S1 = "abc"

  1. 通过 new

例如,String S2 = new String("abc");

  1. 通过 intern()

例如,String S3 = S2.intern();

四、String Pool

这里引用一下 CyC2018 大佬的图!

为了提高效率,JVM 里为 String 设置了一个字符串常量池,它属于运行时常量池它一开始是位于方法区的,而由于 JDK 1.6 之后,也就是 JDK 1.7 开始,常量池(包括字符串常量池)从方法区移入了堆上,这使得字符串的内存变化

也就说:

  • JDK 1.6 以及它之前,字符串常量池位于方法区(堆和方法区是两个)
  • JDK 1.6 之后,字符串常量池位于堆

当然,JDK 1.8 的 JVM 内存模型有其他变化,引入元数据区,移除永久代之类的,这里暂且不讨论,等其他文章再慢慢细说!

五、String & JVM

5.1 通过 new

  • 举例:String S1 = new String("abc");

  • 区别:JDK 1.6 与 JDK 1.6 之后一样,两个版本无变化

  • 步骤:

  1. 创建两个值相等的对象(前提是String Pool 中还没用该对象),一个在堆中,一个在 String Pool 中,它们的引用是不同的!,当然,如果 String Pool 中已经存在,那么只会在堆中创建,所以就创建一次!

  2. 第一次创建:编译时期会在 String Pool 中创建一个字符串对象指向这个 "abc" 字符串字面量(如果已经存在就不创建了);第二次创建:使用 new 的方式会在堆中创建一个字符串对象,在 new 的时候,将 StringPool 的字符串当作参数传递给堆的那个对象的构造函数,它们将指向同一个内部的 value 数组

     public String(String original) {
         this.value = original.value;
         this.hash = original.hash;
     }
  • 两个问题:

    • 在 StringPool 中的对象是匿名的,何时显示被调用?

    有两种情况:

1. 当前指向堆的对象调用 intern() 的时候
2. 任意新建字符串直接赋值的时候

  • 为何要创建在两个区域同时两次?

    因为常量池的对象是作为参数使用的,创建的时候是有顺序的,首先在常量池中创建,然后把该对象作为参数赋值给 String ,从而在堆中创建了该对象

5.2 通过直接赋值

  • 举例:String S2 = "abc";
  • 区别:JDK 1.6 与 JDK 1.6 之后一样,两个版本无变化
  • 步骤:
    1. 查找 String Pool,如果常量池中已经存在该值的对象,返回常量池中的字符串引用
    2. 在常量池中创建该值对象,返回常量池中的新建的字符串的引用

5.3 通过 intern()

  • 举例:String S3 = S2.intern()
  • 作用:返回 S2 的引用给 S3,所以 S3 的值和 S2 相等
  • 区别:JDK 1.6 与 JDK 1.6之后不一样,两个版本有变化

5.3.1 JDK 1.6 以及之前版本

  • 步骤:
  1. 查找 String Pool,如果发现池中有了,返回该字符串的引用
  2. 如果池中没有,则在池中创建一个字符串对象,然后放入常量池,并返回它的引用
  • 一个问题:

    • 步骤 2 如何理解?

    使用赋值创建的对象的引用(如 5.2 的 S2),是直接放入常量池的,使用 new 创建的对象的引用(如 5.1 的 S1),创建第一次时,常量池也会有一份,它们的引用去 intern() 时,肯定在池中可以找到那个字符串字面量值的对象啊!然鹅,关键的来了,的确存在一种情况是,堆上有该字符串,而 String Pool 中不存在,详情看 5.4 的分析!

5.3.2 JDK 1.6 之后的版本

  • 步骤:
    1. 查找 String Pool,如果发现池中有了,返回该字符串的引用
    2. 如果池中没有,则在池中记录下当前调用该 intern() 的字符串对象的引用,并返回它的引用

5.4 String 问题代码实战

前言总结

通过第四部分 & 5.3.1 & 5.3.2,可以得出一个结论:

由于 JDK 1.7 开始,String Pool 位置与 JDK 1.6 以及之前的版本有所不同,所以导致两个字符串的 intern() 方法实现原理有所不同,具体不同体现在在 String Pool 中查找到没有与调用的对象的字面量值相等的对象时,一个是在池里创建新的字符串对象并返回该新的对象的引用,一个是在池里记录堆上的字符串对象并返回其引用

下面举例,将以 JDK 1.6 以及 JDK 1.7 来说明代码的测试结果对比!

以下环境每次都是重新运行,都是第一次创建的情况!

5.4.1 结果相同

  • 例 1-直接赋值 & intern()

    • 代码:
    String S1 = "123"; 
    String ref = S1.intern(); 
    System.out.println(S1 == ref); //true
    • 运行步骤:
    • 在 String Pool 创建一个值为 123 的字符串对象 ,引用 S1 指向它
    • 在 String Pool 中查找值为 123 的字符串对象,并返回该引用给 ref
    • JDK 1.6 & JDK 1.7 都一样,比较为 true,因为都是常量池里的同一对象引用
  • 例 2-new 与 intern()

    • 代码:
    String S2 = new String("123");
    String ref = S2.intern();
    System.out.println(S2 == ref); //false
    • 运行步骤:
    • 在 String Pool 创建一个值为 123 的字符串对象 ,但没有引用显示指向它!
    • 在堆创建一个字符串对象,然后将刚刚在 String Pool 里那个创建的对象作为参数赋值给现在堆上的对象的构造函数,并且让引用 S2 指向它
    • 在 String Pool 中查找值为 123 的字符串对象,此时发现了刚刚上面说的一起创建但没有引用指向它的字符串对象,返回该对象的引用给 ref
    • JDK 1.6 & JDK 1.7 都一样,比较为 false,因为 S2 是位于堆的引用,ref 是位于池里的引用
  • 例 3-直接赋值、new、intern()—Pt1

    • 代码:
    String S1 = "123";
    String S2 = new String("123");
    String ref = S2.intern();
    System.out.println(S1 == ref); //true
    • 运行步骤:
    • 在 String Pool 创建一个值为 123 的字符串对象 ,引用 S1 指向它
    • 在 String Pool 创建一个值为 123 的字符串对象 ,发现已经有了,而且 S1 指向它,所以就不创建了!
    • 在堆创建一个字符串对象,然后将刚刚在 String Pool 里找到的对象(S1 指向它的那个对象)作为参数赋值给现在堆上的对象的构造函数,并且让引用 S2 指向它
    • 在 String Pool 中查找值为 123 的字符串对象,此时发现了刚刚上面说的 S1 指向它,这样的一个字符串对象,返回该对象的引用给 ref
    • JDK 1.6 & JDK 1.7 都一样,比较为 true,因为都是常量池里的同一对象引用
  • 例 4- 直接赋值、new、intern()—Pt2

    • 代码:
    String S2 = new String("123");
    String S1 = "123";
    String ref1 = S1.intern();
    String ref2 = S2.intern();
    System.out.println(ref1 == ref2); //true
    • 运行步骤:
    • 在 String Pool 创建一个值为 123 的字符串对象 ,但没有引用显示指向它!
    • 在堆创建一个字符串对象,然后将刚刚在 String Pool 里那个创建的对象作为参数赋值给现在堆上的对象的构造函数,并且让引用 S2 指向它
    • 在 String Pool 中查找值为 123 的字符串对象,此时发现了刚刚上面说的一起创建但没有引用指向它的字符串对象,返回该对象的引用给 S1
    • 在 String Pool 中查找值为 123 的字符串对象,此时发现了 S1 指向它,这样的一个字符串对象,返回该对象的引用给 ref1
    • 在 String Pool 中查找值为 123 的字符串对象,此时发现了 S1、ref1 指向它,这样的一个字符串对象,返回该对象的引用给 ref2
    • JDK 1.6 & JDK 1.7 都一样,比较为 true,因为都是常量池里的同一对象引用
  • 例4-简洁版的反编译分析:

注意:这里是在 JDK 1.8 环境下的编译与反编译!

  • 代码:

    public class STR {
        public static void main(String[] args) {
            String S1 = new String("123");
            String S2 = "123";          
        }
    }
  • 反编译:

    javap -verbose

  • 分析

    • 看反编译结结果上面部分为常量池数据部分,下面部分为类的数据部分
    • 第 0 行,在堆上 new 一个 class 对象,是 String 类型的
    • 第 3 行,复制引用,压入线程的操作数栈中
    • 第 4 行,从常量池加载 #3 ,也是 123 字面量的引用到操作数栈中
    • 第 6 行,调用 String 构造方法,操作数栈弹栈(弹出了 第 4 行的引用),并将其作为参数
    • 第 9 行,将操作数栈栈顶的引用弹出,赋值给第 1 个引用,也就是将第 3 行压入的引用,赋值给局部变量表第一个引用(代码里的 String S1 这里的 S1 引用)
    • 第 10 行,从常量池加载 #3 ,也是 123 字面量的引用到操作数栈中
    • 第 12 行,将操作数栈栈顶的引用弹出,赋值给第 2 个引用,也就是将第 10 行压入的引用,赋值给局部变量表第二个引用(代码里的 String S2 这里的 S2 引用)
  • 结论

    从第 4 行与第 10 行,可知,S2 引用的是 S1 创建的时候一并创建的那个常量池的引用,也就是说 S2 创建的时候,它的值早已存在,而不是新放入常量池

5.4.2 结果不同

  • 例 1-new 、拼接、intern()

    • 代码:
    String S3 = new String("1")+new String("2")+new String("3");
    S3.intern();
    Stirng ref = "123";
    System.out.println(S3 == ref);//false
  • JDK 1.6 运行步骤:

    • 在 String Pool 中,创建一个值为 1 的对象,一个值为 2 的对象,一个值为 3 的对象,此时都没有引用显式指向它,它们都是匿名的!
    • 在堆里有三个匿名的对象,将上面说的常量池里创建的值 1、值 2、值 3 的对象作为参数赋值给堆里的三个匿名的对象,最好堆里完成了三个值相同的对象创建,但是它是匿名的,没有对象指向它
    • 然后将它们的值拼接后的结果 123 返回给 S3 引用(底层使用 StringBuilder 实现)
    • 此时,堆里有值为 123 、值为 1、值为 2、值为 3的对象
    • 此时,常量池里有值为 1、值为 2、值为 3的对象
    • S3 调用了 intern(),在常量池里创建了一个新的对象,并返回其引用
    • 将自身的引用复制一份放入常量池
    • 返回引用给 ref
    • 比较结果为 false
  • JDK 1.8 运行步骤:

    • 在 String Pool 中,创建一个值为 1 的对象,一个值为 2 的对象,一个值为 3 的对象,此时都没有引用显式指向它,它们都是匿名的!
    • 在堆里有三个匿名的对象,将上面说的常量池里创建的值 1、值 2、值 3 的对象作为参数赋值给堆里的三个匿名的对象,最好堆里完成了三个值相同的对象创建,但是它是匿名的,没有对象指向它
    • 然后将它们的值拼接后的结果 123 返回给 S3 引用(底层使用 StringBuilder 实现)
    • 此时,堆里有值为 123 、值为 1、值为 2、值为 3的对象
    • 此时,常量池里有值为 1、值为 2、值为 3的对象
    • S3 调用了 intern(),复制了一份堆上的引用记录在常量池里,并返回其引用
    • 将自身的引用复制一份放入常量池
    • 返回引用给 ref
    • 比较结果为 true
  • 反编译分析

通过 JDK 1.6 与 JDK 1.8 下同一份代码编译运行,毫无疑问,一个是 false,一个是 true;

然鹅!反编译结果 constant pool 与 代码区竟完全一样!令我无从下手,不过我本身 JVM 指令也是半吊子 = =

不过从代码层次是可以很显然论证上面的例子,而这次从 JVM 的字节码却看不出来,暂且留坑,待我以后回来补这份反编译分析!

© 著作权归作者所有
这个作品真棒,我要支持一下!
“限时免费“ 记录计算机最基础的知识,是我的复习笔记与学习笔记
0条评论
top Created with Sketch.