Java的String字符串内存布局知多少?

1. 初始化

public static void main(String[] args) {
    
    
        String str1 = "hello";
        String str2 = new String("hello");
        char[] chars = {
    
    'h', 'e', 'l', 'l', 'o'};
        String str3 = new String(chars);
}

在这里插入图片描述

仔细观察 str3 的创建过程
分析 new String(chars) 源码可得
在这里插入图片描述
会有一个 Arrays.copyOf(arr, length) 的拷贝工作。这就是 str3 的来历

总结

  1. 所有双引号内容都在 字符串常量池
  2. new 对象都在 堆
  3. new 对象首先会在 字符串常量池 中查找有无 字符串,没有的话就会自己创建,有的话就存储指向 字符串常量池 的引用值

2.比较

1. 直接赋值和引用赋值

了解了上述初始化的部分过程,可以看看常见字符串的比较的 true 与 false

    public static void main(String[] args) {
    
    
        String str1 = "hello";// 所有双引号内容都在 字符串常量池
        String str2 = new String("hello");// new 对象都在 堆
        System.out.println(str1 == str2);// 比较的还是值相不相同,但是这个值不是字符串而是 引用值
        System.out.println(str1.equals(str2));// 比较的是内容是否相同
    }

false
true

在这里插入图片描述

2. 字符串拼接赋值

    public static void main(String[] args) {
    
    
        String str1 = "hello";
        String str2 = "hel" + "lo";// 拼接完后,依旧在常量池中
        System.out.println(str1 == str2);
        System.out.println(str1.equals(str2));
    }

true
true

在这里插入图片描述
为什么双引号的拼接操作也是在常量池中完成的呢?

cxf@cxfdeMBP bit % javap -c test       
警告: 二进制文件test包含bit.test
Compiled from "test.java"
public class bit.test {
    
    
  public bit.test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String hello
       2: astore_1
       3: ldc           #2                  // String hello
       5: astore_2
       6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: aload_1
      10: aload_2
      11: if_acmpne     18
      14: iconst_1
      15: goto          19
      18: iconst_0
      19: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
      22: return
}

查看 main 函数中的 ldc 发现 无论是直接赋值也好还是拼接操作,都是直接形成一个整体字符串

检验数字计算操作是否也和字符串拼接一样

    public static void main(String[] args) {
    
    
        int a= 10;
        int b = 5 + 5;
    }

反汇编如下:

cxf@cxfdeMBP bit % javap -c test
警告: 二进制文件test包含bit.test
Compiled from "test.java"
public class bit.test {
    
    
  public bit.test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: bipush        10
       2: istore_1
       3: bipush        10
       5: istore_2
       6: return
}

我们发现 5+5操作被直接形成 10 这个整体

3. 字符串和变量的拼接赋值

    public static void main(String[] args) {
    
    
     	String str1 = "hello";
        String str2 = "hel";
        String str3 = str2 + "lo";
        System.out.println(str3 == str1);

        String str4 = "world";
        final String str5 = "wor";
        String str6 = str5+"ld";
        System.out.println(str6 == str4);
    }

false
true

str4, str5, str6全在字符串常量池中完成操作,图中为了简化工作量而省略【和2.字符串拼接赋值的图一样】
在这里插入图片描述

扫描二维码关注公众号,回复: 13362947 查看本文章

反汇编如下:

cxf@cxfdeMBP bit % javap -c test
警告: 二进制文件test包含bit.test
Compiled from "test.java"
public class bit.test {
    
    
  public bit.test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String hello
       2: astore_1
       3: ldc           #3                  // String hel
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_2
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: ldc           #7                  // String lo
      19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      25: astore_3
      26: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      29: aload_3
      30: aload_1
      31: if_acmpne     38
      34: iconst_1
      35: goto          39
      38: iconst_0
      39: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
      42: ldc           #11                 // String world
      44: astore        4
      46: ldc           #12                 // String wor
      48: astore        5
      50: ldc           #11                 // String world
      52: astore        6
      54: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      57: aload         6
      59: aload         4
      61: if_acmpne     68
      64: iconst_1
      65: goto          69
      68: iconst_0
      69: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
      72: return
}

反汇编分析

  1. 0: ldc #2 // String hello
    2: astore_1
    3: ldc #3 // String hel
    5: astore_2
    6: new #4 // class java/lang/StringBuilder
    9: dup
    10: invokespecial #5 // Method java/lang/StringBuilder."": ()V
    13: aload_2
    14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    17: ldc #7 // String lo
    19: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    22: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    25: astore_3
    26: getstatic #9 // Field
    发现未加 final 修饰的字符串拼接操作被 StringBuilder 优化
  1. 42: ldc #11 // String world
    44: astore 4
    46: ldc #12 // String wor
    48: astore 5
    50: ldc #11 // String world
    52: astore 6
    54: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
    final 修饰的字符串拼接操作直接在字符串常量池中完成拼接

