字符串文字,实习和反思

时间:2016-04-01 22:20:46

标签: java string reflection jvm

我正在尝试找到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"实例与print的{​​{1}}方法中使用的实例完全相同?

PrintStream

我错过了什么?

修改

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

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

System.out.println(true); 之前,输出为

try

2 个答案:

答案 0 :(得分:6)

这可以说是一个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. 来自value的字符串的
  4. StringTable将通过Reflection替换。
  5. System.out.println(true)启动PrintStream课程中' true' 的解析。
  6. JVM将UTF序列' true' 与来自StringTable的字符串进行比较,但找不到匹配项,因为该字符串已经' false&# 39; 价值。 ' true' 的另一个字符串已创建并放置在StringTable中。
  7. 为什么我认为这是一个错误?

    JLS §3.10.5JVMS §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数据(用户无法访问)与{{1数组(可通过反射访问)。

答案 1 :(得分:1)

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

似乎发生的情况是,在运行时遇到字符串文字时,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遇到"true"类中的文字PrintStream。将新字符串添加到池中。然后调用new MyClass()。在此构造函数中,JVM遇到"true"类中的字符串文字MyClass。该字符串已经在池中,因此池中的实例是将要使用的实例,但关键的是它也是稍后将在第3行和第4行中使用的实例。然后修改支持此字符串的数组。因此,第2,3和4行都打印false。接下来,调用OtherClass.printTrue()并且JVM第一次在"true"中遇到字符串文字OtherClass。此字符串 equal到池中的字符串,因为池中的字符串现在具有后备数组[f, a, l, s, e]。因此,使用新的字符串实例,并在第5行打印true

现在假设我们注释掉第1行:

//        System.out.println(true);       // 1

这次输出是:

  


  假
  假
  真

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