2.善用 intern 方法
善用 String.intern() 方法可以有效的节约内存并提升字符串的运行效率,先来看 intern()
方法的定义与源码:
/** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * <p> * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */ public native String intern();
可以看出 intern()
是一个高效的本地方法,它的定义中说的是,当调用 intern
方法时,如果字符串常量池中已经包含此字符串,则直接返回此字符串的引用,如果不包含此字符串,先将字符串添加到常量池中,再返回此对象的引用。
那什么情况下适合使用 intern()
方法?
Twitter 工程师曾分享过一个 String.intern()
的使用示例,Twitter 每次发布消息状态的时候,都会产生一个地址信息,以当时 Twitter 用户的规模预估,服务器需要 32G 的内存来存储地址信息。
public class Location { private String city; private String region; private String countryCode; private double longitude; private double latitude; }
考虑到其中有很多用户在地址信息上是有重合的,比如,国家、省份、城市等,这时就可以将这部分信息单独列出一个类,以减少重复,代码如下:
public class SharedLocation { private String city; private String region; private String countryCode; } public class Location { private SharedLocation sharedLocation; double longitude; double latitude; }
通过优化,数据存储大小减到了 20G 左右。但对于内存存储这个数据来说,依然很大,怎么办呢?
Twitter 工程师使用 String.intern()
使重复性非常高的地址信息存储大小从 20G 降到几百兆,从而优化了 String 对象的存储。
实现的核心代码如下:
SharedLocation sharedLocation = new SharedLocation(); sharedLocation.setCity(messageInfo.getCity().intern()); sharedLocation.setCountryCode(messageInfo.getRegion().intern()); sharedLocation.setRegion(messageInfo.getCountryCode().intern());
从 JDK1.7 版本以后,常量池已经合并到了堆中,所以不会复制字符串副本,只是会把首次遇到的字符串的引用添加到常量池中。此时只会判断常量池中是否已经有此字符串,如果有就返回常量池中的字符串引用。
这就相当于以下代码:
String s1 = new String("Java中文社群").intern(); String s2 = new String("Java中文社群").intern(); System.out.println(s1 == s2);
执行的结果为:true
此处如果有人问为什么不直接赋值(使用 String s1 = "Java中文社群"),是因为这段代码是简化了上面 Twitter 业务代码的语义而创建的,他使用的是对象的方式,而非直接赋值的方式。更多关于 intern()
的内容可以查看《别再问我new字符串创建了几个对象了!我来证明给你看!》这篇文章。
3.慎重使用 Split 方法
之所以要劝各位慎用 Split
方法,是因为 Split
方法大多数情况下使用的是正则表达式,这种分割方式本身没有什么问题,但是由于正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致 CPU 居高不下。
例如以下正则表达式:
String badRegex = "^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\\\\/])+$"; String bugUrl = "http://www.apigo.com/dddp-web/pdf/download?request=6e7JGxxxxx4ILd-kExxxxxxxqJ4-CHLmqVnenXC692m74H38sdfdsazxcUmfcOH2fAfY1Vw__%5EDadIfJgiEf"; if (bugUrl.matches(badRegex)) { System.out.println("match!!"); } else { System.out.println("no match!!"); }
执行效果如下图所示:
可以看出,此代码导致了 CPU 使用过高。
Java 正则表达式使用的引擎实现是 NFA(Non deterministic Finite Automaton,不确定型有穷自动机)自动机,这种正则表达式引擎在进行字符匹配时会发生回溯(backtracking),而一旦发生回溯,那其消耗的时间就会变得很长,有可能是几分钟,也有可能是几个小时,时间长短取决于回溯的次数和复杂度。
为了更好地解释什么是回溯,我们使用以下面例子进行解释:
text = "abbc"; regex = "ab{1,3}c";
上面的这个例子的目的比较简单,匹配以 a 开头,以 c 结尾,中间有 1-3 个 b 字符的字符串。
NFA 引擎对其解析的过程是这样子的:
- 首先,读取正则表达式第一个匹配符
a
和 字符串第一个字符a
比较,匹配上了,于是读取正则表达式第二个字符;
- 读取正则表达式第二个匹配符
b{1,3}
和字符串的第二个字符 b 比较,匹配上了。但因为b{1,3}
表示 1-3 个b
字符串,以及 NFA 自动机的贪婪特性(也就是说要尽可能多地匹配),所以此时并不会再去读取下一个正则表达式的匹配符,而是依旧使用b{1,3}
和字符串的第三个字符b
比较,发现还是匹配上了,于是继续使用b{1,3}
和字符串的第四个字符c
比较,发现不匹配了,此时就会发生回溯;
- 发生回溯后,我们已经读取的字符串第四个字符
c
将被吐出去,指针回到第三个字符串的位置,之后程序读取正则表达式的下一个操作符c
,然后再读取当前指针的下一个字符c
进行对比,发现匹配上了,于是读取下一个操作符,然后发现已经结束了。
这就是正则匹配执行的流程和简单的回溯执行流程,而上面的示例在匹配到“com/dzfp-web/pdf/download?request=6e7JGm38jf.....”时因为贪婪匹配的原因,所以程序会一直读后面的字符串进行匹配,最后发现没有点号,于是就一个个字符回溯回去了,于是就会导致了 CPU 运行过高。
所以我们应该慎重使用 Split() 方法,我们可以用 String.indexOf() 方法代替 Split() 方法完成字符串的分割。如果实在无法满足需求,你就在使用 Split() 方法时,对回溯问题加以重视就可以了。
总结
本文通过 String 源码分析,发现了 String 的不可变特性,以及不可变特性的 3 大优点讲解;然后讲了字符串优化的三个手段:不要直接 += 字符串、善用 intern() 方法和慎重使用 Split() 方法。并且通过 StringBuilder 的源码分析,了解了 append() 性能高的主要原因,以及正则表达式不稳定性导致回溯问题,进入导致 CPU 使用过高的案例分析,希望可以切实的帮助到你。