面试必备-String的特性详解-用源码、字节码和内存示意图详解,官文提供论点支撑

面试必备-String的特性详解-用源码、字节码和内存示意图详解,官文提供论点支撑,第1张

Java-String

文章目录
    • 1.String[^1]简介
      • 1.1.验证String是不可变对象
      • 1.2.两种创建字符串对象的区别
      • 1.3.创建了几个对象
        • 1.3.1.情况1
        • 1.3.2.情况2
        • 1.3.3.情况3
        • 1.3.4.情况4
    • 2.String实现
      • 2.1.不可变如何实现
    • 3.可变的字符串

基于Oracle JDK11和JVM 11

1.String1简介

Java数据类型分两种:基本数据类型和引用数据类型。关于基本数据类型,可以参考我这篇博文Java极简入门教程——3.基本数据类型和变量_编程还未的博客-CSDN博客

除了八个基本数据类型之外,其他的都是引用数据类型。八个基本数据类型中,并没有包含字符串。Java提供了一个类,来储存字符串类型的值。自然而然,类叫 String。

String类表示字符串。Java程序中的所有字符串文本(例如“Hello World”)都是String类的实例(实例既对象)。

String有几个特点:

  1. String类是不可变的;它的值在创建后无法改变。
  2. 针对常量池的优化。
  3. final修饰
  4. javac 编译器可以根据 JDK 版本使用 StringBuffer、StringBuilder 或 java.lang.invoke.StringConcatFactory 实现运算符(+)

前三个比较重要。

String使用有两种方式,隐式创建和显示创建。

隐式创建

String abc = "张三";

显示创建

String abc = new String("李四");
1.1.验证String是不可变对象

学习一个方法System.identityHashCode(),打印对象的hash码值,这个值和hashCode()返回的一样。

先看一小段代码

String abc = "张三";
System.out.println(abc);
abc = "李四";
System.out.println(abc);

输出结果:

张三
李四

对于这段代码,会不会觉得String对象从“张三”变成了“李四”?有过有此疑问的话。我再放一点代码,验证一下。

String abc = "张三";
System.out.println(abc);

//System.identityHashCode 打印对象的hash码值
System.out.println(System.identityHashCode(abc));

abc = "李四";
System.out.println(abc);

//System.identityHashCode 打印对象的hash码值
System.out.println(System.identityHashCode(abc));

//打印第二遍是为了证明同一个对象的hash码值不会因为多次打印而改变。
System.out.println(System.identityHashCode(abc));

结果如下:

张三
1915910607
李四
284720968
284720968

是不是很震惊。abc指向的对象不一样了。这就是Java中String的特性,不可变对象。

我们解释一下刚才的代码,写在注释里面

//创建一个String对象,字符串文本值是“张三”,
//声明String类型的引用abc,指向值是“张三”的String对象
String abc = "张三";
System.out.println(abc);

//引用指向 值是“李四”的对象。之前值是“张三”的对象并没改变,
//而是创建了一个新对象,abc指向了新对象
abc = "李四";
System.out.println(abc);

注释看完后,这两行代码代码在内存中的变化看下图。关于JVM运行时数据区不太懂的,可以参考我这篇博文面试必备——图文剖析JVM的运行时数据区_编程还未的博客-CSDN博客

为了更好的理解,把我关于JVM运行时数据区的部分内容,贴过来。

Java会为每个类和接口维护一个运行时常量池(run-time constant pool )。类或接口的二进制表示形式(字节码)中的常量池表用于在创建类或接口时构造运行时常量池。

运行时常量池中所有引用最初都是符号引用。这些符号按照如下方式,从类或接口的二进制表示形式(字节码)中得出:

其中一项是字符串常量。字符串常量是指向String类实例的引用,它来自于类或接口二进制表示(字节码)中的CONSTANT_String_info结构。CONSTANT_String_info结构给出了由Unicode码点序列组成的字符串常量。Java语言规定,相同的字符串常量必须指向同一个String实例。2

下面两项是正确的:

  1. 如果以前在包含与结构给定的相同的 Unicode 码位序列的类实例上调用了该方法,则字符串常量是同一类实例的一个。
  2. 否则,将创建一个新的类实例,其中包含结构给出的 Unicode 码位序列。字符串常量获取的结果是指向那个新String实例的引用。最后,新String实例的intern方法被虚拟机自动调用。

