9

我知道对于 Oracle Java 1.7 update 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 是否也更改。第二个选项是创建巨大的字符串,然后是一些子字符串,并检查内存使用情况。

4

4 回答 4

3

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

有趣的是,您担心共享字符串值的情况(在 7u6 之前)。我听说的大多数人都有相反的担忧,他们喜欢共享,而将 7u6 更改为非共享值会导致他们出现问题(或者,他们担心这会导致问题)。

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

首先,比较您的应用程序在进行更改和未更改的类似 JDK 之间的性能,例如 7u4 和 7u6。可能您应该查看 GC 日志或其他内存监控工具。如果差异是可以接受的,你就完成了!

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

如果事实证明在未共享的情况下,额外的调用new String()是不可接受的,那么检测这种情况并使“取消共享”调用有条件的最好方法可能是反映字符串的value字段,即 a 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。

于 2013-11-28T21:43:15.503 回答
3

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

于 2013-11-28T08:08:59.793 回答
2

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

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

如果您解析大量来源,处理它的最佳方法是避免检查字符串的内部结构,但要预料到这种效果并始终在需要它们的地方创建新的字符串(如问题中的第一个代码块中所示)。

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.

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

于 2013-11-28T08:36:09.100 回答
0

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

于 2013-11-28T08:00:46.863 回答