【问题标题】:How to efficiently manage memory using Strings?如何使用字符串有效地管理内存?
【发布时间】:2014-06-16 07:38:51
【问题描述】:

考虑一段示例代码。

public void testString()
{     
    int i = 0;
    while(i < 100000000)
    {
        String s ="Hi hello bye" +i;
        i++;          
    }
}

在每次迭代中,都会创建一个新字符串,并且下一次迭代不再需要它的值。我尝试打印之前消耗的内存并发布此 testString() 功能。以下是他们的价值观。

Before invoking testString():

Total Memory: 91684864 (87.4375 MB)
Max Memory:   1360855040 (1297.8125 MB)
Free Memory:  72163552 (68.82052612304688 MB)

After invoking testString():
Total Memory: 424280064 (404.625 MB)
Max Memory:   1360855040 (1297.8125 MB)
Free Memory:  171766816 (163.80960083007812 MB).

我看到正在使用大量内存,我担心由于当前处理字符串的方式,JVM 堆可能会超出范围。 迭代 1 生成的 String 在迭代 2 中不再需要,可以释放其存储空间。我相信这不会发生在这里。

我尝试使用 StringBuffer 和 StringBuilder 对象,但内存使用量似乎略有改善。

请帮助我更好和最佳的方法。

【问题讨论】:

  • Java 在需要之前不会进行垃圾收集。它不会垃圾收集您立即生成的每个字符串。您的测试方法实际上并没有为您提供所需的信息。
  • 如果您附加字符串,编译器将默认使用StringBuilder。所以无需显式使用它,this 一定能帮到你
  • @user3091574:那根本没有任何好处。
  • 我认为您问题中的关键词是您“害怕 [the] JVM Heap 可能超出范围”。我不相信 JVM 可以在不首先运行 GC 的情况下抛出 OutOfMemory,因此对于您给出的示例,这应该是不可能的。还是你的内存真的用完了?

标签: java string performance memory-leaks jvm


【解决方案1】:

这取决于您如何使用 StringBuilder。这个

    StringBuilder sb = new StringBuilder("");
    while (i < 100000000) {
        sb.delete(0, sb.length());
        sb.append("Hi hello bye").append(i);
        i++;
    }

在内存消耗和速度方面都会更有效率

【讨论】:

  • 是的,但它不允许您实际将值用作字符串。如果您需要循环中的字符串(很可能),那么您将不会获得任何好处。如果你打算使用这样的代码,为什么不直接从“bye”之后的字符中删除呢?无需一直追加...
  • @Evgeniy,我仍然觉得字符串在内存中未被引用。这是运行这段代码后的内存信息。总内存:382009344 (364.3125 MB) 最大内存:1360855040 (1297.8125 MB) 可用内存:264932400 (252.6592254638672 MB) 空间减少约 30 MB。
【解决方案2】:

迭代 1 生成的 String 在迭代 2 中不再需要,可以释放其存储空间。我相信这不会发生在这里。

这肯定正在发生

您正在创建 1 亿个字符串,每个字符串至少包含 13 个字符,其中大部分大约为 20 个字符。每个字符串由一个对象(有开销)和一个char[] 组成——所以我猜一个 20 个字符的字符串大约需要 60 个字节。

如果垃圾回收无效,则 1 亿个对象需要 60 字节,每个对象需要 6GB - 而您看到的总内存仅比开始时大 300MB。

字符串正在被收集——只是不是立即

您还没有告诉我们您需要对 real 代码中的字符串做什么(我假设这样做有真正的动机) - 假设您实际上在每次迭代中都需要一个字符串一个循环,我不认为使用 StringBuilder 会帮助你。如果您需要数据是 StringBuilder,那么您可以提高效率,但很少会创建 StringBuilder 但不要在其上调用 toString

【讨论】:

  • 感谢您的提醒。你真的让我相信确实会发生一些清洁工作。 :) 是的,确实需要每次迭代中的字符串。然而,实际的字符串是一个 3 表连接查询,并且字符串大小超过了提供的示例。有什么方法可以确定JVM什么时候清除垃圾?(如处理多少条记录后)
  • @Vignesh 有 JVM 选项来记录 GC 操作
  • @Jon Skeet,为时已晚,但我们为什么要在这里倍增?第一个将在池中,然后只有 4 字节 int.. 需要分配的唯一 i 部分.. 不是吗?
  • @Sendi_t:不,字符串不是这样工作的。每个字符串都包含完整的文本。 (至少,它们曾经是这样工作的……现在内部结构可能发生了变化……)
【解决方案3】:

