大水怪

一只专注技术的水怪

0%

常量池详解

关于常量池

常量池/Class常量池(Constant pool)

常量池,也叫 Class 常量池(常量池 == Class常量池)。Java文件被编译成 Class文件,Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池,常量池是当Class文件被Java虚拟机加载进来后存放在方法区 各种字面量 (Literal)和 符号引用 。

image-20220825185951061

运行时常量池(Runtime constant pool)

运行时常量池是方法区的一部分。运行时常量池是当Class文件被加载到内存后,Java虚拟机会 **将Class文件常量池里的内容转移到运行时常量池里(运行时常量池也是每个类都有一个)**。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中

只存放字符串引用

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1NzM3MDY4,size_16,color_FFFFFF,t_70-16547411527242

字符串常量池(String pool/String table)

字符串常量池逻辑上是运行时常量池的子集,具体可见https://github.com/fenixsoft/jvm_book/issues/112

字符串常量池又称为:字符串池,全局字符串池,英文也叫String Pool。 在工作中,String类是我们使用频率非常高的一种对象类型。JVM为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间:字符串常量池。字符串常量池由String类私有的维护

常量池和字符串常量池的版本变化

  • 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代

  • 在JDK1.7 字符串常量池、静态变量等被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说 字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代

    字符串常量池本身并没有被移动,还是在native memory,移动的是String实例,也就是逻辑上移动到了堆

  • 在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

    字符串常量池(StringTable)在native memory,一般逻辑上认为在堆里面

  • JDK1.8-1.9,String底层从char数组变成了byte数组,原因是部分字符仅占一个byte,而堆中含有大量的String字符串,该优化能节省较多空间。

  • 补充:

    • 对象只能存在于堆中(虚拟机规范的定义),所以String实体只能存在于堆中
    • 运行时常量池存放的是字面量引用
    • 使用双引号方式显式声明的字符串,则直接放入字符串常量池中(final修饰的“变量”可以直接看作双引号字面量)

StringTable为什么要调整(1.6-1.7)

  • permSize默认比较小
  • 永久代垃圾回收频率低

字符串拼接操作

  • 常量与常量的拼接结果在常量池,原理是编译器优化
  • 常量池中不会存在相同内容的常量
  • 只要其中一个是变量,结果就在堆中。变量拼接的原理是StringBuilder(final不算变量),返回String对象
  • 如果拼接的结果调用intern()方法,则注定将常量池中还没有的字符串对象放入池中,并返回此对象地址

所以建议多使用final定义字符串,并且不使用new,对于new String()声明final不会优化

示例

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
public static boolean IsInPool(String str){
return str == str.intern();
}
public static void main(String[] args) {
final String a1 = "a";
final String a2 = "b";
String a3 = a1 + a2;
String limit1 = "ab";

String a4 = "c";
String a5 = "d";
String a6 = a4 + a5;
String limit2 = "cd";

String b1 = new String("e");
String b2 = new String("f");
String b3 = b1 + b2;
String limit3 = "ef";

final String b4 = new String("g");
final String b5 = new String("h");
final String b6 = b4 + b5;
final String limit4 = "gh";

System.out.println(IsInPool(a3));//true
System.out.println(IsInPool(a6));//false
System.out.println(IsInPool(b3));//false
System.out.println(IsInPool(b6));//false
image-20211008215003210

字符串的创建与常量池

String两种创建方式

  • 方式一(str值和字符串常量池中字面量地址相等)
1
String str = "abc"
  1. 检查字符串常量池是否存在该字符串,存在则不创建并且返回string对象的引用
  2. 不存在则在堆中创建该字符串常量对应的string对象并将其引用存入字符串常量池中,然后返回该创建对象的引用
  • 方式二(str值和字符串常量池中字面量地址不相等)
1
String str = new String("abc")
  1. 检查字符串常量池是否存在该字符串,存在则不创建string对象,不存在则在堆中创建该字符串常量对应的string对象并将其引用存入字符串常量池中

    1
    2
    3
    4
    5
    6
    7
    // hotspot/src/share/vm/classfile/symbolTable.cpp
    // try to reuse the string if possible
    if (!string_or_null.is_null()) {
    string = string_or_null;
    } else {
    string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
    }
  2. 在堆中创建该对象,对象内部的char数组(实际的value)与常量池中创建的string对象共用一个char数组

测试代码

1
2
3
4
5
6
7
8
9
String str1 = new String("abcd");
String str2 = "abcd";
Field value = str1.getClass().getDeclaredField("value");
value.setAccessible(true);
System.out.println(System.identityHashCode(str1));//990368553
System.out.println(System.identityHashCode(value.get(str1)));//1096979270
System.out.println(System.identityHashCode(value.get(str2)));//1096979270
System.out.println(System.identityHashCode(str2));//1078694789
System.out.println(System.identityHashCode(str1.intern()));//1078694789

普遍地

使用双引号方式显式存在的字符串,则直接放入字符串常量池中(final修饰的“变量”可以直接看作双引号字面量)

一些测试(JDK1.8)

情况一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public static void main(String[] args) {
String a1 = "a";
String a2 = new String("b");
String a3 = "c";
String a4 = a1 + a2;
String a5 = a1 + a3;
// String at = "a"+"c";
// String at1 = "a"+"b";

System.out.println(IsInPool(a1));//true
System.out.println(IsInPool("b"));//true
System.out.println(IsInPool(a2));//false
System.out.println(IsInPool(a3));//true

System.out.println(IsInPool(a4));//true
System.out.println(IsInPool("ab"));//true
System.out.println(IsInPool(a5));//true
System.out.println(IsInPool("ac"));//true
}

情况二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
String a1 = "a";
String a2 = new String("b");
String a3 = "c";
String a4 = a1 + a2;
String a5 = a1 + a3;
String at = "a"+"c";
String at1 = "a"+"b";

System.out.println(IsInPool(a1));//true
System.out.println(IsInPool("b"));//true
System.out.println(IsInPool(a2));//false
System.out.println(IsInPool(a3));//true

System.out.println(IsInPool(a4));//false
System.out.println(IsInPool("ab"));//true
System.out.println(IsInPool(a5));//false
System.out.println(IsInPool("ac"));//t
}

