了解过 JDK8 中常量池吗?说说运行时的常量池!

事先说明环境 在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());
    }
    
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
请写出控制台中打印的结果?


答案:
helloWorld
true
java
false
hello
false
1
2
3
4
5
6

你答对了吗?
为什么会出现这种答案呢?

  == 运算,对于非基本类型比较的是内存地址值,要知道这道题的输出的结果,最简单的方法就是知道对象: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));

    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  得到下面一组打印输出,同一个对象的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“
1
2
3
4
5
6
先讲一讲前面2行的由来

helloWorld
true
1
2
  我们都知道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"


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
继续向下讲解:

java
false
1
2
  会发现和上面一样的流程操作,为什么前面会返回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
1
2
不难解释,str3是通过new StringBuilder().toString();创建出来的对象,因此是一个全新的"hello"字符串对象,而调用.intern()方法后,返回的则是常量池中的"hello",两者明显不是同一个对象,因此返回false。

文章末尾更新一下我最近写的更详细的博客(保证能搞懂,为什么会返回true、false、false)
运行时常量池的再深入,从jvm的角度谈谈这道字符串常量池的面试题。
————————————————
版权声明:本文为CSDN博主「诗水人间」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41813208/article/details/109192324

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值