字符串是不可变的,当你要连续添加一个字符串时,每次迭代都会创建一个字符串对象,所以内存已经超出。 如果您使用 StringBuilder,即使它处理多个迭代,它也可以在单个对象中工作。 StringBuilder 是可变的。

StringBuilder iter = new StringBuilder("");
while (i < 100000000) 
{
    iter.delete(0, iter.length());
    iter.append("Hi hello bye").append(i);
    i++;
} 

【讨论】:

  • 如果您不打算对每个字符序列做任何事情,您不妨说i = 100000000; StringBuilder iter = new StringBuilder("Hi hello bye" + (i - 1)); 并完成。如果你是,在任何理智的情况下,你仍然在创建一个额外的字符串。您只是不是每次都创建一个新的 StringBuilder ——这减少了垃圾,但没有达到避免内存使用量激增的程度。
【解决方案4】:

由于变量的作用域只是用于while循环的迭代,所以在这里你不必担心内存溢出,因为在下一次垃圾收集器执行时它会释放所有内存:

while(i < 100000000)
{
    String s ="Hi hello bye" +i;
    i++;          
}// no more required the s afterward

在每次迭代中,String s 都会创建一个新对象,但现在不需要之前的对象,所以它在内存中,直到垃圾收集器清理它。

【讨论】:

    【解决方案5】:

    JVM 不应该因为未引用的对象(例如示例程序中的字符串)而耗尽堆内存,因为在它抛出 OutOfMemory 异常之前,它会运行垃圾回收。从Java Virtual Machine Specification, section 6.3 中查看此异常的语义:

    OutOfMemoryError:Java 虚拟机实现已用完虚拟或物理内存,并且自动存储管理器无法回收足够的内存来满足对象创建请求。

    【讨论】:

    • 应该几乎永远...参见例如stackoverflow.com/questions/13531004/…
    • 好吧,我说的是“未引用”对象。在您的示例中,当OutOfMemory 被抛出时,这是因为对对象的引用仍然存在于当前方法的堆栈框架中。
    【解决方案6】:

    第一次运行会发生什么

    JVM 运行代码,生成字符串,并且垃圾收集器在一定的时间间隔释放使用的内存。除了一些浪费的执行时间,程序将正常运行。

    如果函数被频繁调用会发生什么

    JVM 将开始优化循环,意识到这些字符串永远不会做任何事情,并将整个函数标记为死代码。最终调用该函数实际上什么都不做,因为 JVM 将内容转换为简单的 return

    请帮助我更好和最佳的方法。

    因为即使是 JVM 也不知道你的代码应该做什么......你想要做什么?对于您手头的实际问题,可能有一个最佳解决方案,这与您最初发布的代码示例有很大不同。

    【讨论】:

    • :非常感谢您提供的宝贵信息。是的,上面提供的代码可能并不代表理想的情况。放入循环中的所有字符串都是选择查询,需要在每次迭代时动态生成。注意:每次迭代生成的字符串是不同的。我有一个类似的实现,JVM Crashed 并显示 Heap out of Bounds 的错误报告。这篇文章背后的意图是找出内存泄漏发生在哪里?如果你说 JVM 会处理它,那么理想情况下它不应该崩溃。
    • 不动态生成选择查询。曾经。 en.wikipedia.org/wiki/SQL_injection
    • 当这些字符串未使用和未引用时,JVM 会处理它。在您使用它们之后它们是否真的没有被引用,或者您传递给它们的库是否可以保留它们(或与它们相关的东西)?
    • 如果您有动态选择查询,其中每次调用只有一个(或几个)值更改,那么您可以使用存储过程(又名 PLSQL)。在这种情况下,您只发送一个(相同的)字符串和变量,其余的由数据库完成。这不仅会加速 Java,还会加速数据库查询。
    • @James_pic 完全有可能动态生成参数化查询。
    【解决方案7】:

    使用字符串生成器是您拥有的最佳选择。由于您需要的字符串/字符串构建器的数量非常庞大,因此您不能指望 JVM 在任何情况下都几乎避免使用大量内存。 参考上面的统计数据:

    使用字符串:总内存中可用内存的百分比为40.48% ((163.80960083007812 MB/404.625 MB )*100)

    使用字符串生成器:总内存中可用内存的百分比为69.35 % ((252.659 MB/364.3125 MB)*100),这是一个相当显着的改进。此外,上述变量的使用只在循环范围内,因此 JVM 的垃圾收集器将在需要时运行以清除内存。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-08-08
      • 2010-10-11
      • 2011-10-22
      • 2012-07-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-12
      相关资源
      最近更新 更多