字符串文字,实习和反思

我正试图找到这个问题的第三个解决方案。

我无法理解为什么这不打印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"实例与PrintStreamprint方法中使用的实例完全相同?

 public void print(boolean b) { write(b ? "true" : "false"); } 

我错过了什么?

编辑

@yshavit的一个有趣的观点是,如果你添加这条线

 System.out.println(true); 

try之前,输出是

 true false 

这可以说是一个HotSpot JVM错误。

问题出在字符串文字实习机制中

  • 在常量池解析期间,懒惰地创建字符串文字的java.lang.String实例。
  • 最初, CONSTANT_String_info结构在常量池中表示字符串文字,该结构指向CONSTANT_Utf8_info
  • 每个类都有自己的常量池。 也就是说, MyClassPrintStream拥有自己的一对CONSTANT_String_info / CONSTANT_Utf8_info cpool条目,用于文字‘true’
  • 当第一次访问CONSTANT_String_info时,JVM启动解析过程。 字符串实习是此过程的一部分。
  • 要查找正在实习的文字的匹配项,JVM会将CONSTANT_Utf8_info的内容与StringTable的字符串实例的内容进行比较。
  • ^^^这是问题所在。 将来自cpool的原始UTF数据与可以由用户通过Reflection欺骗的Java char[]数组内容进行比较。

那么,你的测试中发生了什么?

  1. f.set("true", f.get("false"))MyClass启动文字“true”的解析。
  2. JVM在StringTable没有发现匹配序列‘true’的实例,并创建一个新的java.lang.String ,它存储在StringTable
  3. StringTable中String的value通过Reflection替换。
  4. System.out.println(true)PrintStream类中启动文字“true”的解析。
  5. JVM将UTF序列‘true’StringTable Strings进行比较,但没有找到匹配项,因为该String已经有‘false’值。 创建另一个‘true’字符串并放在StringTable

为什么我认为这是一个错误?

JLS§3.10.5和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的一个可能修复是在StringTable存储指向原始UTF序列的指针以及java.lang.String对象,这样实习过程就不会将cpool数据(用户不可访问)与value数组(可通过Reflection访问)进行比较。

我把它写成社区维基,因为我不知道它是否正确并且无论如何都不了解细节。

似乎发生的情况是,在运行时遇到字符串文字时,JVM会检查字符串池(使用equals )以查看该字符串是否已存在。 如果不存在,则使用新实例。 此对象(新对象或已存在于字符串池中的对象)是从现在开始将用于该类中所有相同字符串文字的对象。

考虑这个例子:

 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 } } 

这打印:

真正



真正

我的解释:

在第1行中,JVM在PrintStream类中遇到文字"true" 。 将新字符串添加到池中。 然后调用new MyClass() 。 在此构造函数中,JVM在MyClass类中遇到字符串文字"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 

这次输出是:

真正


真正

为什么第2行产生不同的结果? 这里的区别是在直到修改了后备数组之后PrintStream类中才会遇到文字"true" 。 所以“错误”字符串不是PrintStream类中使用的字符串。 但是,由于与上述相同的原因,第3行和第4行继续打印"false"