【问题标题】:How to use less memory when assign big new String分配大的新字符串时如何使用更少的内存
【发布时间】:2013-10-24 19:43:21
【问题描述】:

Java 字符串是不可变的,所以

当您创建一个字符串时,会在堆中为其分配一块内存,当您更改其值时,会为该字符串创建一个新的内存块,并且旧的内存块可以进行垃圾回收,例如例子

String str = func1_return_big_string_1()"; //not literal
String str= func2_return_big_string_2()"; //not literal

但是由于垃圾收集需要时间来启动,所以我们实际上在堆中拥有包含大字符串 1 和 2 的内存。如果这种情况经常发生,它们对我来说可能是个问题。

有没有办法让大字符串 2 在内存中使用字符串 1 的相同位置,这样我们在将大字符串 2 分配给 str 时就不需要额外的空间。

编辑: 感谢所有输入,最后我意识到我不应该期望 java 代码表现得像 c++ 代码(即不同的内存占用)。我编写了一个 c++ 11 演示,它按预期工作,最大的内存占用约为 20M(我试图加载的最大文件),右值引用和移动赋值运算符都按预期启动。 下面的演示是在 VS2012 中使用 c++ 11 完成的。

#include "stdafx.h"
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <thread>
using namespace std;

string readFile(const string &fileName)
{
    ifstream ifs(fileName.c_str(), ios::in | ios::binary | ios::ate);

    ifstream::pos_type fileSize = ifs.tellg();
    ifs.seekg(0, ios::beg);

    vector<char> bytes(fileSize);
    ifs.read(&bytes[0], fileSize);

    return string(&bytes[0], fileSize);
}

class test{
public:
    string m_content;
};

int _tmain(int argc, _TCHAR* argv[])
{
    string base("c:\\data");
    string ext(".bin");
    string filename;
    test t;
    //std::this_thread::sleep_for(std::chrono::milliseconds(5000));
    cout << "about to start" << endl;
    for(int i=0; i<=50; ++i) {
        cout << i << endl;
        filename = base + std::to_string(i) + ext;
        //rvalue reference & move assignment operator here
        //so no unnecessary copy at all
        t.m_content = readFile(filename);
        cout << "szie of content" << t.m_content.length() << endl;
    }
    cout << "end" << endl;
    system("pause");
    return 0;
}

【问题讨论】:

  • 这些不是我们所说的字符串 literals,对吧?大概是从某种方法返回的大字符串,对吗?
  • 改用 Char 数组?查看 API,我只看到 String.replace() 作为可能的候选者,但这需要你使用正则表达式,我不知道内存分配是如何工作的
  • @arshajii:是的,没错
  • 其次,您是否做过任何测试以确定这里确实存在内存问题? GC 在这方面应该非常有效。
  • 不,但我可以预见这可能会发生,至少我不想在没有必要的情况下回复 GC。

标签: java string memory


【解决方案1】:

使用StringBuffer,StringBuffer.append()

【讨论】:

  • 我认为他想将str 完全重新分配给一个新字符串,而不是以某种方式改变一个预先存在的字符串。
  • Java 有可能吗? Java 的字符串是不可变的。使它们可变的唯一选择是使用(如@Praveen 建议的)StringBuffer 或 StringBuilder 类。
【解决方案2】:

我看到了几个选项:

  1. 使用char[]
  2. StringBuilder 复制到您的版本MyStringBuilder 中,并使用公共可重用缓冲区。主要缺点是它缺少正则表达式。当我需要提高性能时,我就是这样做的。
  3. JDK
  4. 使用公共可重用缓冲区将String 复制到MutableString。我认为添加自定义正则表达式匹配器不会有问题,因为有很多可用的匹配器。

【讨论】:

  • 我不需要担心这种特殊情况下的正则表达式,所以你能举一个你的 2> 解决方案的例子吗?
  • 这是sample。添加了reset() 方法和其他一些方法以重用StringBuilder
【解决方案3】:

为了避免新旧字符串同时在内存中,您可以通过将null 分配给变量来显式允许GC 清理它:

String str;
str = func1_return_big_string_1();
str = null; // Now, GC can clean, when it needs extra memory for the String.
str = func2_return_big_string_2();

更新:为了支持我的主张,我编写了一个测试用例来证明我是对的:http://ideone.com/BwGfSN。 该代码演示了(使用终结器)之间的区别:

