Java编译器能有效地处理内联字符串吗?

6

1.

static final String memFriendly = "Efficiently stored String";
System.out.println(memFriendly);

2.

System.out.println("Efficiently stored String");

Java编译器会以相同的方式处理这两种情况吗?

注:我指的是运行时内存利用率和代码执行时间的有效性。例如,第一种情况是否会在堆栈加载变量memFriendly时花费更多时间?

4个回答

14

这在Java语言规范中有详细说明:

  

每个字符串字面值都是对 String 类(§4.3.3)的实例(§4.3.1、§12.5) 的引用(§4.3)。String 对象具有恒定的值。字符串字面值或更一般地说,是常量表达式(§15.28)的值,“interned”以共享唯一实例,使用方法 String.intern。

你还可以使用 javap 工具自行验证。

对于此代码:

System.out.println("Efficiently stored String");
final String memFriendly = "Efficiently stored String";
System.out.println(memFriendly);

javap 给出以下输出:

0:   getstatic     #2; //Field java/lang/System.out:Ljava/io/PrintStream;
3:   ldc       #3; //String Efficiently stored String
5:   invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
8:   getstatic     #2; //Field java/lang/System.out:Ljava/io/PrintStream;
11:  ldc       #3; //String Efficiently stored String
13:  invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V

感谢您的深入挖掘。 我的使用情况是,我在代码中有一些字符串(将被调用多次),仅使用一次,比如logger.info("Log this")。 现在,将此字符串设置为静态常量是否会提高性能? - Monis Iqbal
使用对静态final字段的引用与内联文字没有任何性能差异。由于Hotspot在运行时执行了大量优化,因此检测是否存在可测量的差异的唯一方法是进行测量。有关微基准测试的相关问题:https://dev59.com/hHRB5IYBdhLWcg3wz6UK - McDowell
谢谢。我还使用了javap并比较了静态final和内联版本的输出。两者完全相同。 你说得对,Hotspot的运行时优化确实让人困惑,我一开始也是这样。我怎么能保证一种方法论更好,以便我可以将其用作最佳实践呢? - Monis Iqbal
1
@Monis:如果在你的应用程序中频繁调用logger.info("Log this");,那么在方法调用中耗费的时间,特别是日志记录到磁盘上的输出将会远远超过字符串存储方式之间的速度差异。 - Grant Wagner
@Grant k,如果字符串非常长怎么办? - Monis Iqbal

5
public static void main(String[] args) {
    System.out.println("Hello world!");

    String hola = "Hola, mundo!";
    System.out.println(hola);
}

这是javap显示的该代码的反汇编结果:
0:   getstatic       #16; //Field java/lang/System.out:Ljava/io/PrintStream;
3:   ldc     #22; //String Hello world!
5:   invokevirtual   #24; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
8:   ldc     #30; //String Hola, mundo!
10:  astore_1
11:  getstatic       #16; //Field java/lang/System.out:Ljava/io/PrintStream;
14:  aload_1
15:  invokevirtual   #24; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
18:  return

看起来第二个字符串被存储了,而第一个字符串直接传递给了方法。这是使用Eclipse编译器构建的,这可能解释了我的答案和McDowell的答案之间的差异。
更新:如果将"hola"声明为"final",则结果如下(如果我理解正确,则不会有"aload_1",这意味着该字符串既被存储又被内联,正如您所期望的那样):
0:   getstatic       #16; //Field java/lang/System.out:Ljava/io/PrintStream;
3:   ldc     #22; //String Hello world!
5:   invokevirtual   #24; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
8:   ldc     #30; //String Hola, mundo!
10:  astore_1
11:  getstatic       #16; //Field java/lang/System.out:Ljava/io/PrintStream;
14:  ldc     #30; //String Hola, mundo!
16:  invokevirtual   #24; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
19:  return

那很正常,因为这两个字符串的文本不同,而在麦克道尔(McDowell)中它们是相同的。 - gizmo
哇!感谢您挖掘这个问题。 我的使用情况是,我的代码中有一些字符串(将被调用多次),仅使用一次,例如logger.info("Log this")。 现在,将此字符串设置为静态常量是否会提高性能? - Monis Iqbal

3
在这种情况下,编译器将同时处理两者。
任何时候在编译时定义字符串,Java都会优化字符串的存储。
如果在运行时定义字符串,Java就无法进行相同的优化。

我也这么认为,但是我在理论上找不到确切的参考。谢谢。 这是一篇带有参考资料的好文章,但不是我要找的: http://muzso.hu/2009/03/05/strange-behaviour-in-java-compilers-string-concatenation-caused-by-compile-time-constants - Monis Iqbal
抱歉,我找不到任何具体的文章或规格。 - jjnguy

-2

5
不,不,不行!出于“性能原因”而随意调整字符串的存储方式(这个术语几乎让我反感)是错误的。除非你正在处理大量字符串,否则它只会给你带来麻烦。 - Bombe
你有什么除了厌恶之外的东西来支持你毫无根据的说法吗?我给出了一个使用intern的好理由,即同一组字符串将需要反复重用的情况。为什么每次都要创建新的呢? intern的唯一缺点是被intern的字符串永远不能被垃圾回收。我猜这就是你抱怨的原因。但如果你将在程序的整个生命周期内重复使用相同的字符串,则将它们intern而不是每次创建新的确实是有意义的。 - anio
看看这个链接,它列出了intern的优缺点。http://mindprod.com/jgloss/interned.html - anio
1
“为什么不调用intern()”这个问题的答案在您提供的“Java词汇表”文章中已经给出——所有在编译时存在的字符串文字都会自动进行内部化。好好想一想,如果您有一个字符串,它不是在编译时存在的,而是基于某些运行时信息(用户输入、控制台参数等)变化的。如果您只创建了一次该字符串并将其传递,那么调用intern()就没有任何意义。如果您重复创建相同的运行时字符串,则可能违反了DRY原则,最好的优化方法可能是消除重复。 - David Moles

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