IsInpool函数

1
2
3
4
5
6
   //使用这个函数需要在这个函数前使用双引号字面量使得字符串直接被加入常量池中
//使用顺序应为待测字符串->字面量->IsInPool

public static boolean IsInPool(String str){
return str == str.intern();
}

对于intern函数的理解

调用这个方法之后就是去看当前字符串是否在字符串常量池中已经存在引用

(1)存 在:那就直接返回该字符串在字符串常量池中所对应的地址给栈中要引用这个字符串的变量。

(2)不存在
① jdk 1.6:先在字符串常量池中创建该字符串,地址与堆中字符串地址不相同(方法区)。然后再返回刚创建的字符串在字符串常量池中所对应的地址给栈中要引用这个字符串的变量。

② jdk 1.7及以后:直接将堆中(不是字符串常量池中)该字符串的地址复制到字符串常量池中,这样字符串常量池就有了该字符串的地址引用,也可以说此时字符串常量池中的字符串只是一个对堆中字符串对象的引用,它们两个的地址相同,然后再把这个地址返回给栈中要引用这个字符串的变量。

具体看下面源码解析,这里描述可能不太精确

对测试的解释

  • 第一次两个拼接测试为true,因为intern函数将堆中字符串对象引用复制到字符串常量池中,所以二者自然相等
  • 第二次两个拼接测试为false,因为intern检查到已经存在该字符常量,且堆常量池中保存的是字符串的值,二者自然不相等

对StringBuilder的优化

对于StringBuilder.append,底层默认为长度为16的char型数组,超出长度则需要新建数组重新依次赋值

如果明确知道长度不高于某个值,可以使用new StringBuilder(max)来初始化

String的不可变性与强制修改

String为什么不可变?

  • 因为String底层是private修饰的final类型的char数组

Java9之后改用byte数组,原因是部分字符仅占一个byte,而堆中含有大量的String字符串,该优化能节省较多空间。

  • value数组创建出来后本身长度不可变,final修饰后也不能修改为其他引用

  • 并且String本身是final类,也就是不可被继承的

以上都是不考虑反射的说法

强制修改String

通过反射可以修改String

