0
点赞
收藏
分享

微信扫一扫

08 String.intern 同一个字符串返回不同的引用


前言

呵呵 最近在看到这样的一篇文章的时候 [公告] String.intern()常量池溢出 

文章讲解的结论是 "HotSpot VM的interned String可以被GC。"

突然有一个想法, 我们之前 不是一直经常会看到 这样的示例么, 然后一般情况下 都会有很多的剖析, 这四个等式是如何如何, 所以结果是怎样 

System.out.println(new String("123") == new String("123"));
    System.out.println(new String("123") == "123".intern());
    System.out.println("123" == "123".intern());
    System.out.println("123".intern() == "123".intern());

我们这里着重关注第四个等式 "System.out.println("123".intern() == "123".intern());" 

根据我们的经验来看, 这个应该是返回 true 

但是我们本文就要构造一个 String.intern 同一个字符串 返回不同的引用的情况 

以下其他代码, 截图 基于 jdk9 

测试用例

package com.hx.test04;

/**
 * StringInternEq
 *
 * @author Jerry.X.He 
 * @version 1.0
 * @date 2020-03-21 16:18
 */
public class Test14StringInternEq implements Cloneable {

  int f01;
  int f02;
  int f03;
  int f04;
  int f05;
  int f06;

  // String.intern
  // -Xint -Xmx10M -XX:+UseSerialGC  -XX:+PrintGCDetails
  public static void main(String[] args) throws Exception {

    String str01 = "2372826".intern();
    str01 = null;
    createFullGc();
    String str02 = "2372826".intern();

    System.out.println(str01 == str02);

  }

  // createFullGc
  public static void createFullGc() {
    for(int i=0; i<2; i++) {
      byte[] bytes = new byte[4 * 1024 * 1024];
    }
  }

}

我们主要关注的是 main 中的内容, createFullGc 的作用是辅助构造 full gc 

另外一点就是 关于 第一个 String.intern 和 第二个 String.intern 的结果的对比不能直接证明, 只能通过一些辅助的方法来证明我们的结果 

接下来我们便看一看测试用例的一些运行时的情况吧 

"str01 = null", 这一行打一个断点, 然后使用 HSDB attach 到当前进程, 查看局部变量信息如下 

08 String.intern 同一个字符串返回不同的引用_sed

0x0000700005a0ea08	0x0000000000000000
0x0000700005a0ea10	0x00000007bf6053a8
0x0000700005a0ea18	0x00000007bf605398

可以看到此时 str01 的引用为 0x00000007bf6053a8, str02 还未到作用域, 默认填充的 0x0 

inspect 一下 str01 

08 String.intern 同一个字符串返回不同的引用_hotspotvm_02

然后 放开断点, 在第二个断点的地方, 使用 HSDB 重新attach当前进程 

08 String.intern 同一个字符串返回不同的引用_java_03

0x0000700005a0ea08	0x00000007bfc3dea8
0x0000700005a0ea10	0x0000000000000000
0x0000700005a0ea18	0x00000007bfc3de58

可以看到 str01 被置为 null, str02 的引用为 0x00000007bfc3dea8, 和前一个 String.intern 拿到的引用是不一样的 

inspect 一下 str02 

08 String.intern 同一个字符串返回不同的引用_hotspotvm_04

然后 本文需要 证明的东西到这里就完了 : "String.intern 同一个字符串返回不同的引用"

"2372826".intern 被回收的地方

根据上面的测试代码的 VM 参数, 我们知道gc相关参数 UseSerialGC, 搭配使用的是 新生代使用的是 DefNewGeneration + 老年代的 TenuredGeneration

以下情况是根据上面的特定的配置进行讨论 

根据我这里的调试实际情况, 发生了一次 minor gc 和 一次 full gc 

08 String.intern 同一个字符串返回不同的引用_System_05

stackTrace 如下 

08 String.intern 同一个字符串返回不同的引用_hotspotvm_06

1. 先看 minor gc 这一次吧 

08 String.intern 同一个字符串返回不同的引用_System_07

08 String.intern 同一个字符串返回不同的引用_sed_08

defNewGeneration 相关内存信息如下 

def new generation   total 3072K, used 1994K [0x00000007bf600000, 0x00000007bf950000, 0x00000007bf950000)
  eden space 2752K,  60% used [0x00000007bf600000, 0x00000007bf7a2ac8, 0x00000007bf8b0000)
  from space 320K, 100% used [0x00000007bf900000, 0x00000007bf950000, 0x00000007bf950000)
  to   space 320K,  10% used [0x00000007bf8b0000, 0x00000007bf8b8028, 0x00000007bf900000)

