10

我的应用程序是多线程的,具有密集的字符串处理。我们正在经历过多的内存消耗,并且分析表明这是由于字符串数据造成的。我认为内存消耗将从使用某种享元模式实现甚至缓存中受益匪浅(我确信字符串经常被重复,尽管我在这方面没有任何硬数据)。

我看过 Java Constant Pool 和 String.intern,但似乎它会引发一些 PermGen 问题。

在java中实现应用程序范围的多线程字符串池的最佳选择是什么?

编辑:另请参阅我之前的相关问题:Java 如何在后台为字符串实现享元模式?

4

5 回答 5

8

注意:此答案使用的示例可能与现代运行时 JVM 库无关。特别是,该substring示例在 OpenJDK/Oracle 7+ 中不再是问题。

我知道这与人们经常告诉你的相反,但有时明确地创建新String实例可能是减少记忆的重要方法。

由于字符串是不可变的,因此有几种方法利用这一事实并共享支持字符数组以节省内存。但是,有时这实际上可以通过防止对这些数组的未使用部分进行垃圾收集来增加内存。

例如,假设您正在解析日志文件的消息 ID 以提取警告 ID。您的代码将如下所示:

//Format:
//ID: [WARNING|ERROR|DEBUG] Message...
String testLine = "5AB729: WARNING Some really really really long message";

Matcher matcher = Pattern.compile("([A-Z0-9]*): WARNING.*").matcher(testLine);
if ( matcher.matches() ) {
    String id = matcher.group(1);
        //...do something with id...
}

但是看看实际存储的数据:

    //...
    String id = matcher.group(1);
    Field valueField = String.class.getDeclaredField("value");
    valueField.setAccessible(true);

    char[] data = ((char[])valueField.get(id));
    System.out.println("Actual data stored for string \"" + id + "\": " + Arrays.toString(data) );

这是整个测试行,因为匹配器只是围绕相同的字符数据包装了一个新的 String 实例。替换为 时比较String id = matcher.group(1);结果String id = new String(matcher.group(1));

于 2010-05-26T19:05:14.503 回答
3

这已经在 J​​VM 级别完成。您只需要确保您不会new String每次都创建 s,无论是显式还是隐式。

即不这样做:

String s1 = new String("foo");
String s2 = new String("foo");

这将在堆中创建两个实例。而是这样做:

String s1 = "foo";
String s2 = "foo";

这将在堆中创建一个实例,并且两者都将引用相同的实例(作为证据,s1 == s2true在此处返回)。

也不要+=用于连接字符串(在循环中):

String s = "";
for (/* some loop condition */) {
    s += "new";
}

每次都会在堆中+=隐式创建一个new String。而是这样做

StringBuilder sb = new StringBuilder();
for (/* some loop condition */) {
    sb.append("new");
}
String s = sb.toString();

如果可以的话,宁愿使用StringBuilder它的同步兄弟StringBuffer而不是String“密集的字符串处理”。它为这些目的提供了有用的方法,例如append(), insert(),delete()等。另请参阅它的 javadoc

于 2010-05-26T18:10:48.550 回答
2

爪哇 7/8

如果您正在按照公认的答案进行操作并使用 Java 7 或更高版本,那么您并没有按照它所说的那样做。

的实施发生subString()了变化。

永远不要编写依赖于可能会发生巨大变化的实现的代码,如果您依赖旧的行为可能会使事情变得更糟。

1950    public String substring(int beginIndex, int endIndex) {
1951        if (beginIndex < 0) {
1952            throw new StringIndexOutOfBoundsException(beginIndex);
1953        }
1954        if (endIndex > count) {
1955            throw new StringIndexOutOfBoundsException(endIndex);
1956        }
1957        if (beginIndex > endIndex) {
1958            throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
1959        }
1960        return ((beginIndex == 0) && (endIndex == count)) ? this :
1961            new String(offset + beginIndex, endIndex - beginIndex, value);
1962    }

因此,如果您使用 Java 7 或更高版本的公认答案,您将创建两倍的内存使用量和需要收集的垃圾。

于 2015-05-30T16:18:34.647 回答
1

有效地将字符串打包到内存中!我曾经写过一个超内存效率的 Set 类,其中字符串存储为树。如果通过遍历字母到达叶子,则该条目包含在集合中。使用起来也很快,非常适合存储大型字典。

而且不要忘记,在我分析的几乎每个应用程序中,字符串通常是内存中最大的部分,所以如果你需要它们,请不要关心它们。

插图:

你有 3 根弦:啤酒、豆子和血。您可以创建这样的树结构:

B
+-e
  +-er
  +-ans
+-lood

对于例如街道名称列表非常有效,这对于固定字典显然是最合理的,因为插入不能有效地完成。事实上,结构应该创建一次,然后序列化,然后加载。

于 2010-05-26T20:23:36.390 回答
0

首先,确定如果您消除了一些解析,您的应用程序和开发人员将遭受多少损失。如果您在此过程中将员工流动率翻倍,那么更快的应用程序对您没有好处!我认为根据您的问题,我们可以假设您已经通过了此测试。

其次,如果您不能消除创建对象,那么您的下一个目标应该是确保它不会在 Eden 集合中存活。而 parse-lookup 可以解决这个问题。但是,“正确实施”的缓存(我不同意这个基本前提,但我不会因为随之而来的咆哮而使您厌烦)通常会带来线程争用。你会用一种记忆压力代替另一种记忆压力。

parse-lookup 惯用语的一个变体可以减少通常从完全缓存中获得的那种附带损害,这​​是一个简单的预先计算的查找表(另请参见“memoization”)。您通常看到的模式是类型安全枚举(TSE)。使用 TSE,您解析字符串,将其传递给 TSE 以检索关联的枚举类型,然后您将字符串丢弃。

您正在处理的文本是自由格式的,还是输入必须遵循严格的规范?如果您的许多文本呈现为一组固定的可能值,那么 TSE 可以在这里为您提供帮助,并为您提供更好的服务:在创建点而不是在使用点向您的信息添加上下文/语义.

于 2010-05-26T18:38:23.003 回答