final 修饰原因:

变量:就是在编译的时候不知道里边的值,在运行的时候才知道里边的值
常量:编译的时候就已经确定里边的值

4. 字符串和引用拼接赋值

    public static void main(String[] args) {
    
    
        String str1 = "hello";
        String str2 = "hel" + new String("lo");
        System.out.println(str2 == str1);
    }

false

str2 指向 字符串常量池 中的 “hel” 的引用和 堆 中的字符串 “lo” 完成拼接后自然而然的就处于堆中【堆引用了常量池中的字符串而非是在堆中新建了一个字符串常量池中的字符串,这样JVM可以节约空间】

5. 引用拼接赋值

    public static void main(String[] args) {
    
    
        String str1 = "hello";
        String str2 = new String("hel") + new String("lo");
        System.out.println(str2 == str1);
    }

false

6. 先创建字符串后引用入池

    public static void main(String[] args) {
    
    
        String str1 = "hello";
        String str2 = new String("hel") + new String("lo");
        str2.intern();
        System.out.println(str2 == str1);
    }

false

在这里插入图片描述

  1. intern: 首先会检查字符串常量池中是否含有需要入池的字符串,如果有则不入池;如果没有则入池
  2. 由于 str1 的先创建导致字符串常量池已经有 “hello” 导致 str2的 intern 入池操作失败。所以 str2 会继续保存 堆 中的引用值而不是字符串常量池中的引用值

7. 先引用入池后创建字符串

    public static void main(String[] args) {
    
    
        String str2 = new String("hello");
        str2.intern();
        String str1 = "hello";
        System.out.println(str2 == str1);
    }

true

在这里插入图片描述

str1 在堆中创建的 “hello” 经过 intern 操作入池后,后续的 str2 不会创建相同的 “hello” 字符串而直接指向字符串常量池中的 “hello” 地址。

8. 先引用拼接入池后创建字符串

   public static void main(String[] args) {
    
    
        String str2 = new String("hel") + new String("lo");
        str2.intern();
        String str1 = "hello";
        System.out.println(str2 == str1);
    }

true

在这里插入图片描述

堆中拼接好,直接指向字符串常量池中,所以 str1 和 str2 的引用相同

9. 先字符串入池在引用拼接入池

    public static void main(String[] args) {
    
    
        String str2 = new String("hel") + new String("lo");
        String str1 = "hello";
        str2.intern();
        System.out.println(str2 == str1);
    }

false

在这里插入图片描述

10. 变量赋值

    public static void main(String[] args) {
    
    
        String str1 = "hello";
        String str2 = str1;
        str1 = "world";
        System.out.println(str2 == str1);
    }

false

在这里插入图片描述

str1=“world” 会改变 str1 的指向

3. 不是传引用就能改变值

   private static void func(String str){
    
    
        str = "world";
    }
    public static void main(String[] args) {
    
    
        String str1 = "hello";
        func(str1);
        System.out.println(str1);
    }

hello

在这里插入图片描述
func(String str) 传进去的是引用,但是并未改变 main 函数中的 str 引用,所以 str 值不变

4. 理解 String 不可变对象

    public static void main(String[] args) {
    
    
        String str1 = "hello";
        str1 += "world";
        str1 += "!!!";
        System.out.println(str1);
    }

helloworld!!!

在这里插入图片描述
也说明 +拼接的字符串操作非常低效,会产生很多垃圾

这算是改变字符串吗?

    public static void main(String[] args) {
    
    
    	String str = new String("H");
        String str1 = "hello";
        str1 = "H"+str1.substring(1);// 还是产生了一个新对象,并没有把原来的 "hello" 改为 "Hello"
        System.out.println(str1);
    }

Hello

其实并不是,操作本质和 + 拼接类似

其实 String str1 = "hello";也是被 new 过的。查看 new String 的源码会发现:
在这里插入图片描述

存储的是一个被 private, final 修饰的 value[] 字段数组。第一反应是无法修改的,但是如果拿到这个类,也就可以修改了。

5. 反射修改字符串

反射:Java 类的一种自省方法。通常情况下:类的细节有时候在类外是看不到的,但是通过反射就可以看到

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    
    
        String str1 = "hello";
        Field valueField = String.class.getDeclaredField("value");// // 获取 String 类中的 value 字段. 这个 value 和 String 源码中的 value 是匹配的.
        valueField.setAccessible(true);// 将这个字段的访问属性设为 true
        char[] vals = (char[]) valueField.get(str1);//把 str1 中的 value 属性获取到.
        vals[0] = 'H';//修改 value 的值
        System.out.println(str1);
    }

Hello

猜你喜欢

转载自blog.csdn.net/weixin_45364220/article/details/120481229
今日推荐