乐趣区

关于java:超过1W字深度剖析JVM常量池全网最详细最有深度

面试题:String a = “ab”; String b = “a” + “b”; a == b 是否相等

面试考察点

考查目标:考查对 JVM 基础知识的了解,波及到常量池、JVM 运行时数据区等。

考查范畴:工作 2 到 5 年。

背景常识

要答复这个问题,须要搞明确两个最根本的问题

  1. String a=“ab”,在 JVM 中产生了什么?
  2. String b=“a”+“b”,底层是如何实现?

JVM 的运行时数据

首先,咱们一起来温习一下 JVM 的运行时数据区。

为了让大家有一个全局的视角,我从类加载,到 JVM 运行时数据区的整体构造画进去,如下图所示。

对于每一个区域的作用,在我之前的面试系列文章中有具体阐明,这里就不做复述了。

在上图中,咱们须要重点关注几个类容:

  1. 字符串常量池
  2. 封装类常量池
  3. 运行时常量池
  4. JIT 编译器

这些内容都和本次面试题有十分大的关联关系,这里对于常量池局部的内容,先保留一个疑难,先追随我来学习一下 JVM 中的常量池。

JVM 中都有哪些常量池

大家常常会听到各种常量池,然而又不晓得这些常量池到底存储在哪里,因而会有很多的疑难:JVM 中到底有哪些常量池?

JVM 中的常量池能够分成以下几类:

  1. Class 文件常量池
  2. 全局字符串常量池
  3. 运行时常量池

Class 文件常量池

每个 Class 文件的字节码中都有一个常量池,外面次要寄存编译器生成的各种字面量和符号援用。为了更直观的了解,咱们编写上面这个程序。

public class StringExample {
    private int value = 1;
    public final static int fs=101;

    public static void main(String[] args) {
        String a="ab";
        String b="a"+"b";
        String c=a+b;
    }
}

上述程序编译后,通过 javap -v StringExample.class 查看该类的字节码文件,截取局部内容如下。

Constant pool:
   #1 = Methodref          #9.#32         // java/lang/Object."<init>":()V
   #2 = Fieldref           #8.#33         // org/example/cl07/StringExample.value:I
   #3 = String             #34            // ab
   #4 = Class              #35            // java/lang/StringBuilder
   #5 = Methodref          #4.#32         // java/lang/StringBuilder."<init>":()V
   #6 = Methodref          #4.#36         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StrvalueingBuilder;
   #7 = Methodref          #4.#37         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #8 = Class              #38            // org/example/cl07/StringExample
   #9 = Class              #39            // java/lang/Object
  #10 = Utf8               value
  #11 = Utf8               I
  #12 = Utf8               fs
  #13 = Utf8               ConstantValue
  #14 = Integer            101
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lorg/example/cl07/StringExample;
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               a
  #27 = Utf8               Ljava/lang/String;
  #28 = Utf8               b
  #29 = Utf8               c
  #30 = Utf8               SourceFile
  #31 = Utf8               StringExample.java
  #32 = NameAndType        #15:#16        // "<init>":()V
  #33 = NameAndType        #10:#11        // value:I
  #34 = Utf8               ab
  #35 = Utf8               java/lang/StringBuilder
  #36 = NameAndType        #40:#41        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #37 = NameAndType        #42:#43        // toString:()Ljava/lang/String;
  #38 = Utf8               org/example/cl07/StringExample
  #39 = Utf8               java/lang/Object
  #40 = Utf8               append
  #41 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = Utf8               toString
  #43 = Utf8               ()Ljava/lang/String;

咱们关注一下 Constant pool 形容的局部,示意 Class 文件的常量池。在该常量池中次要寄存两类常量。

  1. 字面量。
  2. 符号援用。

字面量

  • 字面量,给根本类型变量赋值的形式就叫做字面量或者字面值。比方:String a=“b”,这里“b”就是字符串字面量,同样类推还有整数字面值、浮点类型字面量、字符字面量。

    在上述代码中,字面量常量的字节码为:

    #3 = String             #34            // ab
    #26 = Utf8               a
    #34 = Utf8               ab
  • final 润饰的成员变量、动态变量、实例变量、局部变量,比方:

      #11 = Utf8               I
      #12 = Utf8               fs
      #13 = Utf8               ConstantValue
      #14 = Integer            101

