前言
最近在看到这样一篇文章 [公告] 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 <970655147@qq.com>
* @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 到当前进程, 查看局部变量信息如下
0x0000700005a0ea08 0x0000000000000000
0x0000700005a0ea10 0x00000007bf6053a8
0x0000700005a0ea18 0x00000007bf605398
可以看到此时 str01 的引用为 0x00000007bf6053a8, str02 还未到作用域, 默认填充的 0x0
inspect 一下 str01
然后 放开断点, 在第二个断点的地方, 使用 HSDB 重新attach当前进程
0x0000700005a0ea08 0x00000007bfc3dea8
0x0000700005a0ea10 0x0000000000000000
0x0000700005a0ea18 0x00000007bfc3de58
可以看到 str01 被置为 null, str02 的引用为 0x00000007bfc3dea8, 和前一个 String.intern 拿到的引用是不一样的
inspect 一下 str02
然后 本文需要 证明的东西到这里就完了 : "String.intern 同一个字符串返回不同的引用"
"2372826".intern 被回收的地方
根据上面的测试代码的 VM 参数, 我们知道gc相关参数 UseSerialGC, 搭配使用的是 新生代使用的是 DefNewGeneration + 老年代的 TenuredGeneration
以下情况是根据上面的特定的配置进行讨论
根据我这里的调试实际情况, 发生了一次 minor gc 和 一次 full gc
stackTrace 如下
1. 先看 minor gc 这一次吧
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 这一次
怎么回事?, 发生 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 阶段创建数组
看到这里吧 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'
这里创建了一个 Object[1], 添加到了 loader_data 的 chunkList 里面
loader_data 对应的 loader 为 "jdk/internal/loader/ClassLoaders$AppClassLoader", 也就是我们常说的 AppClassLoader
2.2 解析 "2372826" 引用
这里便是 解析这个 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, 但是另外一个是什么呢 ?
单步走一步, 然后使用 HSDB attach 到该进程, 然后 查看一下 引用, 我们发现只有一个, str01 置为了 null 之后, 不在引用 "2372826", 另外一个是什么呢 ?
呵呵 者另外一个引用, 显然就是 loader_data 里面的 chunkList 里面的这个 Object[1] 了
3. "2372826" gc 之后在 oldGen ?
如果您足够细心的话, 你会发现 文章开头的第一次截图 和 第二次截图的 "2372826" 的oop是有差异的
第一个断点 oop[str01] 是在 newGen, 第二个断点 oop[str02] 是在 oldGen
呵呵 那么这是为什么呢??, 哈哈 我看到这个 也有一点不解
我们定位到 老年代的 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 里面
再查看一下 其他栈帧的情况, 可以看到 _old_gen, _young_gen 在 _old_gen 的 markSweep 的处理过程中都会 移动到 _old_gen 里面
可以看到, 一般情况下 _young_gen, _old_gen 里面的所有的对象就移动到了 _old_gen 里面了
4. rewriter 的 _reference_map 是那个大版本添加的?
查看一下 openjdk7 的 rewriter.hpp
查看一下 openjdk8 的 rewriter.hpp
查看一下 openjdk8 的 constantsPool.initialize_resolved_references
查看一下 openjdk9 的 rewriter.hpp
[删除于 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 了
当然, 也是基于上面剖析一系列流程的特性来编写的这个测试用例