String 对象内存分配(常量池和堆的分配)

 

   在一次面试中,面试官问了一个关于字符串对象创建问题,问题内容如下:

场景一:
String s1 = "123" + "456";

场景二:
String s1 = "123";
String s2 = s1 + "456";

  在这两个场景下,分别会创建多少个字符串对象?当时其实并没太了解在 JVM 中,字符串对象的内存分配具体策略,只了解在 JVM 中有一个常量池,常量池中存放了常用的字符串对象。 
  当时的回答是,在场景一中,会创建三个字符串对象,因为 s1 在编译时能够确定,因此 “123” 、 “456” 和 “123456” 都是存放在常量池中;而场景二也是会创建对象,但因为 s2 需要在运行时才能确定,因此 “123456” 是存放在堆中,而 “123” 和 “456” 存放在常量池中。 
  后来查阅了一些资料,才发现这种说法其实是不对的,下面我将从 一些例子来说明 String 对象分配和常量池的机制。

String 对象

String 类的定义

  相信学习过 Java 的朋友,对 String 类应该都很熟悉,因为你在编程的时候肯定需要经常用到,我们先来看看 String 类的定义:

public final class String {
    private final char value[];
}

  从 String 类是一个 final 类,它不可被继承,并且内部通过一个字符数组表示该字符串的内容,且一旦创建后,该字符串内容是不可变的,它的 equals 方法就是通过比较字符数组的内容是否相等来判断字符串对象是否相等。以上的内容仅需要有一个大概了解即可,接下来才是主题。

String 对象创建

  创建字符串对象方式有很多种,一般常用的方式有如下:

    方式一:
    String str = "123";

    方式二:
    String str = new String("123");

    方式三:
    String str = "123".intern();

  上述的方式都会创建一个 “123” 对象,但是他们内部的实现机制是不同的,下面就分别对每种方式进行分析。

String str = “123”;

  通过引号直接创建字符串对象,先会先从常量池中判断是否存在 “123” 对象,如果不存在,则会在常量池中创建该对象,并且返回常量池中 “123” 对象的引用给 str;如果存在 “123” 话,则直接返回常量池中 “123” 的对象引用。 
  因此如下例子,将会输出 true

public class StringTest {
    public static void main(String[] args) {
        String s1 = "123";

        String s2 = "123";

        System.out.println(s1 == s2);
    }
}

String s1 = new String(“123”);

  这种方式估计大家都很熟悉,一般创建一个对象都是通过 new 关键字。String s1 = new String(“123”) 这种方式它将会创建两个 “123” 对象,为什么呢? 
  首先 “123” 是一个常量字符串,因此会先在常量池中创建 “123” 字符串对象,然后在堆中再创建一个字符串对象,将 “123” 的字符数组复制到堆中新创建的对象的字符数组上,因此该方式不仅会在堆中,还会在常量池中创建 “123” 字符串对象。 
  因此如下例子,将会输出 false,因为堆中和常量池中 “123” 字符串不是同一个对象。

public class StringTest {
    public static void main(String[] args) {
        String s1 = new String("123");

        String s2 = "123";

        System.out.println(s1 == s2);
    }
}

String str = “123”.intern()

  该种方式通过 intern 方法来返回一个字符串对象引用,因此首先需要了解 intern 方法的实现原理,intern 方法是一个 native 方法,具体的方法实现可以自己去查看。 
  方法的主要过程:当线程池存在 “123” 字符串对象,则直接返回该常量池中的字符串引用;若不存在,则会先在常量池中创建 “123” 字符串对象,返回新创建对象的引用。该方法常用于将某些经常访问的字符串对象保存在常量池中,避免经常创建对象。 
  因此如下例子,将会返回 false false true。

public class StringTest {
    public static void main(String[] args) {
        String s1 = new String("123");

        String s2 = "123";

        String s3 = s1.intern();

        System.out.println(s1 == s2);
        System.out.println(s1 == s3);
        System.out.println(s2 == s3);
    }
}

常量池

  通过上面的方法讲解,我们可以看到创建字符串对象经常会涉及到常量池,那么在 Java 中,常量池是分配在 JVM 的哪块内存区域,它又是如何来管理这些常量字符串对象的? 
  在了解常量池前,首先要先对两个结构有一个认识,分别是 CONSTANT_Utf8_info 和 CONSTANT_String_info,如下是他们的定义