从下面的字节码来看,字面量和 final 润饰的属性是保留在常量池中,这些存在于常量池的字面量,指得是数据的值,比方ab101

对于根本数据类型,比方 private int value=1,在常量池中只保留了他的 字段描述符(I) 字段名称(value),它的字面量不会存在与常量池。

  #10 = Utf8               value
  #11 = Utf8               I

另外,对于 String c=a+b;c 这个属性的值也没有保留到常量池,因为在编译期间,ab 的值时不确定的。

#29 = Utf8               c
#35 = Utf8               java/lang/StringBuilder
#36 = NameAndType        #40:#41        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#37 = NameAndType        #42:#43        // toString:()Ljava/lang/String;
#39 = Utf8               java/lang/Object
#40 = Utf8               append
#41 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;

如果,咱们把代码批改成上面这种模式

public static void main(String[] args) {
  final String a="ab";
  final String b="a"+"b";
  String c=a+b;
}

从新生成字节码之后,能够看到字节码产生了变动,c这个属性的值 abab 也保留到了常量池中。

#26 = Utf8               c
#27 = Utf8               SourceFile
#28 = Utf8               StringExample.java
#29 = NameAndType        #12:#13        // "<init>":()V
#30 = NameAndType        #7:#8          // value:I
#31 = Utf8               ab
#32 = Utf8               abab

符号援用

符号援用 次要设波及编译原理方面的概念,包含上面三类常量:

  1. 类和接口的全限定名(Full Qualified Name),也就是Ljava/lang/String;,次要用于在运行时解析失去类的间接援用。

      #23 = Utf8               ([Ljava/lang/String;)V
      #25 = Utf8               [Ljava/lang/String;
      #27 = Utf8               Ljava/lang/String;
  2. 字段的名称和描述符 (Descriptor),字段也就是类或者接口中申明的 变量 ,包含 类级别变量 (static)实例级的变量

    #1 = Methodref          #9.#32         // java/lang/Object."<init>":()V
    #2 = Fieldref           #8.#33         // org/example/cl07/StringExample.value:I
    #3 = String             #34            // ab
    #4 = Class              #35            // java/lang/StringBuilder
    #5 = Methodref          #4.#32         // java/lang/StringBuilder."<init>":()V
    #6 = Methodref          #4.#36         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StrvalueingBuilder;
    #7 = Methodref          #4.#37         // java/lang/StringBuilder.toString:()Ljava/lang/String;
    #8 = Class              #38            // org/example/cl07/StringExample
      
    #24 = Utf8               args
    #26 = Utf8               a
    #28 = Utf8               b
    #29 = Utf8               c
  3. 办法的名称和描述符 , 办法的形容相似于 JNI 动静注册时的“办法签名”,也就是 参数类型 + 返回值类型 ,比方上面的这种字节码,示意main 办法和 String 返回类型。

      #19 = Utf8               main
      #20 = Utf8               ([Ljava/lang/String;)V

小结:在 Class 文件中,存在着一些不会发生变化的货色,比方一个类的名字、类的字段名字 / 所属数据类型、办法名称 / 返回类型 / 参数名、常量、字面量等。这些在 JVM 解释执行程序的时候十分重要,所以编译器将源代码编译成 class 文件之后,会用一部分字节分类存储这些不变的代码,而这些字节咱们就称为常量池。

运行时常量池

运行时常量池是每一个类或者接口的常量池(Constant Pool)的运行时的表现形式。

咱们晓得,一个类的加载过程,会通过:加载 连贯(验证、筹备、解析)初始化 的过程,而在类加载这个阶段,须要做以下几件事件:

  1. 通过一个类的全类限定名获取此类的二进制字节流。
  2. 在堆内存生成一个 java.lang.Class 对象, 代表加载这个类, 做为这个类的入口。
  3. class 字节流的动态存储构造转化成办法区(元空间)的运行时数据结构。

