Jdk源码分析

文章40 |   阅读 10217 |   点赞0

来源:https://yumbo.blog.csdn.net/category_10384063.html

运行时常量池的一道面试题(jdk8环境)

x33g5p2x  于2021-12-18 转载在 其他  
字(9.8k)|赞(0)|评价(0)|浏览(267)

事先说明环境 在jdk8下,高版本的jdk可能找不到对应的Version类

代码:

public class TestDemo {
    
    @Test
    public void test01() {
        //
        String str1 = new StringBuilder("hello").append("World").toString();
        System.out.println(str1.intern());
        System.out.println(str1 == str1.intern());

        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern());
        System.out.println(str2 == str2.intern());

        String str3 = new StringBuilder("hello").toString();
        System.out.println(str3.intern());
        System.out.println(str3 == str3.intern());
    }
    
}
请写出控制台中打印的结果?

答案

helloWorld
true
java
false
hello
false
你答对了吗?

为什么会出现这种答案呢?

== 运算,对于非基本类型比较的是内存地址值,要知道这道题的输出的结果,最简单的方法就是知道对象:str1、str1.intern()、str2、str2.intern()、str3、str3.intern()的内存地址值就能判断输出是 true还是false。但这是jvm极力要遏制的,因此我是无法得知内存地址的,当然如果通过其它方式是可以做到的,但是jvm是没有这个api的,下面我们通过hashCode侧面排除一些情况,如果hash值不同,肯定不是同一个对象,hash值相同可能不是同一个对象。

hash值相同不能确认是同一个对象,但是能得知不同则一定不是同一个对象。也就是下面"java"。str1和str2是两个不同的对象,str1来自堆的Eden区中,而str2则是Old区的字符串常量池中。

public class GCTest {

    public static void main(String[] args) {

        String str1 = new StringBuilder("ja").append("va").toString();
        String str2 = str1.intern();
        System.out.println(str1==str2);
        System.out.println(System.identityHashCode(str1)); // 通过System提供的方法得到hash值,打印hash值相当于内存地址,hashCode方法不完全等于
        System.out.println(System.identityHashCode(str2)); // 打印地址

        String str3 = new StringBuilder("hello").append("world").toString();
        String str4 = str3.intern();
        System.out.println(str3==str4);
        System.out.println(System.identityHashCode(str3));
        System.out.println(System.identityHashCode(str4));

    }
}