FastScanClosure 处理一下 "2372826" 之后, 将该 oop 放到了 defNewGeneration 的 to 里面去了 

这个就是 minor gc 的这一次处理, 呵呵 到这里的话, 回想一下 我们的题目, 是否是 一次 minor gc 也能够达到效果呢 ?

2. 再来看看 full_gc 这一次 

08 String.intern 同一个字符串返回不同的引用_hotspotvm_09

怎么回事?, 发生 gc 的时候 应该是在 createFullGc 里面吧, 那么这时候 str01 为 null, 应该没有指向 "2372826" 的引用了啊 ? 

呵呵 也是这个原因, 妈的 本文章的前一天晚上 找了一晚上 ... 

呵呵 虽然 str01 没有引用 "2372826", 但是还有其他的引用  

-- 艹 多钻出来一个 new Object[]{"2372826"};
[Ljava.lang.Object;
{0x00000007bf8b0dd8} - klass: 'java/lang/Object'[]
 - length: 1
 -   0 : "2372826"{0x00000007bf8b8010}

从这个为线索继续往上面找 0x00000007bf8b0dd8
这个 Object[] 来自于一个 ClassLoaderData
c = {ClassLoaderData::ChunkedHandleList::Chunk * | 0x7fe583c4a420} 0x00007fe583c4a420

然后从这个 Chunk 里面的数据是怎么放进去的呢 ? 

这里会分为两个部分, 一个部分是创建数组, 另外的一个部分是运行时解析给定的字符串的直接引用, 放到数组里面 

2.1 Rewriter 阶段创建数组

08 String.intern 同一个字符串返回不同的引用_System_10

看到这里吧 String 类型的常量 放到了 _resolved_references_map 里面, 容量为 1, 放进去了一个 索引为 2 

这里贴一下 Test14StringInternEq 的常量池信息, 索引为 2 的正是 "2372826"

{constant pool}
 - holder: 0x00000007c008fc30
 - cache: 0x0000000000000000
 - resolved_references: 0x0000000000000000
 - reference_map: 0x0000000000000000
 -   1 : Method : klass_index=9 name_and_type_index=42
 -   2 : String : '2372826'
 -   3 : Method : klass_index=34 name_and_type_index=44
 -   4 : Method : klass_index=8 name_and_type_index=45
 -   5 : Field : klass_index=46 name_and_type_index=47
 -   6 : Method : klass_index=35 name_and_type_index=48
 -   7 : Integer : 4194304
 -   8 : Unresolved Class : 'com/hx/test04/Test14StringInternEq'
 -   9 : Unresolved Class : 'java/lang/Object'
 -  10 : Unresolved Class : 'java/lang/Cloneable'
 -  11 : Utf8 : 'f01'
 -  12 : Utf8 : 'I'
 -  13 : Utf8 : 'f02'
 -  14 : Utf8 : 'f03'
 -  15 : Utf8 : 'f04'
 -  16 : Utf8 : 'f05'
 -  17 : Utf8 : 'f06'
 -  18 : Utf8 : '<init>'
 -  19 : Utf8 : '()V'
 -  20 : Utf8 : 'Code'
 -  21 : Utf8 : 'LineNumberTable'
 -  22 : Utf8 : 'LocalVariableTable'
 -  23 : Utf8 : 'this'
 -  24 : Utf8 : 'Lcom/hx/test04/Test14StringInternEq;'
 -  25 : Utf8 : 'main'
 -  26 : Utf8 : '([Ljava/lang/String;)V'
 -  27 : Utf8 : 'args'
 -  28 : Utf8 : '[Ljava/lang/String;'
 -  29 : Utf8 : 'str01'
 -  30 : Utf8 : 'Ljava/lang/String;'
 -  31 : Utf8 : 'str02'
 -  32 : Utf8 : 'StackMapTable'
 -  33 : Unresolved Class : '[Ljava/lang/String;'
 -  34 : Unresolved Class : 'java/lang/String'
 -  35 : Unresolved Class : 'java/io/PrintStream'
 -  36 : Utf8 : 'Exceptions'
 -  37 : Unresolved Class : 'java/lang/Exception'
 -  38 : Utf8 : 'createFullGc'
 -  39 : Utf8 : 'i'
 -  40 : Utf8 : 'SourceFile'
 -  41 : Utf8 : 'Test14StringInternEq.java'
 -  42 : NameAndType : name_index=18 signature_index=19
 -  43 : Utf8 : '2372826'
 -  44 : NameAndType : name_index=55 signature_index=56
 -  45 : NameAndType : name_index=38 signature_index=19
 -  46 : Unresolved Class : 'java/lang/System'
 -  47 : NameAndType : name_index=58 signature_index=59
 -  48 : NameAndType : name_index=60 signature_index=61
 -  49 : Utf8 : 'com/hx/test04/Test14StringInternEq'
 -  50 : Utf8 : 'java/lang/Object'
 -  51 : Utf8 : 'java/lang/Cloneable'
 -  52 : Utf8 : 'java/lang/String'
 -  53 : Utf8 : 'java/io/PrintStream'
 -  54 : Utf8 : 'java/lang/Exception'
 -  55 : Utf8 : 'intern'
 -  56 : Utf8 : '()Ljava/lang/String;'
 -  57 : Utf8 : 'java/lang/System'
 -  58 : Utf8 : 'out'
 -  59 : Utf8 : 'Ljava/io/PrintStream;'
 -  60 : Utf8 : 'println'
 -  61 : Utf8 : '(Z)V'

08 String.intern 同一个字符串返回不同的引用_hotspotvm_11

这里创建了一个 Object[1], 添加到了 loader_data 的 chunkList 里面

loader_data 对应的 loader 为 "jdk/internal/loader/ClassLoaders$AppClassLoader", 也就是我们常说的 AppClassLoader

2.2 解析 "2372826" 引用

08 String.intern 同一个字符串返回不同的引用_sed_12

这里便是 解析这个 resolved_references[0] 的地方

调整一下栈帧查看其 当前调用的 method 为 "com.hx.test04.Test14StringInternEq.main([Ljava/lang/String;)V"

main 的字节码如下 

public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: ldc           #2                  // String 2372826
       2: invokevirtual #3                  // Method java/lang/String.intern:()Ljava/lang/String;
       5: astore_1
       6: aconst_null
       7: astore_1
       8: invokestatic  #4                  // Method createFullGc:()V
      11: ldc           #2                  // String 2372826
      13: invokevirtual #3                  // Method java/lang/String.intern:()Ljava/lang/String;
      16: astore_2
      17: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      20: aload_1
      21: aload_2
      22: if_acmpne     29
      25: iconst_1
      26: goto          30
      29: iconst_0
      30: invokevirtual #6                  // Method java/io/PrintStream.println:(Z)V
      33: return

查看一下相关寄存器的状态

(lldb) re r
General Purpose Registers:
       rax = 0x00000007bf7982c8
       rbx = 0x00000000000000e6
       rcx = 0x0000000000000001
       rdx = 0x00000007bf7982c8
       rdi = 0x0000000000000007
       rsi = 0x00000007bf7982c8
       rbp = 0x0000700002447440
       rsp = 0x0000700002447000
        r8 = 0x0000000102ce3df8  libjvm.dylib`UseCompressedOops
        r9 = 0x0000000000000036
       r10 = 0x00007fe66a00f800
       r11 = 0xfffff019974458a4
       r12 = 0x0000000000000000
       r13 = 0x0000000103acc020
       r14 = 0x00007000024476a8
       r15 = 0x00007fe66a00f800
       rip = 0x00000001020285b5  libjvm.dylib`ConstantPool::resolve_constant_at_impl(constantPoolHandle const&, int, int, Thread*) + 4005 at constantPool.cpp:777
    rflags = 0x0000000000000206
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

查看一下 r13 寄存器存放的地址的数据信息 

(lldb) x 0x0000000103acc020
0x103acc020: e6 00 b6 01 00 4c 01 4c b8 02 00 e6 00 b6 01 00  .....L.L........
0x103acc030: 4d b2 03 00 2b 2c a6 00 07 04 a7 00 04 03 b6 04  M...+,..........

0xe6 为 fast_aldc 指令, 0xb6 为 invokevirtual, 0x4c 为 astore_1, 0x1 为 aconst_null 

呵呵 结合这些核心的指令, 以及上面的字节码 我觉得 当前执行的代码应该就能够确定下来了吧 

2.3. 如何证明引用 "2372826" 的其他引用(除去 str01)只有一个呢? 

呵呵 我们上面调试虽然是证明了 "2372826" 除了 str01 引用之外还有其他引用, 但是不确定 是否只有 loader_data 里面这个, 那么就需要一些其他的辅助方法来证明一下了

我们来做一个调试, 在 "str01 = null;" 的地方打一个断点, 然后使用 HSDB attach 到该进程, 然后 查看一下 引用, 我们发现了两个, 一个很明显 应该是 str01, 但是另外一个是什么呢 ?

08 String.intern 同一个字符串返回不同的引用_string_13

单步走一步, 然后使用 HSDB attach 到该进程, 然后 查看一下 引用, 我们发现只有一个, str01 置为了 null 之后, 不在引用 "2372826", 另外一个是什么呢 ?

08 String.intern 同一个字符串返回不同的引用_System_14

呵呵 者另外一个引用, 显然就是 loader_data 里面的 chunkList 里面的这个 Object[1] 了瑟  

3. "2372826" gc 之后在 oldGen ?

如果您足够细心的话, 你会发现 文章开头的第一次截图 和 第二次截图的 "2372826" 的oop是有差异的 

第一个断点 oop[str01] 是在 newGen, 第二个断点 oop[str02] 是在 oldGen 

呵呵 那么这是为什么呢??, 哈哈 我看到这个 也有一点不解 

08 String.intern 同一个字符串返回不同的引用_string_15

我们定位到 老年代的 mark_sweep_phase2(计算存活对象移动的目标位置) 

查看一下 整个堆的内存信息 

def new generation   total 3072K, used 316K [0x00000007bf600000, 0x00000007bf950000, 0x00000007bf950000)
  eden space 2752K,   0% used [0x00000007bf600000, 0x00000007bf600000, 0x00000007bf8b0000)
  from space 320K,  98% used [0x00000007bf8b0000, 0x00000007bf8ff078, 0x00000007bf900000)
  to   space 320K,   0% used [0x00000007bf900000, 0x00000007bf900000, 0x00000007bf950000)
 tenured generation   total 6848K, used 4871K [0x00000007bf950000, 0x00000007c0000000, 0x00000007c0000000)
   the space 6848K,  71% used [0x00000007bf950000, 0x00000007bfe11f40, 0x00000007bfa15600, 0x00000007c0000000)
 Metaspace       used 5503K, capacity 5628K, committed 5760K, reserved 1056768K
  class space    used 544K, capacity 577K, committed 640K, reserved 1048576K

可以看到, cur_obj 是在 defNewGeneration 的 from space, 然后 计算之后的地址 是在 tenuredGeneration 里面 

08 String.intern 同一个字符串返回不同的引用_string_16

再查看一下 其他栈帧的情况, 可以看到 _old_gen, _young_gen 在 _old_gen 的 markSweep 的处理过程中都会 移动到 _old_gen 里面

08 String.intern 同一个字符串返回不同的引用_string_17

可以看到, 一般情况下 _young_gen, _old_gen 里面的所有的对象就移动到了 _old_gen 里面了

4. rewriter 的 _reference_map 是那个大版本添加的? 

查看一下 openjdk7 的 rewriter.hpp 

08 String.intern 同一个字符串返回不同的引用_sed_18

查看一下 openjdk8 的 rewriter.hpp 

08 String.intern 同一个字符串返回不同的引用_System_19

查看一下 openjdk8 的 constantsPool.initialize_resolved_references

08 String.intern 同一个字符串返回不同的引用_string_20

查看一下 openjdk9 的 rewriter.hpp 

08 String.intern 同一个字符串返回不同的引用_hotspotvm_21

[删除于 2021.11.21]这里就没有去查看具体的小版本号了, 所以理论上来说 在没得 reference_map 的处理之前, full gc 会回收掉 "2372826" 

在添加了 reference_map 相关处理之后, full gc 不会回收掉 "2372826" 

2021.11.21 部分内容修正 

关于上面这个 [删除于 2021.11.21] 错误的结论, 新增了一篇文章 (续01)String.intern 同一个字符串返回不同的引用

另外, 针对 ZeroForAll 的提问, 新加了一个测试用例 Test24StringInConstantsPool.java.zip, 用这个测试用例来跑, 就会出现 OOM 了 

当然, 也是基于上面剖析一系列流程的特性来编写的这个测试用例 

完 

参考

[公告] String.intern()常量池溢出

(续01)String.intern 同一个字符串返回不同的引用


举报

相关推荐

0 条评论