如何检测String.substring是否复制字符数据

我知道对于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; } 

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

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

有趣的是,您关注的是共享字符串值的情况(在7u6之前)。 我听过的大多数人都有相反的担忧,他们喜欢分享和7u6对非共享价值的改变导致他们出现问题(或者,他们害怕会导致问题)。

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

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

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

如果事实certificate在非共享情况下,额外调用new String()是不可接受的,那么检测这种情况并使“unsharing”调用条件的最佳方法是反映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()调用产生的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。

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

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

在最新的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. 

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

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