【问题标题】:Is the argument of append in StringBuilder a new String object and will it be garbage collected?StringBuilder 中 append 的参数是否是一个新的 String 对象,它会被垃圾回收吗?
【发布时间】:2018-03-06 21:35:09
【问题描述】:

在下面的代码中,当它到达注释时,如果没有运行GC,大约会创建1000个对象(根据OCA书),StringBuilder被修改并保持为一个对象,空字符串@987654322 @ 被汇集和重用,这就是所有的解释。不是参数s 是需要GCed 的new String("s")i,它不会先转换为new String 对象,然后与" " 结合创建另一个new String 对象使它们该行有 2 个 String 对象,符合 GC 条件以及 append 的参数,每个循环中总共有 3 个 String 对象。那么当代码到达注释行时总共有 3000 个对象?

public class Mounds {

    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        String s = new String();
        for (int i = 0; i < 1000; i++) {
            s = " " + i;
            sb.append(s);
        }
// done with loop
    }
}

【问题讨论】:

  • 任何人都知道为什么在循环中这样写,而不是sb.append(" " + i) ??
  • @PrzemysławMoskal 变化不大...
  • 这是一道测试题,来自一本书
  • @luk2302 不是吗?由于字符串是不可变的,因此不会将新字符串分配给s 导致在每个循环中创建新字符串,在需要时可以对它进行垃圾收集。另一方面,使用StringBuilder.append() 不会在每个循环AFAIK 中创建新的String 实例。如果我错了,请纠正我。
  • 未创建新的String 以将参数传递给append。我不知道你从哪里得到这个想法。 s 存储一个参考值。 Java 是按值传递的,因此传递该引用值的副本,它引用同一个String 对象。

标签: java string garbage-collection stringbuilder


【解决方案1】:

如果我们编译这段代码并查看生成的字节码,我们可以准确地检查它

  public static void main(java.lang.String[]) throws java.io.IOException;
Code:
   0: new           #19                 // class java/lang/StringBuilder
   3: dup
   4: invokespecial #21                 // Method java/lang/StringBuilder."<init>":()V
   7: astore_1
   8: new           #22                 // class java/lang/String
  11: dup
  12: invokespecial #24                 // Method java/lang/String."<init>":()V
  15: astore_2
  16: iconst_0
  17: istore_3
  18: goto          47
  21: new           #19                 // class java/lang/StringBuilder
  24: dup
  25: ldc           #25                 // String
  27: invokespecial #27                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
  30: iload_3
  31: invokevirtual #30                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  34: invokevirtual #34                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  37: astore_2
  38: aload_1
  39: aload_2
  40: invokevirtual #38                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  43: pop
  44: iinc          3, 1
  47: iload_3
  48: sipush        1000
  51: if_icmplt     21
  54: return

我们关心的指令是从 21 到 40。在 21 中,创建了第二个 StringBuilder,我们稍后再讲。

在 25 中我们看到,有一个 ldc,这意味着一个字面量被压入堆栈,在本例中是字面量 String " "。

然后真正的魔法发生了。调用第二个 StringBuilder 的构造函数,它将堆栈中的文字作为参数。然后使用 iload_3 从局部变量数组加载 int i,然后调用第二个 StringBuilder 的 append 方法将 i 附加到它,然后调用 toString。使用 astore_2 和 aload_1 存储 toString 调用的返回值,并加载第一个 StringBuilder,然后再次加载 String。最后调用第一个 StringBuilder 的 append 方法,将新的 String 添加到 StringBuilder。

事实证明,在每个循环中都会创建一个新的 StringBuilder,因为每次使用 " " + i 时都必须创建一个 StringBuilder 来连接 String 和 int。另外中间StringBuilder的toString方法会创建一个新的String,所以总共会有2000个Object。

更好的版本应该是这样的:

for (int i = 0; i < 1000; i++) {
        sb.append(' ');
        sb.append(i);
    }

