字符串文字、实习和反射
String literals, interning and reflection
我正在尝试找到 this question 的第三个解决方案。
我不明白为什么不打印 false
。
public class MyClass {
public MyClass() {
try {
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
f.set("true", f.get("false"));
} catch (Exception e) {
}
}
public static void main(String[] args) {
MyClass m = new MyClass();
System.out.println(m.equals(m));
}
}
当然,由于字符串驻留,被修改的 "true"
实例与 PrintStream
?
的 print
方法中使用的实例完全相同
public void print(boolean b) {
write(b ? "true" : "false");
}
我错过了什么?
编辑
@yshavit 的一个有趣观点是,如果您添加行
System.out.println(true);
在try
之前,输出是
true
false
我把它写成一个社区 wiki,因为我不知道它是否正确,也不了解细节。
似乎发生的情况是,当在运行时遇到字符串文字时,JVM 检查字符串池(使用 equals
)以查看该字符串是否已经存在。如果不存在,则使用新实例。这个对象(要么是新对象,要么是已经在字符串池中的对象)是从现在开始将用于 all 字符串文字的那个 class都一样。
考虑这个例子:
public class MyClass {
public MyClass() {
try {
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
f.set("true", f.get("false"));
} catch (Exception e) {
}
}
public static void main(String[] args) {
System.out.println(true); // 1
new MyClass();
System.out.println(true); // 2
System.out.println("true"); // 3
printTrue();
OtherClass.printTrue();
}
public static void printTrue() {
System.out.println("true"); // 4
}
}
public class OtherClass {
static void printTrue() {
System.out.println("true"); // 5
}
}
这会打印:
true
false
false
false
true
我的解释:
在第 1 行,JVM 在 PrintStream
class 中遇到文字 "true"
。一个新字符串被添加到池中。然后 new MyClass()
被调用。在此构造函数中,JVM 在 MyClass
class 中遇到字符串文字 "true"
。该字符串已经在池中,因此池中的实例是将要使用的实例,但至关重要的是,它也是稍后将在第 3 行和第 4 行中使用的实例。然后修改支持该字符串的数组。因此第 2、3 和 4 行都打印 false
。接下来,调用 OtherClass.printTrue()
并且 JVM 在 OtherClass
中第一次遇到字符串文字 "true"
。此字符串对于池中的字符串是 而不是 equal
,因为池中的字符串现在具有后备数组 [f, a, l, s, e]
。因此,使用了一个新的字符串实例,并在第 5 行打印了 true
。
现在假设我们注释掉第 1 行:
// System.out.println(true); // 1
这次的输出是:
true
false
false
true
为什么第 2 行会产生不同的结果?此处的不同之处在于 "true"
不会在 PrintStream
class 中遇到,直到 在 后备数组被修改后。所以 "wrong" 字符串不是 PrintStream
class 中使用的字符串。然而,出于与上述相同的原因,第 3 行和第 4 行继续打印 "false"
。
这可以说是 HotSpot JVM 错误。
问题出在字符串字面量驻留机制。
java.lang.String
字符串文字的实例是在常量池解析期间延迟创建的。
- 最初,字符串文字在常量池中表示为
CONSTANT_String_info
structure that points to CONSTANT_Utf8_info
。
- 每个class都有自己的常量池。也就是说,
MyClass
和 PrintStream
有自己的一对 CONSTANT_String_info
/ CONSTANT_Utf8_info
cpool 条目用于文字 'true' .
- 第一次访问
CONSTANT_String_info
时,JVM启动解析过程。字符串实习是这个过程的一部分。
- 为了找到与驻留文字的匹配项,JVM 将
CONSTANT_Utf8_info
的内容与 StringTable
. 中字符串实例的内容进行比较
- ^^^ 这就是问题所在。来自 cpool 的原始 UTF 数据与 Java
char[]
数组内容进行比较,用户可以通过反射欺骗这些内容。
那么,你的测试发生了什么?
f.set("true", f.get("false"))
发起'true' 在MyClass
. 中的解析
- JVM 在
StringTable
中没有发现匹配序列 'true' 的实例,并创建一个新的 java.lang.String
,存储在 StringTable
.
来自 StringTable
的字符串的 value
通过反射被替换。
System.out.println(true)
发起 'true' 在 PrintStream
class. 中的解析
- JVM 将 UTF 序列 'true' 与来自
StringTable
的字符串进行比较,但找不到匹配项,因为该字符串已经有 'false' 值。 'true' 的另一个字符串被创建并放置在 StringTable
.
为什么我认为这是一个错误?
JLS §3.10.5 and JVMS §5.1 要求包含相同字符序列的字符串文字必须指向 java.lang.String
.
的相同实例
但是,在下面的代码中,解析具有相同字符序列的两个字符串文字会导致不同个实例。
public class Test {
static class Inner {
static String trueLiteral = "true";
}
public static void main(String[] args) throws Exception {
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
f.set("true", f.get("false"));
if ("true" == Inner.trueLiteral) {
System.out.println("OK");
} else {
System.out.println("BUG!");
}
}
}
JVM 的一个可能修复方法是将指向原始 UTF 序列的指针与 java.lang.String
对象一起存储在 StringTable
中,这样实习进程就不会将 cpool 数据(用户无法访问)与 value
数组(可通过反射访问)。
我正在尝试找到 this question 的第三个解决方案。
我不明白为什么不打印 false
。
public class MyClass {
public MyClass() {
try {
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
f.set("true", f.get("false"));
} catch (Exception e) {
}
}
public static void main(String[] args) {
MyClass m = new MyClass();
System.out.println(m.equals(m));
}
}
当然,由于字符串驻留,被修改的 "true"
实例与 PrintStream
?
print
方法中使用的实例完全相同
public void print(boolean b) {
write(b ? "true" : "false");
}
我错过了什么?
编辑
@yshavit 的一个有趣观点是,如果您添加行
System.out.println(true);
在try
之前,输出是
true
false
我把它写成一个社区 wiki,因为我不知道它是否正确,也不了解细节。
似乎发生的情况是,当在运行时遇到字符串文字时,JVM 检查字符串池(使用 equals
)以查看该字符串是否已经存在。如果不存在,则使用新实例。这个对象(要么是新对象,要么是已经在字符串池中的对象)是从现在开始将用于 all 字符串文字的那个 class都一样。
考虑这个例子:
public class MyClass {
public MyClass() {
try {
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
f.set("true", f.get("false"));
} catch (Exception e) {
}
}
public static void main(String[] args) {
System.out.println(true); // 1
new MyClass();
System.out.println(true); // 2
System.out.println("true"); // 3
printTrue();
OtherClass.printTrue();
}
public static void printTrue() {
System.out.println("true"); // 4
}
}
public class OtherClass {
static void printTrue() {
System.out.println("true"); // 5
}
}
这会打印:
true
false
false
false
true
我的解释:
在第 1 行,JVM 在 PrintStream
class 中遇到文字 "true"
。一个新字符串被添加到池中。然后 new MyClass()
被调用。在此构造函数中,JVM 在 MyClass
class 中遇到字符串文字 "true"
。该字符串已经在池中,因此池中的实例是将要使用的实例,但至关重要的是,它也是稍后将在第 3 行和第 4 行中使用的实例。然后修改支持该字符串的数组。因此第 2、3 和 4 行都打印 false
。接下来,调用 OtherClass.printTrue()
并且 JVM 在 OtherClass
中第一次遇到字符串文字 "true"
。此字符串对于池中的字符串是 而不是 equal
,因为池中的字符串现在具有后备数组 [f, a, l, s, e]
。因此,使用了一个新的字符串实例,并在第 5 行打印了 true
。
现在假设我们注释掉第 1 行:
// System.out.println(true); // 1
这次的输出是:
true
false
false
true
为什么第 2 行会产生不同的结果?此处的不同之处在于 "true"
不会在 PrintStream
class 中遇到,直到 在 后备数组被修改后。所以 "wrong" 字符串不是 PrintStream
class 中使用的字符串。然而,出于与上述相同的原因,第 3 行和第 4 行继续打印 "false"
。
这可以说是 HotSpot JVM 错误。
问题出在字符串字面量驻留机制。
java.lang.String
字符串文字的实例是在常量池解析期间延迟创建的。- 最初,字符串文字在常量池中表示为
CONSTANT_String_info
structure that points toCONSTANT_Utf8_info
。 - 每个class都有自己的常量池。也就是说,
MyClass
和PrintStream
有自己的一对CONSTANT_String_info
/CONSTANT_Utf8_info
cpool 条目用于文字 'true' . - 第一次访问
CONSTANT_String_info
时,JVM启动解析过程。字符串实习是这个过程的一部分。 - 为了找到与驻留文字的匹配项,JVM 将
CONSTANT_Utf8_info
的内容与StringTable
. 中字符串实例的内容进行比较
- ^^^ 这就是问题所在。来自 cpool 的原始 UTF 数据与 Java
char[]
数组内容进行比较,用户可以通过反射欺骗这些内容。
那么,你的测试发生了什么?
f.set("true", f.get("false"))
发起'true' 在MyClass
. 中的解析
- JVM 在
StringTable
中没有发现匹配序列 'true' 的实例,并创建一个新的java.lang.String
,存储在StringTable
.
来自 value
通过反射被替换。System.out.println(true)
发起 'true' 在PrintStream
class. 中的解析
- JVM 将 UTF 序列 'true' 与来自
StringTable
的字符串进行比较,但找不到匹配项,因为该字符串已经有 'false' 值。 'true' 的另一个字符串被创建并放置在StringTable
.
StringTable
的字符串的 为什么我认为这是一个错误?
JLS §3.10.5 and JVMS §5.1 要求包含相同字符序列的字符串文字必须指向 java.lang.String
.
但是,在下面的代码中,解析具有相同字符序列的两个字符串文字会导致不同个实例。
public class Test {
static class Inner {
static String trueLiteral = "true";
}
public static void main(String[] args) throws Exception {
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
f.set("true", f.get("false"));
if ("true" == Inner.trueLiteral) {
System.out.println("OK");
} else {
System.out.println("BUG!");
}
}
}
JVM 的一个可能修复方法是将指向原始 UTF 序列的指针与 java.lang.String
对象一起存储在 StringTable
中,这样实习进程就不会将 cpool 数据(用户无法访问)与 value
数组(可通过反射访问)。