两块测试代码块应独立测试,因为第一部分代码块的isInPool会对常量池造成影响

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
public static boolean isInPool(String str){
return str == str.intern();
}
public static void main(String[] args) throws Exception {
String s1 = new String("String of ");
String s2 = new String("test");
String s = s1 + s2;
String origin = "String of test";


//用于检测常量池字面量是否被修改
//如果被修改,那么intern返回的应该是常量池中地址,跟对象地址应该不一样
String s3 = new String("Reflect of ");
String s4 = new String("test");
String s5 = s3 + s4;

/**
* Influential---修改常量池中对象
* 如果修改了,那么等下检测时str == str.intern()肯定会是true,
* 因为常量池中不存在修改后的字面量,所以intern返回的是对象本身
*/
System.out.println("修改堆对象");
System.out.println("修改前地址:"+System.identityHashCode(s));
System.out.println("修改前值:"+s);
Field value = s.getClass().getDeclaredField("value");
value.setAccessible(true);
char[] o = (char[]) value.get(s);
System.out.println("反射数据地址:"+System.identityHashCode(o));
o[0] = 'R';
o[1] = 'e';
o[2] = 'f';
o[3] = 'l';
o[4] = 'e';
o[5] = 'c';
o[6] = 't';
System.out.println("修改后地址:"+System.identityHashCode(s));
System.out.println("修改后值:"+s);
System.out.println(isInPool(s));//true
System.out.println("地址:"+System.identityHashCode(s));

/**
* Influential---修改String对象
* 如果修改了常量池中String字面量,那么等下检测时str == str.intern()应该要是false,
* 因为常量池按理已经存在了修改后的字面量,所以intern返回的常量池中地址
* 那么为什么会是true?
*/
System.out.println("===============分割线===============");
System.out.println("下面是修改常量池");
System.out.println("修改前地址:"+System.identityHashCode(origin));
System.out.println("修改前值:"+origin);
Field value2 = origin.getClass().getDeclaredField("value");
value2.setAccessible(true);
char[] o2 = (char[]) value2.get(origin);
System.out.println("反射数据地址:"+System.identityHashCode(o2));
o2[0] = 'R';
o2[1] = 'e';
o2[2] = 'f';
o2[3] = 'l';
o2[4] = 'e';
o2[5] = 'c';
o2[6] = 't';
System.out.println("修改后地址:"+System.identityHashCode(origin));
System.out.println("修改后值:"+origin);

System.out.println(isInPool(s5));//true
}

对于上面第二部分代码的解释

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
public static void main(String[] args) throws Exception {
String origin = "String of test";


//用于检测常量池字面量是否被修改
//如果被修改,那么intern返回的应该是常量池中地址,跟对象地址应该不一样
String s3 ="Reflect of test";
System.out.println(System.identityHashCode(s3));//true



/**
* Influential---修改String对象
* 如果修改了常量池中String字面量,那么等下检测时str == str.intern()应该要是false,
* 因为常量池按理已经存在了修改后的字面量,所以intern返回的常量池中地址
* 那么为什么会是true?
*/
System.out.println("===============分割线===============");
System.out.println("下面是修改常量池");
System.out.println("修改前地址:" + System.identityHashCode(origin));
System.out.println("修改前值:" + origin);
Field value2 = origin.getClass().getDeclaredField("value");
value2.setAccessible(true);
char[] o2 = (char[]) value2.get(origin);
System.out.println("反射数据地址:" + System.identityHashCode(o2));
o2[0] = 'R';
o2[1] = 'e';
o2[2] = 'f';
o2[3] = 'l';
o2[4] = 'e';
o2[5] = 'c';
o2[6] = 't';
System.out.println("修改后地址:" + System.identityHashCode(origin));
System.out.println("修改后值:" + origin);

//此时S3"Reflect of test"
//origin为"Reflect of test"
System.out.println("修改前常量池的string of reflect地址"+System.identityHashCode(s3));//true
System.out.println("修改string of test产生的string of reflect地址"+System.identityHashCode(origin));//true
String ss="Reflect of test";
System.out.println(System.identityHashCode(ss));

String sss="String of test";
System.out.println(System.identityHashCode(sss));
System.out.println(origin);
System.out.println(sss);
}

这段代码会导致最后声明的sss输出结果变成Reflect of test

image-20220410235820491

原因是:

字符串常量池底层其实是个hash表,这也很好地解释了字符串常量池检查重复的机制,查重能达到O(1)的复杂度。

将一个字符串值存入常量池时,会在将hash值,字符串数组作为kv存入查重的hash表中。

引用对象实体内容修改以后hash表里面字面量还是没变。

上面这句话是错误的解释,常量的操作主要是三种

  • 一方面是编译时会优化
  • 一方面显示声明会自动加常量池
  • 一方面即时编译器也会优化

实际上图中的问题是编译期间将多个常量只创建了一份,也就是通过的是class常量池,并非通过字符串常量池去重。

不过通过字面量显示声明的常量创建还是会走常量池的(无论是第一次创建还是后续重复创建)。