而其中第三点,将 class 字节流代表的动态贮存构造转化为办法区的运行时数据结构 这个过程,就蕴含了 class 文件常量池进入运行时常量池的过程。

所以,运行时常量池的作用是存储 class 文件常量池中的符号信息,在类的解析阶段会把这些符号援用转换成间接援用 (实例对象的内存地址), 翻译进去的间接援用也是存储在运行时常量池中。class 文件常量池的大部分数据会被加载到运行时常量池。

运行时常量池保留在办法区(JDK1.8 元空间)中,它是全局共享的,不同的类共用一个运行时常量池。

另外,运行时常量池具备动态性的特色,它的内容并不是全副起源与编译后的 class 文件,在运行时也能够通过代码生成常量并放入运行时常量池。比方 String.intern() 办法。

字符串常量池

字符串常量池,简略来说就是专门针对 String 类型设计的常量池。

字符串常量池的罕用创立形式有两种。

String a="Hello";
String b=new String("Mic");
  1. a这个变量,是在编译期间就曾经确定的,会进入到字符串常量池。
  2. b这个变量,是通过 new 关键字实例化,new是创立一个对象实例并初始化该实例,因而这个字符串对象是在运行时能力确定的,创立的实例在堆空间上。

字符串常量池存储在堆内存空间中,创立模式如下图所示。

当应用 String a=“Hello” 这种形式创立字符串对象时,JVM 首先会先查看该字符串对象是否存在与字符串常量池中,如果存在,则间接返回常量池中该字符串的援用。否则,会在常量池中创立一个新的字符串,并返回常量池中该字符串的援用。(这种形式能够缩小同一个字符串被反复创立,节约内存,这也是享元模式的体现)。

如下图所示,如果再通过 String c=“Hello” 创立一个字符串,发现常量池曾经存在了 Hello 这个字符串,则间接把该字符串的援用返回即可。(String 外面的享元模式设计)

当应用 String b=new String(“Mic”) 这种形式创立字符串对象时,因为 String 自身的不可变性(后续剖析),因而在 JVM 编译过程中,会把 Mic 放入到 Class 文件的常量池中,在类加载时,会在字符串常量池中创立 Mic 这个字符串。接着应用 new 关键字,在堆内存中创立一个 String 对象并指向常量池中 Mic 字符串的援用。

如下图所示,如果再通过 new String(“Mic”) 创立一个字符串对象,此时因为字符串常量池曾经存在 Mic,所以只须要在堆内存中创立一个String 对象即可。

简略总结一下:JVM 之所以独自设计字符串常量池,是 JVM 为了进步性能以及缩小内存开销的一些优化:

  1. String 对象作为 Java 语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地应用字符串,能够晋升零碎的整体性能。
  2. 创立字符串常量时,首先查看字符串常量池是否存在该字符串,如果有,则间接返回该援用实例,不存在,则实例化该字符串放入常量池中。

字符串常量池是 JVM 所保护的一个字符串实例的援用表,在 HotSpot VM 中,它是一个叫做 StringTable 的全局表。在字符串常量池中保护的是字符串实例的援用,底层 C ++ 实现就是一个 Hashtable。这些被保护的援用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”!

封装类常量池

除了字符串常量池,Java 的根本类型的封装类大部分也都实现了常量池。包含Byte,Short,Integer,Long,Character,Boolean

留神,浮点数据类型 Float,Double 是没有常量池的。

封装类的常量池是在各自外部类中实现的,比方 IntegerCache(Integer 的外部类)。要留神的是,这些常量池是有范畴的:

  • Byte,Short,Integer,Long : [-128~127]
  • Character : [0~127]
  • Boolean : [True, False]

测试代码如下:

public static void main(String[] args) {
  Character a=129;
  Character b=129;
  Character c=120;
  Character d=120;
  System.out.println(a==b);
  System.out.println(c==d);
  System.out.println("...integer...");
  Integer i=100;
  Integer n=100;
  Integer t=290;
  Integer e=290;
  System.out.println(i==n);
  System.out.println(t==e);
}