原文如下:

  • A string constant is a reference to an instance of class String, and is derived from a CONSTANT_String_info structure. To derive a string constant, the Java Virtual Machine examines the sequence of code points given by the CONSTANT_String_info structure:
    • If the method String.intern has previously been invoked on an instance of class String containing a sequence of Unicode code points identical to that given by the CONSTANT_String_info structure, then the string constant is a reference to that same instance of class String.
    • Otherwise, a new instance of class String is created containing the sequence of Unicode code points given by the CONSTANT_String_info structure. The string constant is a reference to the new instance. Finally, the method String.intern is invoked on the new instance.

总结一下上面的内容,得到两点:

  1. 运行时常量池中有字符串常量,字符串常量是指向字符串实例的引用。
  2. 相同的字符串常量必须指向同一个String实例。

基于上面两点,以及对象在堆中(不了解这个的,可以看一下我的博文面试必备——图文剖析JVM的运行时数据区_编程还未的博客-CSDN博客)。

【字符串常量池在堆中】这个观点,在JVM规范中并无体现。至于HotSpot有没有用【堆中的字符串常量池】实现上述总结的两点,不确定。

我们画一下String的内存示意:

String abc = "张三";

abc = "李四";

由此可以看出,“张三”对象还在,abc指向了“李四”对象。

内存图+hashCode码值,为String类是不可变的; 提供支持。

1.2.两种创建字符串对象的区别

Java的String有两种创建对象的方式。

第一种

String abc = "张三";

第二种

String cba = new Stgring("李四");

第一种,会在方法区的运行时常量池的字符串常量中寻找,是否有对象“张三”的引用,有就直接指向;没有就在对创建对象“张三”,把“张三”对象的引用保存到运行时常量池的字符串常量中,abc再指向字符串常量中的引用。

第二种,堆创建对象,检查运行时常量池的字符串常量,有没有指向“李四”对象的引用。如果有,刚才new String()创建的对象,指向字符串常量中“李四”的引用;如果没有,就在堆中创建“李四”对象,“李四”对象的引用保存到字符串常量中,cba指向字符串常量。

放两种情况的字节码,注释放在字节码中。字节码指令官方3

第一种字节码

//String abc = "张三";

LINENUMBER 9 L0
LDC "\u5f20\u4e09"
ASTORE 1

第一种字节码带注释

//String abc = "张三";

//源码的行数 第9行
LINENUMBER 9 L0
//LDC 从运行时常量池推送到顶(Push item from run-time constant pool),如果是字符串常量,则把值推出来
//"\u5f20\u4e09"是“张三”的Unicode
LDC "\u5f20\u4e09"
//ASTORE 存储到局部变量中(Store into local variable reference)     
ASTORE 1

第二种字节码

//String cba = new Stgring("李四");

LINENUMBER 11 L0
NEW java/lang/String
DUP
LDC "\u674e\u56db"
INVOKESPECIAL java/lang/String. (Ljava/lang/String;)V
ASTORE 1

第二种字节码带注释

//String cba = new Stgring("李四");

//源码的行数 第11行
LINENUMBER 11 L0
//NEW 创建对象(Create new object)    
NEW java/lang/String
//DUP 复制顶部 *** 作数堆栈值(Duplicate the top operand stack value)    
DUP
//LDC 从运行时常量池推送到顶(Push item from run-time constant pool),如果是字符串常量,则把值推出来
//"\u674e\u56db"是“李四”的Unicode    
LDC "\u674e\u56db"
//INVOKESPECIAL 调用实例方法;直接调用实例初始化方法和当前类及其超类型的方法
//(Invoke instance method; direct invocation of instance initialization methods and methods of the current class and its supertypes)    
INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
//ASTORE 存储到局部变量中(Store into local variable reference)     
ASTORE 1

第一种的内存示意图

String abc = "张三";

第二种的内存示意图

1.3.创建了几个对象 1.3.1.情况1
String abc = "张三";

0个对象或1个对象。

  1. 0个对象:如果字符串常量池中有“张三”的话,abc直接指向字符串常量池的“张三”;
  2. 1个对象:如果字符串常量池中没“张三”的话,先创建对象,对象存储的内容是“张三”,把“张三”放到字符串常量池。

内存情况,比较简单跟上面的图一样,不重复了。