GCTest test;
// Without the null assignment
test = create(0);
test = create(1);
test = null;
System.gc();

try {Thread.sleep(10);} catch (Exception e){}
System.out.println();

// With the null assignment
test = create(2);
test = null;
test = create(3);
test = null;
System.gc();

【讨论】:

  • 是的,你可以。致电System.gc()。但这不是我在这里展示的。这个想法是释放之前的 String 对象,以便在 func2 中创建字符串时允许 GC 启动。
  • 有趣的是,每个人都在投票,而这确实是他问题的技术答案。
  • 可能是因为 JVM 应该为用户执行此 null 分配,所以他们投了反对票? JIT 可以理解 str 在当前评估范围内不再被引用,可以进行 GC-free。
  • @AndreyChaschev:不,那不是真的。而在func2_return_big_string_2() 方法中,对另一个大字符串(存储在str 中)的引用仍然存在。只有当方法返回时,str 的值才会被覆盖。因此,在创建下一个大字符串时,GC 无法释放前一个。因此,将其分配给null 允许GC 在func2_return_big_string_2() 分配下一个时摆脱它。
  • @MartijnCourteaux:我们是否同意 JIT 是 优化编译器?我们是否也同意 JVM 可以在没有它的情况下运行?如果没有,请查看here。前几次迭代总是被解释,同意吗?
【解决方案4】:

对于非实习字符串来说,这并不重要。如果您开始耗尽内存,垃圾收集器将删除不再引用的所有对象。

Interned Strings 更难收集,详见Garbage collection of String literals

EDIT 非驻留字符串就像一个普通对象。一旦不再引用它,它就会被垃圾回收。

如果 str 是唯一指向原始 String 的引用,并且 str 被更改为指向其他东西,则原始 String 有资格进行垃圾回收。因此,您不再需要担心内存不足,因为 JVM 会在需要内存时收集它。

【讨论】:

  • @Gob00st 然后我的第二句话就是我的答案
  • 我不确定我得到你的答案,你愿意解释更多吗?还编辑了我的问题。
  • @Yes 没有参考 GC 最终会收集我希望它可以快速完成这项工作,这样内存就不必飙升。
【解决方案5】:

我刚刚找到了一个MutableString 实现。它在Maven Central 中可用。以下是他们的 JavaDoc 页面的摘录:

  • 可变字符串占用空间很小——它们的唯一属性是一个支持字符数组和一个整数;
  • 他们的方法试图尽可能高效:例如,如果对数组访问的限制暗示了对参数的某些限制,我们不会显式检查它,并且使用布隆过滤器来加速多字符替换;
  • 它们允许您直接访问后备阵列(风险自负);
  • 它们实现了CharSequence,,因此,例如,您可以使用标准 Java API 将可变字符串与正则表达式匹配或拆分;
  • 它们实现了Appendable,因此它们可以与Formatter 和类似的类一起使用;

更新

您可以利用MutableStringAppendable 接口以几乎零内存开销(8KB,这是Java 中的默认缓冲区大小)读取文件。使用 Guava 的 CharStreams.copy 看起来像这样:

MutableString str = new MutableString((int) file.length());
CharStreams.copy(Files.newReaderSupplier(file, Charset.defaultCharset()), str);
System.out.println(str);

Full working example.

【讨论】:

  • 太棒了!接受答案会激发我们未来的贡献! :-)
  • 虽然它并没有节省太多,因为当可变字符串进行追加时,它会将从文件加载的 char 数组复制到它自己的数组中。我希望它可以在c ++ 11中使用右值引用来移动语义:意味着将它的数组指向临时对象的数组(从文件加载),但它没有做聪明的事情......任何支持你。干杯
  • 我已经更新了一个答案,其中包含一个缓冲读取到此MutableString 的示例。 HTH
  • 代码无法编译:CharStreams 类型的通用方法 copy(InputSupplier, OutputSupplier) 不适用于参数 (InputSupplier, MutableString)。推断的类型 FileInputStream 不是有界参数 的有效替代品
  • 我的错,这个是用于内存缓冲区的。我已经更新并测试了它的文件。 Also available as Gist.
猜你喜欢
  • 2017-10-29
  • 1970-01-01
  • 1970-01-01
  • 2021-07-27
  • 1970-01-01
  • 2012-07-10
  • 1970-01-01
  • 2021-01-11
  • 2011-03-22
相关资源
最近更新 更多