运行后果:

false
true
...integer...
true
false

封装类的常量池,其实就是在各个封装类外面本人实现的缓存实例(并不是 JVM 虚拟机层面的实现),如在 Integer 中,存在 IntegerCache,提前缓存了 -128~127 之间的数据实例。意味着这个区间内的数据,都采纳同样的数据对象。这也是为什么下面的程序中,通过== 判断失去的后果为true

这种设计其实就是享元模式的利用。

private static class IntegerCache {
  static final int low = -128;
  static final int high;
  static final Integer cache[];

  static {
    // high value may be configured by property
    int h = 127;
    String integerCacheHighPropValue =
      sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    if (integerCacheHighPropValue != null) {
      try {int i = parseInt(integerCacheHighPropValue);
        i = Math.max(i, 127);
        // Maximum array size is Integer.MAX_VALUE
        h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
      } catch(NumberFormatException nfe) {// If the property cannot be parsed into an int, ignore it.}
    }
    high = h;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
      cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
  }

  private IntegerCache() {}
}

封装类常量池的设计初衷其实 String 雷同,也是针对频繁应用的数据区间进行缓存,防止频繁创建对象的内存开销。

对于字符串常量池的问题摸索

在上述常量池中,对于 String 字符串常量池的设计,还有很多问题须要摸索:

  1. 如果常量池中曾经存在某个字符串常量,后续定义雷同字符串的字面量时,是如何指向同一个字符串常量的援用?也就是上面这段代码的断言后果是true

    String a="Mic";
    String b="Mic";
    assert(a==b); //true
  2. 字符串常量池的容量到底有多大?
  3. 为什么要设计针对字符串独自设计一个常量池?

为什么要设计针对字符串独自设计一个常量池?

首先,咱们来看一下 String 的定义。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
}

从上述源码中能够发现。

  1. String 这个类是被 final 润饰的,代表该类无奈被继承。
  2. String 这个类的成员属性 value[] 也是被 final 润饰,代表该成员属性不可被批改。

因而 String 具备不可变的个性,也就是说 String 一旦被创立,就无奈更改。这么设计的益处有几个。

  1. 不便实现字符串常量池:在 Java 中,因为会大量的应用 String 常量,如果每一次申明一个 String 都创立一个 String 对象,那将会造成极大的空间资源的节约。Java 提出了 String pool 的概念,在堆中开拓一块存储空间 String pool,当初始化一个 String 变量时,如果该字符串曾经存在了,就不会去创立一个新的字符串变量,而是会返回曾经存在了的字符串的援用。如果字符串是可变的,某一个字符串变量扭转了其值,那么其指向的变量的值也会扭转,String pool 将不可能实现!
  2. 线程安全性,在并发场景下,多个线程同时读一个资源,是平安的,不会引发竞争,但对资源进行写操作时是不平安的,不可变对象不能被写,所以保障了多线程的平安。
  3. 保障 hash 属性值不会频繁变更。确保了唯一性,使得相似 HashMap 容器能力实现相应的 key-value 缓存性能,于是在创建对象时其 hashcode 就能够释怀的缓存了,不须要从新计算。这也就是 Map 喜爱将 String 作为 Key 的起因,处理速度要快过其它的键对象。所以 HashMap 中的键往往都应用 String。

留神,因为 String 的不可变性能够不便实现字符串常量池这一点很重要,这时实现字符串常量池的前提。

字符串常量池,其实就是享元模式的设计,它和在 JDK 中提供的 IntegerCache、以及 Character 等封装对象的缓存设计相似,只是 String 是 JVM 层面的实现。

字符串的调配,和其余的对象调配一样,消耗昂扬的工夫与空间代价。JVM 为了进步性能和缩小内存开销,在实例化字符串常量的时候进行了一些优化。为 了缩小在 JVM 中创立的字符串的数量,字符串类保护了一个字符串池,每当代码创立字符串常量时,JVM 会首先查看字符串常量池。如果字符串曾经存在池中,就返回池中的实例援用。如果字符串不在池中,就会实例化一个字符串并放到池中。Java 可能进行这样的优化是因为字符串是不可变的,能够不必放心数据抵触 进行共享。