测试的时候可以通过将常量分别在多个类中编写来避免这种编译期间的去重(并且使用-Xint指定节解释执行)。

SymbolTable底层是基于hashtable实现的,结构是数组+链表,当存储的数据足够多,遇到hash碰撞严重时(Hotspot触发rehash的条件是:查找一个字符串超过100次),是通过切换hash算法实现的。

intern源码研究(JDK8)

https://blog.csdn.net/qq_19648191/article/details/117358082

https://www.freesion.com/article/61901174982/

https://blog.csdn.net/qq_33678688/article/details/89091930

https://zhuanlan.zhihu.com/p/28973077

  1. hotspot/src/share/vm/prims/jvm.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // String support ///////////////////////////////////////////////////////////////////////////

    JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
    JVMWrapper("JVM_InternString");
    JvmtiVMObjectAllocEventCollector oam;
    if (str == NULL) return NULL;
    oop string = JNIHandles::resolve_non_null(str);
    oop result = StringTable::intern(string, CHECK_NULL);
    return (jstring) JNIHandles::make_local(env, result);
    JVM_END
  2. hotspot/src/share/vm/classfile/symbolTable.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    oop StringTable::intern(oop string, TRAPS) {
    if (string == NULL) return NULL;
    ResourceMark rm(THREAD);
    int length;
    // 创建string的句柄
    Handle h_string (THREAD, string);
    // 转换成Unicode编码的玩意,这里似乎是重新开空间创建一个Unicode的字符串
    // 但是在后面这个Unicode字符串只用于计算哈希值和查找比对,除非下一个intern方法中发现句柄没有创建才会用到这个Unicode来创建句柄
    jchar* chars = java_lang_String::as_unicode_string(string, length,
    CHECK_NULL);
    // 调用最终的intern
    oop result = intern(h_string, chars, length, CHECK_NULL);
    return result;
    }
  3. hotspot/src/share/vm/classfile/symbolTable.cpp

    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
    // string是个handle,chars是转成了unicode的原字符串
    oop StringTable::intern(Handle string_or_null, jchar* name,
    int len, TRAPS) { y
    unsigned int hashValue = hash_string(name, len);
    int index = the_table()->hash_to_index(hashValue);
    oop found_string = the_table()->lookup(index, name, len, hashValue);

    // Found
    if (found_string != NULL) return found_string;

    debug_only(StableMemoryChecker smc(name, len * sizeof(name[0])));
    assert(!Universe::heap()->is_in_reserved(name),
    "proposed name of symbol must be stable");

    Handle string;
    // try to reuse the string if possible
    // 如果这个句柄已经创建好了能直接用那就直接用
    if (!string_or_null.is_null()) {
    string = string_or_null;
    } else {
    // 如果句柄没有创建好那就从转为Unicode编码后的这个字符串创建一个句柄
    // 目前看来似乎是编译期间使用双引号创建字面量会走这个逻辑,字符串常量池会直接创建一个string
    string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
    }

    // Grab the StringTable_lock before getting the_table() because it could
    // change at safepoint.
    MutexLocker ml(StringTable_lock, THREAD);

    // Otherwise, add to symbol to table
    // 向stringtable里面添加句柄
    return the_table()->basic_add(index, string, name, len,
    hashValue, CHECK_NULL);
    }
  4. hotspot/src/share/vm/classfile/symbolTable.cpp

    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
    oop StringTable::basic_add(int index_arg, Handle string, jchar* name,
    int len, unsigned int hashValue_arg, TRAPS) {

    assert(java_lang_String::equals(string(), name, len),
    "string must be properly initialized");
    // Cannot hit a safepoint in this function because the "this" pointer can move.
    No_Safepoint_Verifier nsv;

    // Check if the symbol table has been rehashed, if so, need to recalculate
    // the hash value and index before second lookup.
    unsigned int hashValue;
    int index;
    if (use_alternate_hashcode()) {
    hashValue = hash_string(name, len);
    index = hash_to_index(hashValue);
    } else {
    hashValue = hashValue_arg;
    index = index_arg;
    }

    // Since look-up was done lock-free, we need to check if another
    // thread beat us in the race to insert the symbol.
    // 如果已经有了就不用添加了并且可以直接返回
    oop test = lookup(index, name, len, hashValue); // calls lookup(u1*, int)
    if (test != NULL) {
    // Entry already added
    return test;
    }
    // HashtableEntry 里面保存hash值以及这个字符串的引用(_literal),有了引用就可以得到字符串的字面量和地址了
    HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string());
    add_entry(index, entry);
    return string();
    }