1.3.2.情况2
String abc = "张三";
String def = "张三";
//打印 abc 的hash码值
System.out.println(System.identityHashCode(abc));
//打印 def 的hash码值
System.out.println(System.identityHashCode(def));

0个对象或1个对象。分析同情况1。

这里只分析String abc = "张三";就可以了。程序的运行结果如下:

1915910607
1915910607

abc的hash码值和def的hash码值是一样的,能证明指向的是同一个对象。

或者用比较也可以,结果是true。==比较对象的地址值,equals比较的是字面量。

String abc = "张三";
String def = "张三";
System.out.println(abc==def);

为什么出现这种情况?def是在干嘛?

JVM的机制,会把String的值,比如“张三”保存到运行时常量池的字符串常量中,方便后续使用,而不重复创建对象,节省开销。这个在上面也讲过了,是Java语言规范的规定。

def发现字符串常量中有指向“张三”对象的引用,直接指向字符串常量。内存情况如下

1.3.3.情况3
String abc = "张三";
String hig = new String("张三");
System.out.println(System.identityHashCode(abc));
System.out.println(System.identityHashCode(hig));

输出结果是如下

1915910607
284720968

说明abc和hig指向的不同的对象。

abc那行代码:0个或1个,分析跟上面情况1一样。

hig那行代码:1个对象。new String()肯定会创建1个对象,因为已有“张三”对象的引用在运行时常量池的字符串常量中,new String()会保存字符串常量的引用。

内存分析如下:

1.3.4.情况4
String abc = "张三";
String klm = new String("李四");
System.out.println(System.identityHashCode(abc));
System.out.println(System.identityHashCode(klm));

运行结果

1915910607
284720968

abc那行代码:0个或1个,分析同情况1

klm那行代码:1个或2个。new String()肯定会创建1个对象。“李四”如果在运行时常量池的字符串常量中,就不会在新创建对象了;没有的话就创个对象,把引用存到运行时常量池的字符串常量中。最后都是字符串常量中的引用保存到刚new String()的对象中。所以是1个或2个。

内存分析如下:

2.String实现

这是Java中最常用的一个类,如果看过Java内存的话,会发现占用内存最多的就是char[](Java8及之前)或者byte[](Java8及之后),这也能看出String的重要性。怎么看出来的呢?

看下面源码:

    /**
     * The value is used for character storage.
     *
     * @implNote This field is trusted by the VM, and is a subject to
     * constant folding if String instance is constant. Overwriting this
     * field after construction will cause problems.
     *
     * Additionally, it is marked with {@link Stable} to trust the contents
     * of the array. No other facility in JDK provides this functionality (yet).
     * {@link Stable} is safe here, because value is never null.
     */
    @Stable
    private final byte[] value;
    /**
     * Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument; in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.
     *
     * @param  original
     *         A {@code String}
     */
    @HotSpotIntrinsicCandidate
    public String(String original) {
        this.value = original.value;
        this.coder = original.coder;
        this.hash = original.hash;
    }
  1. private final byte[] value;用来存储。
  2. public String(String original) {}构造方法,也是把值存到value。

因为Java中使用是String是最多的,String底层是byte[],所以通常JVM内存镜像中,byte[]占用最多内存。

2.1.不可变如何实现

先讲一下final4。这里只提两点:

  1. 修饰属性:只能在初始化时赋值,且只能赋值一次,赋值后值无法改变。
  2. 修饰类:被修饰的类无法被继承。

上面提到过,String底层用private final byte[] value来存值,这个value是byte[]数组,初始化一次后不可再变;且并没有提供方法类修改这个byte[]内部的值;private修饰,只能在本类中使用;final修饰不可被继承,没子类重写里面的方法。

最主要的是:初始化时赋值一次,不提供任何修改底层存储值的方法。

3.可变的字符串

要想使用可变字符串,Java提供了StringBuilderStringBuffer,可以使用这个类。

这俩类的主要区别是:StringBuffer是线程安全的、速度慢一些,StringBuilder是非线程安全的,速度快一些。


  1. 引用资料:Java11语言规范、JVM11规范、JDK11源码 ↩︎

  2. String Literals ↩︎

  3. Instructions ↩︎

  4. final ↩︎

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/733468.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-27
下一篇 2022-04-27

发表评论

登录后才能评论

评论列表(0条)

保存