咱们把字符串常量池当成是一个缓存,通过 双引号 定义一个字符串常量时,首先从字符串常量池中去查找,找到了就间接返回该字符串常量池的援用,否则就创立一个新的字符串常量放在常量池中。

常量池有多大呢?

我想大家肯定和我一样好奇,常量池到底能存储多少个常量?

后面咱们说过,常量池实质上是一个 hash 表,这个 hash 示意不可动静扩容的。也就意味着极有可能呈现单个 bucket 中的链表很长,导致性能升高。

在 JDK1.8 中,这个 hash 表的固定 Bucket 数量是 60013 个,咱们能够通过上面这个参数配置指定数量

-XX:StringTableSize=N

能够减少上面这个虚拟机参数,来打印常量池的数据。

-XX:+PrintStringTableStatistics

减少参数后,运行上面这段代码。

public class StringExample {
    private int value = 1;
    public final static int fs=101;

    public static void main(String[] args) {
        final String a="ab";
        final String b="a"+"b";
        String c=a+b;
    }
}

在 JVM 退出时,会打印常量池的应用状况如下:

SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     12192 =    292608 bytes, avg  24.000
Number of literals      :     12192 =    470416 bytes, avg  38.584
Total footprint         :           =    923112 bytes
Average bucket size     :     0.609
Variance of bucket size :     0.613
Std. dev. of bucket size:     0.783
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :       889 =     21336 bytes, avg  24.000
Number of literals      :       889 =     59984 bytes, avg  67.474
Total footprint         :           =    561424 bytes
Average bucket size     :     0.015
Variance of bucket size :     0.015
Std. dev. of bucket size:     0.122
Maximum bucket size     :         2

能够看到字符串常量池的总大小是60013,其中字面量是889

字面量是什么时候进入到字符串常量池的

字符串字面量,和其余根本类型的字面量或常量不同,并不会在类加载中的解析(resolve)阶段填充并驻留在字符串常量池中,而是以非凡的模式存储在 运行时常量池(Run-Time Constant Pool)中。而是只有当此字符串字面量被调用时(如对其执行 ldc 字节码指令,将其增加到栈顶),HotSpot VM 才会对其进行 resolve,为其在字符串常量池中创立对应的 String 实例。

具体来说,应该是在 执行 ldc 指令时(该指令示意 int、float 或 String 型常量从常量池推送至栈顶)

在 JDK1.8 的 HotSpot VM 中,这种未真正解析(resolve)的 String 字面量,被称为 pseudo-string,以 JVM_CONSTANT_String 的模式寄存在运行时常量池中,此时并未为其创立 String 实例。

在编译期,字符串字面量以 ”CONSTANT_String_info”+”CONSTANT_Utf8_info” 的模式寄存在 class 文件的 常量池(Constant Pool)中;

在类加载之后,字符串字面量以 ”JVM_CONSTANT_UnresolvedString(JDK1.7)” 或者 ”JVM_CONSTANT_String(JDK1.8)” 的模式寄存在 运行时常量池(Run-time Constant Pool)中;

在首次应用某个字符串字面量时,字符串字面量以真正的 String 对象的形式寄存在 字符串常量池(String Pool)中。

通过上面这段代码能够证实。

public static void main(String[] args) {String a =new String(new char[]{'a','b','c'});
  String b = a.intern();
  System.out.println(a == b);

  String x =new String("def");
  String y = x.intern();
  System.out.println(x == y);
}

应用 new char[]{‘a’,’b’,’c’} 构建的字符串,并没有在编译的时候应用常量池,而是在调用 a.intern() 时,将 abc 保留到常量池并返回该常量池的援用。

intern()办法

在 Integer 中的 valueOf 办法中,咱们能够看到,如果传递的值 i 是在 IntegerCache.lowIntegerCache.high范畴以内,则间接从 IntegerCache.cache 中返回缓存的实例对象。