得到下面一组打印输出,同一个对象的hash值肯定是相同的,而下面str1和str2的hash值不同肯定不是同一个对象(注意不能调用String重写的hashCode(),我们要调用Object提供的native修饰的hashCode()或者利用System.identityHashCode()得到hash值(这种情况就相当于是内存地址)

false		  // str1和str2不是同一个对象
460141958     // 堆空间创建的那个对象 ”java“
1163157884	  // 字符串常量池中的字符串对象
true		  // 说明str3和str4是同一个对象
1956725890	  // 堆中创建的字符串对象 ”helloworld“
1956725890	  // 堆中创建的字符串对象 ”helloworld“

先讲一讲前面2行的由来

helloWorld
true

我们都知道String类有一个intern()方法,它的作用就是将字符串存入常量池中,并且方法执行完后将这个字符串对象返回。

不难理解第一次打印前在常量池中没有helloWorld字符串,因此会将这个对象存入常量池中。然后返回字符串打印了第一个helloWorld字符串。
在比较==的 intern()方法返回的是常量池中的字符串对象(也是前面创建的对象,两个对象是同一个对象),所以返回了第一个true

为了证明,我们可以通过javap -v TestDemo.class命令将字节码文件反编译得到如下字节码

字节码指令集 如果字节码指令不太了解请先看这篇文章

下面请阅读一遍:
  字节码中分常量池、方法test01是我们重点关注的地方,首先注意到常量池中已经有了hello、World、ja、va字符串。
  原因在于我们代码中使用String str1 = new StringBuilder("hello").append("World").toString();这里面"hello"这类就是常量,接着我们直接读test01方法

public class com.example.demo.test.TestDemo
  minor version: 0
  major version: 58
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #47                         // com/example/demo/test/TestDemo
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // java/lang/StringBuilder
   #8 = Utf8               java/lang/StringBuilder
   #9 = String             #10            // 常量"hello"字符串
  #10 = Utf8               hello
  #11 = Methodref          #7.#12         // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
  #12 = NameAndType        #5:#13         // "<init>":(Ljava/lang/String;)V
  #13 = Utf8               (Ljava/lang/String;)V
  #14 = String             #15            // 常量"World"字符串
  #15 = Utf8               World
  #16 = Methodref          #7.#17         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #17 = NameAndType        #18:#19        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #18 = Utf8               append
  #19 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #20 = Methodref          #7.#21         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #21 = NameAndType        #22:#23        // toString:()Ljava/lang/String;
  #22 = Utf8               toString
  #23 = Utf8               ()Ljava/lang/String;
  #24 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
  #25 = Class              #27            // java/lang/System
  #26 = NameAndType        #28:#29        // out:Ljava/io/PrintStream;
  #27 = Utf8               java/lang/System
  #28 = Utf8               out
  #29 = Utf8               Ljava/io/PrintStream;
  #30 = Methodref          #31.#32        // java/lang/String.intern:()Ljava/lang/String;
  #31 = Class              #33            // java/lang/String
  #32 = NameAndType        #34:#23        // intern:()Ljava/lang/String;
  #33 = Utf8               java/lang/String
  #34 = Utf8               intern
  #35 = Methodref          #36.#37        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #36 = Class              #38            // java/io/PrintStream
  #37 = NameAndType        #39:#13        // println:(Ljava/lang/String;)V
  #38 = Utf8               java/io/PrintStream
  #39 = Utf8               println
  #40 = Methodref          #36.#41        // java/io/PrintStream.println:(Z)V
  #41 = NameAndType        #39:#42        // println:(Z)V
  #42 = Utf8               (Z)V
  #43 = String             #44            // 常量"ja"字符串
  #44 = Utf8               ja
  #45 = String             #46            // 常量"va"字符串
  #46 = Utf8               va
  #47 = Class              #48            // com/example/demo/test/TestDemo
  #48 = Utf8               com/example/demo/test/TestDemo
  #49 = Utf8               Code
  #50 = Utf8               LineNumberTable
  #51 = Utf8               LocalVariableTable
  #52 = Utf8               this
  #53 = Utf8               Lcom/example/demo/test/TestDemo;
  #54 = Utf8               test01
  #55 = Utf8               str1
  #56 = Utf8               Ljava/lang/String;
  #57 = Utf8               str2
  #58 = Utf8               str3
  #59 = Utf8               StackMapTable
  #60 = Utf8               RuntimeVisibleAnnotations
  #61 = Utf8               Lorg/junit/Test;
  #62 = Utf8               SourceFile
  #63 = Utf8               TestDemo.java
{
  public com.example.demo.test.TestDemo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/example/demo/test/TestDemo;

  public void test01();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=1
         0: new           #7                  // 创建 java/lang/StringBuilder 对象
         3: dup
         4: ldc           #9                  // 入栈常量池(#9)中的字符串常量hello
         6: invokespecial #11                 // 实例初始化将hello传入
         9: ldc           #14                 // 入栈常量池(在#14)中的字符串常量World
        11: invokevirtual #16                 // StringBuilder.append方法调用传入World
        14: invokevirtual #20                 // 调用StringBuilder.toString方法
        17: astore_1						  // 将返回的值的地址引用存入到局部变量1
        18: getstatic     #24                 // 获取打印流
        21: aload_1							  // 将局部变量1装载成引用类型,也就是"helloWorld"
        22: invokevirtual #30                 // "helloWorld"调用String.intern:()方法
        25: invokevirtual #35                 // 调用打印流,打印"helloWorld"
        28: getstatic     #24                 // 获取打印流
        31: aload_1						      // 加载局部变量1 也就是"helloWorld"
        32: aload_1                           // 加载局部变量1 "helloWorld"
        33: invokevirtual #30                 // "helloWorld"调用intern方法
        36: if_acmpne     43                  // 如果条件满足就转执行43 的iconst_0也就是将0入栈
        39: iconst_1						  // 将int类型常量值1压入栈
        40: goto          44                  // 无条件转移到44
        43: iconst_0						  // 将0入栈
        44: invokevirtual #40                 // 调用方法传入也就是“ja"(对应常量池的#44)
        47: new           #7                  // 创建StringBuilder对象
        50: dup
        51: ldc           #43                 // 加载字符串"ja"
        53: invokespecial #11                 // 实例化StringBuilder
        56: ldc           #45                 // 加载字符串"va"
        58: invokevirtual #16                 // 调用append方法
        61: invokevirtual #20                 // 调用toString方法
        64: astore_2 						  // 将toString的结果存入局部变量2中
        65: getstatic     #24                 // 获取打印流
        68: aload_2          				  // 加载局部变量2的值也就是加载"java"
        69: invokevirtual #30                 // 调用intern方法
        72: invokevirtual #35                 // 打印"java"
        75: getstatic     #24                 // 获取打印流
        78: aload_2							  // 加载局部变量2"java"
        79: aload_2							  // 加载局部变量2"java"
        80: invokevirtual #30                 // 调用intern方法
        83: if_acmpne     90				  // 如果条件成立跳转到90也就是将0入栈
        86: iconst_1 						  // 将常量值1入栈
        87: goto          91 				  // 无条件跳转到91
        90: iconst_0						  // 将常量值0入栈
        91: invokevirtual #40                 // 打印
        94: new           #7                  // class java/lang/StringBuilder
        97: dup
        98: ldc           #9                  // String hello
       100: invokespecial #11                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
       103: invokevirtual #20                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
       106: astore_3
       107: getstatic     #24                 // Field java/lang/System.out:Ljava/io/PrintStream;
       110: aload_3
       111: invokevirtual #30                 // Method java/lang/String.intern:()Ljava/lang/String;
       114: invokevirtual #35                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       117: getstatic     #24                 // Field java/lang/System.out:Ljava/io/PrintStream;
       120: aload_3
       121: aload_3
       122: invokevirtual #30                 // Method java/lang/String.intern:()Ljava/lang/String;
       125: if_acmpne     132
       128: iconst_1
       129: goto          133
       132: iconst_0
       133: invokevirtual #40                 // Method java/io/PrintStream.println:(Z)V
       136: return
      LineNumberTable:
        line 13: 0
        line 14: 18
        line 15: 28
        line 17: 47
        line 18: 65
        line 19: 75
        line 21: 94
        line 22: 107
        line 23: 117
        line 24: 136
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0     137     0  this   Lcom/example/demo/test/TestDemo;
           18     119     1  str1   Ljava/lang/String;
           65      72     2  str2   Ljava/lang/String;
          107      30     3  str3   Ljava/lang/String;
      StackMapTable: number_of_entries = 6
        frame_type = 255 /* full_frame */
          offset_delta = 43
          locals = [ class com/example/demo/test/TestDemo, class java/lang/String ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class com/example/demo/test/TestDemo, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
        frame_type = 255 /* full_frame */
          offset_delta = 45
          locals = [ class com/example/demo/test/TestDemo, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class com/example/demo/test/TestDemo, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
        frame_type = 255 /* full_frame */
          offset_delta = 40
          locals = [ class com/example/demo/test/TestDemo, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class com/example/demo/test/TestDemo, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
    RuntimeVisibleAnnotations:
      0: #61()
        org.junit.Test
}
SourceFile: "TestDemo.java"

继续向下讲解:

java
false

会发现和上面一样的流程操作,为什么前面会返回true后面返回false。为什么会出现这种情况呢?

通过阅读字节码,会发现"helloWorld"、"java"他们的字节码指令逻辑顺序是一模一样的,为何这里是false呢?

原因在于"java"在类加载机制过程中执行了System类的System.initializeSystemClass()方法,在方法中调用了sun.misc.Version.init();就将"java"已经加载到常量池中了(字节码中的常量池只是class文件常量池不代码jvm环境的所有常量池,而运行时常量池中已经存在"java"字符串),
部分截图如下,会发现"java"常量已经被使用了。

  故,判断语句变成了,new创建出来的"java"对象,与运行时常量池中的"java"是两个不同的对象,因此返回false。

反过来说明,前面之所以返回true的原因在于new创建好"helloWorld"后,当调用intern方法时,不会重新创建一个新的"helloWorld",而是会将创建好的"helloWorld"存入运行时常量池中,此时intern()实际上只是做了一个地址引用(这个结论应该不正确,因为gc也会导致对象移动,我估摸着是真正的将对象移动到了Old区的字符串常量池,关于字符串常量的位置见文章:java8以后字符串常量池的位置,以及元空间的探秘,使用VisualVM进行实战验证)。因此当判断str1 == str1.intern()时,他们的地址是同一个地址,也是同一个对象,因此返回true。

总结:这道题目的前面两问区别在于"java"是在程序运行时就已经在运行时常量了,而其它字符串则没有,因此出现不同的结果,同理类似于"java"这类字符串常量的应该还有一些在某些类中有定义。

有了前面的基础后,后面一问
hello
false

不难解释,str3是通过new StringBuilder().toString();创建出来的对象,因此是一个全新的"hello"字符串对象,而调用.intern()方法后,返回的则是常量池中的"hello",两者明显不是同一个对象,因此返回false。

文章末尾更新一下我最近写的更详细的博客(保证能搞懂,为什么会返回true、false、false

运行时常量池的再深入,从jvm的角度谈谈这道字符串常量池的面试题。

相关文章