【问题标题】:How to detect whether String.substring copies the character data如何检测String.substring是否复制了字符数据
【发布时间】:2013-11-28 07:28:54
【问题描述】:

我知道,对于 Oracle Java 1.7 更新 6 及更高版本,在使用 String.substring 时, 复制 String 的内部字符数组,对于旧版本,它是共享的。 但我没有找到可以告诉我当前行为的官方 API。

用例

我的用例是: 在解析器中,我喜欢检测String.substring 是否复制或共享底层字符数组。 问题是,如果字符数组是共享的,那么我的解析器需要使用new String(s) 显式“取消共享”以避免 记忆问题。但是,如果String.substring 无论如何都会复制数据,那么这不是必需的,并且可以避免在解析器中显式复制数据。用例:

// possibly the query is very very large
String query = "select * from test ...";
// the identifier is used outside of the parser
String identifier = query.substring(14, 18);

// avoid if possible for speed,
// but needed if identifier internally 
// references the large query char array
identifier = new String(identifier);

我需要什么

基本上,我想要一个静态方法boolean isSubstringCopyingForSure() 来检测是否不需要new String(..)。如果有SecurityManager,检测不起作用,我没关系。基本上,检测应该是保守的(为了避免内存问题,我宁愿使用new String(..),即使没有必要)。

选项

我有几个选择,但我不确定它们是否可靠,特别是对于非 Oracle JVM:

检查 String.offset 字段

/**
 * @return true if substring is copying, false if not or if it is not clear
 */
static boolean isSubstringCopyingForSure() {
    if (System.getSecurityManager() != null) {
        // we can not reliably check it
        return false;
    }
    try {
        for (Field f : String.class.getDeclaredFields()) {
            if ("offset".equals(f.getName())) {
                return false;
            }
        }
        return true;
    } catch (Exception e) {
        // weird, we do have a security manager?
    }
    return false;
}

检查 JVM 版本

static boolean isSubstringCopyingForSure() {
    // but what about non-Oracle JREs?
    return System.getProperty("java.vendor").startsWith("Oracle") &&
           System.getProperty("java.version").compareTo("1.7.0_45") >= 0;
}

检查行为 有两种选择,都比较复杂。一种是使用自定义字符集创建一个字符串,然后使用子字符串创建一个新字符串 b,然后 修改 原始字符串并检查 b 是否也被更改。第二个选项是创建巨大的字符串,然后是一些子字符串,并检查内存使用情况。

【问题讨论】:

  • 如何创建一个字符串并检查是否为string.substring(4, 6) == string.substring(4, 6)?您可以缓存此布尔结果并重用它。不完全确定它是否有效,只是一个想法。
  • 可以和java.vendor系统属性结合使用
  • @Meagain 这不起作用。无论字符数组是否共享,子字符串都会返回一个新对象。
  • @benjamin.d 你当然是对的,我会更新这个问题。对于 Mac OS 上的 Java 1.6,供应商曾经是 Apple,但对于 Java 1.7,这应该不是问题。
  • 让我重新考虑一下。我将发布代码。

标签: java string jvm ibm-jvm


【解决方案1】:

没错,这个改动确实是在 7u6 中进行的。对此没有 API 更改,因为此更改严格来说是实现更改,而不是 API 更改,也没有 API 来检测正在运行的 JDK 的行为。但是,应用程序当然可能会因为更改而注意到性能或内存利用率的差异。事实上,编写一个在 7u4 中工作但在 7u6 中失败的程序并不难,反之亦然。我们预计这种权衡对大多数应用程序都是有利的,但毫无疑问,有些应用程序会受到这种变化的影响。

有趣的是,您担心共享字符串值的情况(在 7u6 之前)。我听说的大多数人都有相反的担忧,他们喜欢共享和 7u6 对非共享值的更改正在给他们带来问题(或者,他们担心这会导致问题)。 p>

无论如何,要做的是衡量,而不是猜测!

首先,比较您的应用程序在类似 JDK 之间的性能,无论是否发生变化,例如7u4 和 7u6。可能您应该查看 GC 日志或其他内存监控工具。如果差异是可以接受的,那么您就完成了!

假设 7u6 之前的共享字符串值会导致问题,下一步是尝试new String(s.substring(...)) 的简单解决方法来强制取消共享字符串值。然后测量它。同样,如果两个 JDK 的性能都可以接受,那么您就完成了!

如果事实证明在未共享的情况下,对new String() 的额外调用是不可接受的,那么检测这种情况并使“取消共享”调用有条件的最佳方法可能是反映字符串的value 字段,这是一个char[],并得到它的长度:

int getValueLength(String s) throws Exception {
    Field field = String.class.getDeclaredField("value");
    field.setAccessible(true);
    return ((char[])field.get(s)).length;
}

考虑一个由调用substring() 产生的字符串,它返回一个比原始字符串短的字符串。在共享的情况下,子字符串的length() 将不同于如上所示检索到的value 数组的长度。在非共享的情况下,它们将是相同的。例如:

String s = "abcdefghij".substring(2, 5);
int logicalLength = s.length();
int valueLength = getValueLength(s);

