我知道对于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是否也被更改。第二个选项是创建大字符串,然后是一些子字符串,并检查内存使用情况。
答案 0 :(得分:4)
是的,确实这个改变发生在7u6。对此没有API更改,因为此更改严格来说是实现更改,而不是API更改,也没有用于检测正在运行的JDK具有哪种行为的API。但是,由于更改,应用程序当然可以注意到性能或内存利用率的差异。实际上,编写一个在7u4中工作但在7u6中失败的程序并不困难,反之亦然。我们预计这种权衡对大多数应用程序都有利,但毫无疑问,有些应用程序会受到这种变化的影响。
有趣的是,您关注的是共享字符串值的情况(在7u6之前)。我听过的大多数人都有相反的担忧,他们喜欢共享和7u6更改为非共享值导致他们出现问题(或者,他们害怕会导致问题)。
无论如何,要做的就是衡量,而不是猜测!
首先,比较具有和不具有更改的类似JDK之间的应用程序性能,例如: 7u4和7u6。您可能应该关注GC日志或其他内存监控工具。如果差异可以接受,那就完成了!
假设7u6之前的共享字符串值导致问题,下一步是尝试new String(s.substring(...))
的简单解决方法强制取消共享字符串值。然后衡量一下。同样,如果两个JDK的性能都可以接受,那么你就完成了!
如果事实证明在非共享情况下,对new String()
的额外调用是不可接受的,那么检测此情况并使“取消共享”调用成为条件的最佳方法可能是反映字符串的{{ 1}}字段,即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
在早于7u6的JDK上,值的长度将为10,而在7u6或更高版本中,值的长度将为3.在这两种情况下,逻辑长度当然都是3.
答案 1 :(得分:3)
这不是您需要关注的细节。 不是!在这两种情况下都只需要调用identifier = new String(identifier)
(JDK6和JDK7)。在JDK6下,它将创建一个副本(根据需要)。在JDK7下,因为子字符串已经是一个唯一的字符串,所以构造函数本质上是一个无操作(不执行复制 - 读取代码)。当然,对象创建有一些轻微的开销,但由于Younger一代中的对象重用,我挑战你来确定性能差异。
答案 2 :(得分:2)
在较旧的Java版本中,String.substring(..)
将使用与原始版本相同的char数组,并使用不同的offset
和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.
这样,您最终会得到两个变体的相同结果,而不必区分它们,也没有不必要的数组复制开销。
答案 3 :(得分:0)
你确定,制作字符串副本真的很贵吗?我相信JVM优化器具有关于字符串的内在函数并避免不必要的副本。此外,大型文本使用由编译器编译器生成的一次性算法(如LALR自动机)进行解析。因此,解析器输入通常是java.io.Reader
或另一个流接口,而不是实体String
。解析本身就是昂贵的,仍然没有类型检查那么昂贵。我不认为复制字符串是一个真正的瓶颈。在假设之前,您最好使用分析器和微基准测试。