CONSTANT_Utf8_info {
    int tag;
    int length;
    byte[] bytes;
}

CONSTANT_String_info {
    int tag;
    int index;
}

  前者用于存储常量池具体的字面量的数据,后者主要主要是用于存储索引,通过 index 来查找对面的字面量。它们在 JVM 中具体是以一种如 HashTable 的形式来组织起来的。

下面来看下面一个例子:

public class StringTest {
    public static void main(String[] args) {
        String s1 = new String("123");
        String s2 = "123";
        System.out.println(s1 == s2);

        String s3 = new String("123") + new String("123");
        s3.intern();
        String s4 = "123123";
        System.out.println(s3 == s4);
    }
}

  在下面的例子,它会输出 false true(使用的是 JDK 1.8),但当使用 JDK 1.6 运行时,却会输出 false false。下面就这两种情况具体分析,为什么会出现这种情况。

JDK 1.6 的常量池

  在 JDK 1.6 中,Java 有一个区域是 Perm Space(永久代),在该版本中,常量池是存放于该区域的,上述例子对象内存分配如下图 
这里写图片描述 
  堆中的对象和永久代的对象分离的,通过 new 关键字会在堆中创建字符串对象,字面量和通过 intern 方法创建的字符串对象都会在常量池中创建字符串对象(若此时常量池不存在该字面量),因此它们的对象引用不是同一个,所以通过 == 比较会返回 false。

JDK 1.6 后的常量池

  在 JDK 1.6 后,Java 将常量池从 Perm Space 移出放到堆中,甚至 1.8 还将该区域取消掉了,上述例子在这些版本的对象内存分配如下图: 
这里写图片描述 
过程解析: 
1. 首先第一句语句 String s1 = new String(“123”);,这里要编译时首先涉及到 “123” 这个字面量,因此会先在常量池中创建 “123” 这个对象,然后通过 new 关键字在堆中再创建一个 “123” 的对象,此时 s1 引用的是后者,和常量池的 “123” 不是同一个引用; 
2. 第二句语句 String s2 = “123”;,由于常量池此时已有 “123” 这个字面量对象,因此直接返回该引用给 s2,从第一步可知道 s1 和 s2 并不是同一个对象,因此他们将会返回 false; 
3. 第三句语句 String s3 = new String(“123”) + new String(“123”); 首先涉及到字面量 “123” 肯定会在常量池中创建 “123” 字面量对象,然后再在堆中创建一个 ”123123” 对象,返回该引用给 s3; 
4. 第四句语句 s3.intern(); intern 方法会先判断常量池中是否存在与 s3 相同字符串的对象,若有,则返回该引用;若无,则在常量池创建一个引用(CONSTAT_String_info)指向 s3,然后返回该引用,实际上返回的是 s3 的引用。 
5. 第五条语句,s4 = “123123”;,会返回常量池 “123123” 的引用给 s4,从第四步可知,该引用实际上是 s3 引用的对象,因此 s3 == s4 返回true。如果将第四和第五语句调换,则返回的结果是 false,你们可以自己分析一下为什么。

面试题的解答

  相信到这里大家应该已经对 String 和 常量池有一个初步的认识,那么现在我们回到最一开始的问题,实际上它是怎么运行的呢?

场景一:
String s1 = "123" + "456";

场景二:
String s1 = "123";
String s2 = s1 + "456";

  其实在 Java 中,对于字符串连接它会优化成使用 StringBuilder 来完成连接(具体从哪个版本开始支持,我也不知道),那么它们实际上会转变成 
   
场景2:

String s1 = "123";
String s2 = new StringBuilder.append(s).append("456").toString();

在这种情况,他依然涉及到 “123” 和 “456” 两个字面量,因此在常量池中仍会保存这两个字面量对象,然后再在堆中创建一个 “123456” 对象,这里涉及了三个对象的创建。

场景1: 
上面说了会使用 StringBuilder 进行 + 号连接,那么场景1不就和场景2一样了?其实具体的过程并不是这样,对于字面量的连接,Java 会将其优化成 String s1 = “123456”,最终这里只会创建一个对象。

最后

  本文给大家介绍了 String 创建对象的方式,通过例子分别介绍这些方式的区别;另外还介绍了在不同版本下的常量池的实现机制,博文可能会存在错误的地方,望指出,一起交流交流。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值