补充

关于内存分配位置

  1. hotspot/src/share/vm/runtime/handles.inline.hpp

    句柄的创建用的是线程本地内存,也就是堆空间

    1
    2
    3
    4
    5
    6
    7
    8
    inline Handle::Handle(Thread* thread, oop obj) {
    assert(thread == Thread::current(), "sanity check");
    if (obj == NULL) {
    _handle = NULL;
    } else {
    _handle = thread->handle_area()->allocate_handle(obj);
    }
    }
  2. hotspot/src/share/vm/classfile/symbolTable.cpp

    Unicode字符串的创建也是用的堆空间

    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
    jchar* java_lang_String::as_unicode_string(oop java_string, int& length, TRAPS) {
    typeArrayOop value = java_lang_String::value(java_string);
    int offset = java_lang_String::offset(java_string);
    length = java_lang_String::length(java_string);

    jchar* result = NEW_RESOURCE_ARRAY_RETURN_NULL(jchar, length);
    if (result != NULL) {
    for (int index = 0; index < length; index++) {
    result[index] = value->char_at(index + offset);
    }
    } else {
    THROW_MSG_0(vmSymbols::java_lang_OutOfMemoryError(), "could not allocate Unicode string");
    }
    return result;
    }

    // hotspot/src/share/vm/memory/allocation.hpp
    #define NEW_RESOURCE_ARRAY_RETURN_NULL(type, size)\
    (type*) resource_allocate_bytes((size) * sizeof(type), AllocFailStrategy::RETURN_NULL)

    // hotspot/src/share/vm/memory/resourceArea.cpp
    // Allocation in thread-local resource area
    extern char* resource_allocate_bytes(size_t size, AllocFailType alloc_failmode) {
    return Thread::current()->resource_area()->allocate_bytes(size, alloc_failmode);
    }
  3. StringTable的创建实际上是在nativememory

    1. https://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html

      官方文档说明说的是将interned strings移到堆内存中,而非StringTable

      Area: HotSpot
      Standard/Platform: JDK 7
      Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and lessw data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.
      RFE: 6962931

    2. 深入理解Java虚拟机中3.2.2 可达性分析算法写到

      在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

      ·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。

      ·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

      ·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用

    3. R大说的

      常量池”如果说的是SymbolTable / StringTable,这俩table自身原本就一直在native memory里,是它们所引用的东西在哪里更有意思。上面说了,java7是把SymbolTable引用的Symbol移动到了native memory,而StringTable引用的java.lang.String实例则从PermGen移动到了普通Java heap。

    4. StringTable的创建源码

      hotspot/src/share/vm/memory/universe.cpp

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      jint universe_init() {

      ......

      if (UseSharedSpaces) {
      // Read the data structures supporting the shared spaces (shared
      // system dictionary, symbol table, etc.). After that, access to
      // the file (other than the mapped regions) is no longer needed, and
      // the file is closed. Closing the file does not affect the
      // currently mapped regions.
      MetaspaceShared::initialize_shared_spaces();
      StringTable::create_table();
      } else {
      SymbolTable::create_table();
      StringTable::create_table();
      ClassLoader::create_package_info_table();
      }

      ......
      }

      调用链(jdk12)

      image-20220720175742728

      最终创建一个CHeap对象,不在 Java 的几个托管内存里,其实也就是native memory

总结

总的来说,字符串常量池一直在nativememory中

而JDK1.7之后interned strings被移动到了堆内存中,而StringTable保存的是字符串的hashvalue以及字符串本身

另外,SymbolTable也在native memory中,至于SymbolTable的作用见https://hllvm-group.iteye.com/group/topic/26412#post-187861

这些Utf8常量在HotSpot VM里以symbolOopDesc对象(下面简称symbol对象)来表现;它们可以通过一个全局的SymbolTable对象找到。注意:constantPool对象并不“包含”这些symbol对象,而只是引用着它们而已;或者说,constantPool对象只存了对symbol对象的引用,而没有存它们的内容。

https://www.zhihu.com/question/29352626/answer/44050736

Symbol内嵌的存储不是char数组。如果要CONSTANT_String(String)引用了CONSTANT_Utf8(Symbol),那么ldc在初始化这个String对象的时候会从Symbol那边读取内容并创建对应的char[]以及String对象。

Symbols在JDK7和JDK8里都在native memory里,但不在Metaspace里。
Symbols在native memory里通过引用计数管理,同时有个全局的SymbolTable管理着所有Symbol。