Java如何存储字面值?

3
System.out.println(5678);

这个字面值直接被用于打印语句中。但是Java是直接将其存储在内存中还是创建一个自动变量然后将其存储在那里?如果第二种情况是真的,那么如果有人意外地使用相同的变量名访问该变量会发生什么?


5
这不是一个变量,而是一个编译时常量。Java 不会将它转换成变量。 - khelwood
如果你写了 System.out.println(2 + 3),那么值2和3在编译后的代码中甚至不存在(因为它们自动变成了5)。 - MC Emperor
1
虽然不是很相关,但字节范围整数值被缓存,并且可以通过反射修改缓存。请参见这个令人难忘的代码高尔夫答案。 - Mena
1
@khelwood,所以我猜用户无法访问这个编译时常量? - user12208242
1
@user12208242 在Java内部并不行,但是如前所述,从外部通过将本地代码注入到JVM进程中并干扰其内存(与所有程序一样)理论上可能劫持它。 - Zabuzard
3个回答

6

解释

Java在幕后所做的一切都完全对用户和程序员隐藏。

只要你仍然写Java代码,就不可能搞乱它。当然,如果你尝试连接JVM进程并注入C代码或通过其本地接口与其交互,情况可能会有所不同。

在Java中,内存管理也完全由JVM处理。程序员无法直接管理内存。


字节码

变量内联

话虽如此,让我们来看一下这个片段的结果字节码(请参见javabytes.io或使用javap -c Test.class):

// Source code
public class Test {
    public static void main(String [] args) {
        int value = 4000;
        System.out.println(value);

        System.out.println(5678);
    }
}

// Byte code
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1    // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: sipush        4000
       3: istore_1
       4: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
       7: iload_1
       8: invokevirtual #3    // Method java/io/PrintStream.println:(I)V
      11: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
      14: sipush        5678
      17: invokevirtual #3    // Method java/io/PrintStream.println:(I)V
      20: return
}

正如您所看到的,这两个变量实际上是相同的。`sipush` 命令直接加载值,没有任何变量的迹象。
`sipush` 直接将 `short` 类型的值推送到堆栈中,然后被 `invokevirtual` 指令捡起来调用 print 方法(详见 Java bytecode instruction list)。
为什么会这样呢?因为编译器很聪明。它发现变量 `value` 没有任何作用,于是完全将其删除了。它将代码改为 `System.out.println(4000)` 并完全删除了 `value`。

变量存在

但我们想要看到一些变量,因此让我们将其复杂化一些,以便 Java 无法再内联该变量,通过引入只能在运行时计算的依赖关系:
// Source code
public class Test {
    public static void main(String [] args) {
        int value = (int) System.currentTimeMillis();
        System.out.println(value);

        System.out.println(5678);
    }
}

// Byte code
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1    // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #2    // Method java/lang/System.currentTimeMillis:()J
       3: l2i
       4: istore_1
       5: getstatic     #3    // Field java/lang/System.out:Ljava/io/PrintStream;
       8: iload_1
       9: invokevirtual #4    // Method java/io/PrintStream.println:(I)V
      12: getstatic     #3    // Field java/lang/System.out:Ljava/io/PrintStream;
      15: sipush        5678
      18: invokevirtual #4    // Method java/io/PrintStream.println:(I)V
      21: return
}   

最后我们看到了一些变量操作!该变量由方法计算,转换为int,然后通过istore_1进行存储。然后,通过iload_1将其动态加载到堆栈上,并传递给打印方法。

因此,对于变量,我们需要使用istoreiload将其传递给方法。对于文字,我们可以直接使用sipush将其加载到方法中。


2
我编写了以下测试文件:
class test {
    public static void main(String[] args) {
        System.out.println(5678);
        System.out.println("test string");
    }
}

使用“javap -c”反编译它的结果如下:
    public static void main(java.lang.String[]);
      Code:
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: sipush        5678
         6: invokevirtual #13                 // Method java/io/PrintStream.println:(I)V
         9: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        12: ldc           #19                 // String test string
        14: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        17: return

如果你添加了 -verbose 标志,它也会打印常量池,你可以查找 #19 并看到它是字符串 test string
如果你对更多这样的细节感兴趣:JVM规范 中有更多细节。

1

简短回答:不行!你不能在编译时访问它们。

Java将字面量存储在permgen space内存中,无法通过变量名称在编译时访问。这也取决于JVM如何实现它。

例如,在下面的代码中,如果我们谈论String字面量,Java可能会将"sameSame"存储在某个内存位置(String Pool)中,然后对两个方法都使用它,而不是创建相同的字符串两次。

private static String test1(){
    return "sameSame";
}

private static String test2(){
    return "sameSame";
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接