System.out.printf("%d %d ", logicalLength, valueLength);
if (logicalLength != valueLength) {
    System.out.println("shared");
else
    System.out.println("unshared");

在早于 7u6 的 JDK 上,该值的长度将为 10,而在 7u6 或更高版本上,该值的长度将为 3。当然,在这两种情况下,逻辑长度都是 3。

【讨论】:

  • 很好的答案!除了我不喜欢在库中调用 setAccessible(true) :-) 事实证明 s = new String(s) 对于 Java 7u6 / Java 8 来说相当快,所以我会采用我认为的那个解决方案。
  • 谢谢!很高兴知道简单的s = new String(s) 解决方法是可以接受的。我很想知道 7u6 之前的共享字符串值方法造成了多大的问题。
  • 这是一个糟糕的答案。具体实现的行为与问题无关。 API 合约没有具体说明底层数组是否被复制。它可能在 OpenJDK 中,而不是在 IBM 的 JDK 中,并且只有在大小上有显着差异时(对于某些未说明的“显着”值),其他一些 JDK 才可能复制它。
【解决方案2】:

这不是您需要关心的细节。 真的没有! 在两种情况下(JDK6 和 JDK7)都只需调用 identifier = new String(identifier)。在 JDK6 下,它将创建一个副本(根据需要)。在 JDK7 下,因为子字符串已经是一个唯一的字符串,所以构造函数本质上是一个无操作(不执行复制——阅读代码)。当然,创建对象会有一点开销,但由于年轻一代中的对象重用,我挑战你来限定性能差异。

【讨论】:

  • 好主意!我将测量new String(s) 的性能开销并发布。
  • 我对你的结果很感兴趣。在 JDK7 中,identifier = new String(identifier) 应该不超过几微秒(也许更少!),所以即使有数万个字符串,我也会惊讶地发现性能差异大于几毫秒。
  • 我认为你是对的,这并没有太大的区别。我仍然喜欢保持问题的开放性,因为也许有人想出了一个创造性的解决方案。
  • 如果此答案解决了您的问题,并且您认为您收集了足够多的其他答案,请将此答案标记为正确。
【解决方案3】:

在较旧的 Java 版本中,String.substring(..) 将使用与原始相同的 char 数组,但 offsetcount 不同。

在最新的 Java 版本中(根据 Thomas Mueller 的评论:自 1.7 Update 6 起),这种情况发生了变化,现在使用新的 char 数组创建子字符串。

如果您解析大量来源,处理它的最佳方法是避免检查字符串的内部,但预期这种效果并总是创建new Strings 您需要它们的地方(如您问题中的第一个代码块)。

String identifier = query.substring(14, 18);
// older Java versions: backed by same char array, different offset and count
// newer Java versions: copy of the desired run of the original char array

identifier = new String(identifier);
// older Java versions: when the backed char array is larger than count, a copy of the desired run will be made
// newer Java versions: trivial operation, create a new String instance which is backed by the same char array, no copy needed.

这样,您最终会得到两个变体的相同结果,而无需区分它们,也没有不必要的数组复制开销。

【讨论】:

  • "将始终使用与原始相同的 char 数组":不,这在 Java 1.7 更新 6 中发生了变化。
  • 有吗?我正在查看最新的 Java 8 EA 的源代码,它仍然在那里。
  • 嗯,在我拥有的 Java 8 版本中,没有“偏移”字段。你能再检查一下吗?
  • 哦,你是对的:'substring(..)' 中仍然使用了 'String(char value[], int offset, int count)' 构造函数,但现在它实际上复制了array: 'Arrays.copyOfRange(value, offset, offset+count);'
  • 是的,实际上总是使用x = new String(x) 可能比有条件地使用if (Y) x = new String(x) 更快。
【解决方案4】:

你确定复制字符串真的很贵吗?我相信 JVM 优化器具有关于字符串的内在特性并避免不必要的副本。大型文本也使用编译器生成的一次性算法(例如 LALR 自动机)进行解析。因此,解析器输入通常是java.io.Reader 或另一个流接口,而不是固定的String。解析本身通常很昂贵,但仍然没有类型检查那么昂贵。我不认为复制字符串是一个真正的瓶颈。在您做出假设之前,您可以更好地使用分析器和微基准测试。

【讨论】:

  • 在我的例子中,解析器输入是一个字符串(它是 JDBC API)。但我会测量性能差异并发布。
  • 如果您处理大量字符串并且只累积其中的一小部分(例如在示例中,仅从查询中提取标识符),则会产生大量内存开销(标识符字符串支持与源相同的字符数组),所以内存问题很合理。
  • 当你通过Reader处理字符串时,你不会得到substring,因此不存在内存泄漏的风险。在上述情况下,我做了两个假设: 1. 字符串不是真正 大 2. 它们在处理调用后被丢弃。因此,如果它们是正确的,我们不会得到任何显着的内存开销。
  • 输入是一个字符串(正如我所写,它是 JDBC API:我正在解析 SQL 语句)。我不会从String 创建一个Reader,因为这会比使用new String(s) 的效率更低。查询通常约为 100 个,但有时有数千个字符。
猜你喜欢
  • 1970-01-01
  • 2012-12-21
  • 2021-01-03
  • 2011-03-09
  • 2011-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-03
  • 2020-09-20
相关资源
最近更新 更多