public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)
    return IntegerCache.cache[i + (-IntegerCache.low)];
  return new Integer(i);
}

那么,在 String 类型中,既然存在字符串常量池,那么有没有办法可能实现相似于 IntegerCache 的性能呢?

答案是:intern()办法。因为字符串池是虚拟机层面的技术,所以在 String 的类定义中并没有相似 IntegerCache 这样的对象池,String类中提及缓存 / 池的概念只有 intern() 这个办法。

/**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
public native String intern();

这个办法的作用是:去拿 String 的内容去 Stringtable 里查表,如果存在,则返回援用,不存在,就把该对象的 ” 援用 ” 保留在 Stringtable 表里

比方上面这段程序:

public static void main(String[] args) {String str = new String("Hello World");
  String str1=str.intern();
  String str2 = "Hello World";
  System.out.print(str1 == str2);
}

运行的后果为:true。

实现逻辑如下图所示,str1通过调用 str.intern() 去常量池表中获取 Hello World 字符串的援用,接着 str2 通过字面量的模式申明一个字符串常量,因为此时 Hello World 曾经存在于字符串常量池中,所以同样返回该字符串常量 Hello World 的援用,使得 str1str2具备雷同的援用地址,从而运行后果为true

总结:intern 办法会从字符串常量池中查问以后字符串是否存在:

  • 若不存在就会将以后字符串放入常量池中,并返回当地字符串地址援用。
  • 如果存在就返回字符串常量池那个字符串地址。

留神,所有字符串字面量在初始化时,会默认调用 intern() 办法。

这段程序,之所以 a==b,是因为申明a 时,会通过 intern() 办法去字符串常量池中查找是否存在字符串 Hello,因为不存在,则会创立一个。同理,变量b 也同样如此,所以 b 在申明时,发现字符常量池中曾经存在 Hello 的字符串常量,所以间接返回该字符串常量的援用。

public static void main(String[] args) {
  String a="Hello";
  String b="Hello";
}

OK,学习到这里,是不是感觉本人懂了?我出一道题目来考考大家,上面这段程序的运行后果是什么?

public static void main(String[] args) {String a =new String(new char[]{'a','b','c'});
  String b = a.intern();
  System.out.println(a == b);

  String x =new String("def");
  String y = x.intern();
  System.out.println(x == y);
}

正确答案是:

true
false

第二个输入为 false 还能够了解,因为 new String(“def”) 会做两件事:

  1. 在字符串常量池中创立一个字符串def
  2. new关键字创立一个实例对象 string,并指向字符串常量池def 的援用。

x.intern(),是从字符串常量池获取def 的援用,他们的指向地址不同,我前面的内容还会具体解释。

第一个输入后果为 true 是为啥捏?

JDK 文档中对于 intern() 办法的阐明:当调用 intern 办法时,如果常量池(内置在 JVM 中的)中曾经蕴含雷同的字符串,则返回池中的字符串。否则,将此 String 对象增加到池中,并返回对该 String 对象的援用。

在构建 String a 的时候,应用 new char[]{‘a’,’b’,’c’} 初始化字符串时(不会主动调用 intern(),字符串采纳懒加载形式进入到常量池),并没有在字符串常量池中构建abc 这个字符串实例。所以当调用 a.intern() 办法时,会把该 String 对象增加到字符常量池中,并返回对该 String 对象的援用,所以 ab指向的援用地址是同一个。

问题答复

面试题:String a = “ab”; String b = “a” + “b”; a == b 是否相等

答复 a==b 是相等的,起因如下:

  1. 变量 ab都是常量字符串,其中 b 这个变量,在编译时,因为不存在可变动的因素,所以编译器会间接把变量 b 赋值为 ab(这个是属于编译器优化领域,也就是编译之后,b 会保留到 Class 常量池中的字面量)。
  2. 对于字符串常量,初始化 a 时,会在字符串常量池中创立一个字符串 ab 并返回该字符串常量池的援用。
  3. 对于变量 b,赋值ab 时,首先从字符串常量池中查找是否存在雷同的字符串,如果存在,则返回该字符串援用。
  4. 因而,a 和 b 所指向的援用是同一个,所以 a==b 成立。

问题总结

对于常量池局部的内容,要比拟深刻和全面的了解,还是须要花一些工夫的。

比方大家通过浏览下面的内容,认为对字符串常量池有一个十分深刻的了解,能够,咱们再来看一个问题:

public static void main(String[] args) {String str = new String("Hello World");
  String str1=str.intern();
  System.out.print(str == str1);
}

下面这段代码,很显然返回 false,起因如下图所示。很显著strstr1所指向的援用地址不是同一个。

然而咱们把上述代码革新一下:

public static void main(String[] args) {String str = new String("Hello World")+new String("!");
  String str1=str.intern();
  System.out.print(str == str1);
}

上述程序输入的后果变成了:true。为什么呢?

这里也是 JVM 编译器层面做的优化,因为 String 是不可变类型,所以实践上来说,上述程序的执行逻辑是:通过 + 进行字符串拼接时,相当于把原有的 String 变量指向的字符串常量 HelloWorld 取出来,加上另外一个 String 变量指向的字符串常量!,再生成一个新的对象。

假如咱们是通过 for 循环来对 String 变量进行拼接,那将会生成大量的对象,如果这些对象没有被及时回收,会造成十分大的内存节约。

所以 JVM 优化之后,其实是通过 StringBuilder 来进行拼接,也就是只会产生一个对象实例 StringBuilder,而后再通过append 办法来拼接。

为了证实我说的状况,来看一下上述代码的字节码。

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=3, args_size=1
         0: new           #3                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
         7: new           #5                  // class java/lang/String
        10: dup
        11: ldc           #6                  // String Hello World
        13: invokespecial #7                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        16: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: new           #5                  // class java/lang/String
        22: dup
        23: ldc           #9                  // String !
        25: invokespecial #7                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        28: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        31: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: astore_1
        35: aload_1
        36: invokevirtual #11                 // Method java/lang/String.intern:()Ljava/lang/String;
        39: astore_2
        40: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        43: aload_1
        44: aload_2
        45: if_acmpne     52
        48: iconst_1
        49: goto          53
        52: iconst_0
        53: invokevirtual #13                 // Method java/io/PrintStream.print:(Z)V
        56: return

从字节码中能够看到,构建了一个 StringBuilder,

 0: new           #3                  // class java/lang/StringBuilder

而后把字符串常量通过 append 办法进行拼接,最初调用 toString() 办法失去一个字符串常量。

16: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

因而,上述代码,等价于上面这种模式。

public static void main(String[] args) {StringBuilder sb=new StringBuilder().append(new String("Hello World")).append(new String("!"));
  String str=sb.toString();
  String str1=str.intern();
  System.out.print(str == str1);
}

所以,失去的后果是true

基于这个问题的变体还有很多,比方再来变一次,上面这段程序的运行后果是多少?

public static void main(String[] args) {
  String s1 = "a";
  String s2 = "b";
  String s3 = "ab";
  String s4 = s1 + s2;
  System.out.println(s3 == s4);
}

答案是false

因为上述程序等价于,s3s4 指向不同的地址援用,天然不相等。

public static void main(String[] args) {
  String s1 = "a";
  String s2 = "b";
  String s3 = "ab";
  StringBuilder sb=new StringBuilder().append(s1).append(s2);
  String s4 = sb.toString();
  System.out.println(s3 == s4);
}

总结:只有足够清晰的了解了字符串常量池相干的所有知识点,不论面试过程中如何变动,你都能精确答复,这就是常识的力量!
版权申明:本博客所有文章除特地申明外,均采纳 CC BY-NC-SA 4.0 许可协定。转载请注明来自 Mic 带你学架构
如果本篇文章对您有帮忙,还请帮忙点个关注和赞,您的保持是我一直创作的能源。欢送关注同名微信公众号获取更多技术干货!

退出移动版