这将创建以下字节码:

  public static void main(java.lang.String[]) throws java.io.IOException;
Code:
   0: new           #19                 // class java/lang/StringBuilder
   3: dup
   4: invokespecial #21                 // Method java/lang/StringBuilder."<init>":()V
   7: astore_1
   8: new           #22                 // class java/lang/String
  11: dup
  12: invokespecial #24                 // Method java/lang/String."<init>":()V
  15: astore_2
  16: iconst_0
  17: istore_3
  18: goto          37
  21: aload_1
  22: bipush        32
  24: invokevirtual #25                 // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
  27: pop
  28: aload_1
  29: iload_3
  30: invokevirtual #29                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  33: pop
  34: iinc          3, 1
  37: iload_3
  38: sipush        1000
  41: if_icmplt     21
  44: return

我们可以看到,现在只有一个StringBuilder,它的append方法被调用了两次,所以这里没有分配内存,这样应该会更好。

【讨论】:

  • “现在只有一个 StringBuilder” ...并且没有中间字符串被创建和传递,对吧?
  • @luk2302 完全正确,因为也没有 toString 调用
  • 您的意思是在该循环中甚至没有创建一个新的 String 对象?我真的很困惑,看这是 Oracle OCA/OCP Java SE Programmer I & II Study Guide 一书中的解释答案,StringBuilders 是可变的,所以所有的 append() 调用都一遍又一遍地作用于同一个 StringBuilder 对象.然而,字符串是不可变的,因此每个字符串连接操作都会产生一个新的字符串对象。此外,字符串“”被创建一次,并在每次循环迭代中重复使用。
  • 你是对的,StringBuilder 是可变的,你明确使用的那个不会被丢弃或重新创建。但是你必须明白,字符串连接的工作原理是创建一个新的 StringBuilder,将所有内容附加到它上面,然后在其上调用 toString。在 toString 调用中会创建一个新的 String ,并且由于每次都会更改,因此无法重用。
  • ...现在与 Java 9 生成的字节码进行比较。
【解决方案2】:

最佳用法是:

    StringBuilder sb = new StringBuilder(4000);
    for (int i = 0; i < 1000; ++i) {
        sb.append(' ').append(i);
    }
    ... do something with sb.toString()

作为:

  • String s = new String(); 创建一个不必要的空字符串。与String s = ""; 相同。 (未考虑优化。)
  • s = " " + i; 将两个字符串连接成一个新字符串。一项任务应该留给 StringBuilder,因为这是它的唯一目的。
  • 附加一个字符' ' 比一个字符串" " 更有效。
  • new StringBuilder(4000) 具有可以在此处使用的初始容量,防止在追加时出现间歇性重新分配。 1000 个数字,其中 900 个是 3 位数字,加上一个空格,可以容纳 4000 个字符。

【讨论】:

  • sb.append(' ').append(i);而不是sb.append(' '); sb.append(i)链接它实际上很重要...
  • @Eugene 为什么会这样?
  • @luk2302 因为 JVM 可以发现 sb.append.append.append 模式与 sb.append; sb.append 模式,它们将被优化
【解决方案3】:

编译器可能意识到变量s 的范围在循环内,因此它将赋值内联到append() 以产生

sb.append(" " + i)

所以现在只有 int 的转换在每次迭代中创建一个新的 String。

【讨论】:

  • 而与空字符串的连接会创建另一个!? (不是反对者)
  • 不一定,可能是更多的编译器魔法,因为连接的结果在从 int 转换之前就已经知道了
  • 编译器不做这样的事情。这也将具有与原始 sn-p 完全相同的行为。您必须复制对串联创建的String 的引用。
  • 是的,这就是我的意思,整体连接对象在哪里?还传递 String 参数一定也创建了另一个对象,不是吗?
  • @SamuelIronhead 不,不是另一个对象,只是对同一个现有对象的引用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-31
  • 1970-01-01
相关资源
最近更新 更多