Java的 String类有个有意思的public方法:

public String intern()
返回标准表示的字符串对象。String类维护私有字符串池。

调用此方法时,如果字符串池已经包含等于此字符串对象的字符串(通过equals方法确定),
则返回池中的字符串。 否则,将此String对象添加到池中,并返回对此String对象的引用。

这个功能为String提供了字符串池,我们可以使用它来优化内存。 但是,这有一个缺点:在OpenJDK中,String.intern()是本地方法,它实际上调用了JVM的相关方法来实现该功能。这样实现的原因是,当VM和JDK代码必须就特定String对象的标识达成一致时,String interning就必须是JDK-VM接口的一部分。

这样的实现意味着:

  1. 您需要在每个intern调用使用JDK-JVM接口,这会浪费CPU。
  2. 性能受本地HashTable实现的影响,可能落后于高性能Java版本,特别是在并发访问的情况下。
  3. 由于Java Strings是来自VM的引用,因此它们成为GC rootset的一部分。 在许多情况下,这需要在GC停顿期间执行额外的工作。

吞吐量实验

我们可以构建简单的实验来说明问题。 使用HashMap和ConcurrentHashMap实现intern方法,这为我们提供了一个非常好的JMH基准:

 @State(Scope.Benchmark)
public class StringIntern { @Param({"1", "100", "10000", "1000000"})
private int size; private StringInterner str;
private CHMInterner chm;
private HMInterner hm; @Setup
public void setup() {
str = new StringInterner();
chm = new CHMInterner();
hm = new HMInterner();
} public static class StringInterner {
public String intern(String s) {
return s.intern();
}
} @Benchmark
public void intern(Blackhole bh) {
for (int c = 0; c < size; c++) {
bh.consume(str.intern("String" + c));
}
} public static class CHMInterner {
private final Map<String, String> map; public CHMInterner() {
map = new ConcurrentHashMap<>();
} public String intern(String s) {
String exist = map.putIfAbsent(s, s);
return (exist == null) ? s : exist;
}
} @Benchmark
public void chm(Blackhole bh) {
for (int c = 0; c < size; c++) {
bh.consume(chm.intern("String" + c));
}
} public static class HMInterner {
private final Map<String, String> map; public HMInterner() {
map = new HashMap<>();
} public String intern(String s) {
String exist = map.putIfAbsent(s, s);
return (exist == null) ? s : exist;
}
} @Benchmark
public void hm(Blackhole bh) {
for (int c = 0; c < size; c++) {
bh.consume(hm.intern("String" + c));
}
}
}

该测试试图在很多字符串上执行intern方法,但实际的intern仅在第一次遍历循环时发生,之后只访问map中的字符串。 size参数用于控制我们intern的字符串数量,从而限制我们正在处理的字符串表大小。 对于intern来说,通常都这样使用。

使用JDK 8u131运行它:

Benchmark             (size)  Mode  Cnt       Score       Error  Units

StringIntern.chm           1  avgt   25       0.038 ±     0.001  us/op
StringIntern.chm 100 avgt 25 4.030 ± 0.013 us/op
StringIntern.chm 10000 avgt 25 516.483 ± 3.638 us/op
StringIntern.chm 1000000 avgt 25 93588.623 ± 4838.265 us/op StringIntern.hm 1 avgt 25 0.028 ± 0.001 us/op
StringIntern.hm 100 avgt 25 2.982 ± 0.073 us/op
StringIntern.hm 10000 avgt 25 422.782 ± 1.960 us/op
StringIntern.hm 1000000 avgt 25 81194.779 ± 4905.934 us/op StringIntern.intern 1 avgt 25 0.089 ± 0.001 us/op
StringIntern.intern 100 avgt 25 9.324 ± 0.096 us/op
StringIntern.intern 10000 avgt 25 1196.700 ± 141.915 us/op
StringIntern.intern 1000000 avgt 25 650243.474 ± 36680.057 us/op

可以看出 String.intern()明显更慢。慢的原因在于本地实现,这在perf record -g中清晰可见:

-    6.63%     0.00%  java     [unknown]           [k] 0x00000006f8000041
- 0x6f8000041
- 6.41% 0x7faedd1ee354
- 6.41% 0x7faedd170426
- JVM_InternString
- 5.82% StringTable::intern
- 4.85% StringTable::intern
0.39% java_lang_String::equals
0.19% Monitor::lock
+ 0.00% StringTable::basic_add
- 0.97% java_lang_String::as_unicode_string
resource_allocate_bytes
0.19% JNIHandleBlock::allocate_handle
0.19% JNIHandles::make_local

虽然JNI转换成本相当高,但似乎在StringTable实现上也花了相当多的时间。 使用 -XX:+PrintStringTableStatistics,将输出如下内容:

StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 1002714 = 24065136 bytes, avg 24.000
Number of literals : 1002714 = 64192616 bytes, avg 64.019
Total footprint : = 88737856 bytes
Average bucket size : 16.708 ; <---- !!!!!!

注意最后一行,平均每个bucket 16个元素表示已经过载。 更糟糕的是,字符串表不可调整大小(虽然有实验工作使它们可以调整大小,但是因为“其他原因”而被移除)。 通过设置更大的-XX:StringTableSize可能会减轻该问题:

Benchmark             (size)  Mode  Cnt       Score       Error  Units

# Default, copied from above
StringIntern.chm 1 avgt 25 0.038 ± 0.001 us/op
StringIntern.chm 100 avgt 25 4.030 ± 0.013 us/op
StringIntern.chm 10000 avgt 25 516.483 ± 3.638 us/op
StringIntern.chm 1000000 avgt 25 93588.623 ± 4838.265 us/op # Default, copied from above
StringIntern.intern 1 avgt 25 0.089 ± 0.001 us/op
StringIntern.intern 100 avgt 25 9.324 ± 0.096 us/op
StringIntern.intern 10000 avgt 25 1196.700 ± 141.915 us/op
StringIntern.intern 1000000 avgt 25 650243.474 ± 36680.057 us/op # StringTableSize = 10M
StringIntern.intern 1 avgt 5 0.097 ± 0.041 us/op
StringIntern.intern 100 avgt 5 10.174 ± 5.026 us/op
StringIntern.intern 10000 avgt 5 1152.387 ± 558.044 us/op
StringIntern.intern 1000000 avgt 5 130862.190 ± 61200.783 us/op

然而这只能暂时缓解问题,因为你必须提前做好规划。 如果盲目地将String表大小设置为较大值,并且不使用它,则会浪费内存。 即使您使用很大的StringTable,JNI本地调用仍然会消耗CPU。

GC停顿实验

本地字符串表最大问题在于它是GC root的一部分。也就是说,它应该需要垃圾收集器进行特殊扫描/更新。 在OpenJDK中,这意味着在暂停期间额外工作。 实际上,对于Shenandoah(译者注:对于ZGC也如此),暂停主要依赖于GC root set大小,在String表中存在1M记录会导致以下结果:

$ ... StringIntern -p size=1000000 --jvmArgs "-XX:+UseShenandoahGC -Xlog:gc+stats -Xmx1g -Xms1g"
...
Initial Mark Pauses (G) = 0.03 s (a = 15667 us) (n = 2) (lvls, us = 15039, 15039, 15039, 15039, 16260)
Initial Mark Pauses (N) = 0.03 s (a = 15516 us) (n = 2) (lvls, us = 14844, 14844, 14844, 14844, 16088)
Scan Roots = 0.03 s (a = 15448 us) (n = 2) (lvls, us = 14844, 14844, 14844, 14844, 16018)
S: Thread Roots = 0.00 s (a = 64 us) (n = 2) (lvls, us = 41, 41, 41, 41, 87)
S: String Table Roots = 0.03 s (a = 13210 us) (n = 2) (lvls, us = 12695, 12695, 12695, 12695, 13544)
S: Universe Roots = 0.00 s (a = 2 us) (n = 2) (lvls, us = 2, 2, 2, 2, 2)
S: JNI Roots = 0.00 s (a = 3 us) (n = 2) (lvls, us = 2, 2, 2, 2, 4)
S: JNI Weak Roots = 0.00 s (a = 35 us) (n = 2) (lvls, us = 29, 29, 29, 29, 42)
S: Synchronizer Roots = 0.00 s (a = 0 us) (n = 2) (lvls, us = 0, 0, 0, 0, 0)
S: Flat Profiler Roots = 0.00 s (a = 0 us) (n = 2) (lvls, us = 0, 0, 0, 0, 0)
S: Management Roots = 0.00 s (a = 1 us) (n = 2) (lvls, us = 1, 1, 1, 1, 1)
S: System Dict Roots = 0.00 s (a = 9 us) (n = 2) (lvls, us = 8, 8, 8, 8, 11)
S: CLDG Roots = 0.00 s (a = 75 us) (n = 2) (lvls, us = 68, 68, 68, 68, 81)
S: JVMTI Roots = 0.00 s (a = 0 us) (n = 2) (lvls, us = 0, 0, 0, 0, 1)

因为我们在root set中添加了内容,每次暂停会增加13ms。

某些GC实现仅在完成重要操作时执行String表清理。 比如,如果不进行卸载类,从JVM角度来看清理String表是没有意义的(因为加载的类是intern字符串的主要来源)。 因此,此工作负载在G1和CMS中会也会表现出有趣的行为:

public class InternMuch {
public static void main(String... args) {
for (int c = 0; c < 1_000_000_000; c++) {
String s = "" + c + "root";
s.intern();
}
}
}

用CMS跑一遍:

$ java -XX:+UseConcMarkSweepGC -Xmx2g -Xms2g -verbose:gc -XX:StringTableSize=6661443 InternMuch

GC(7) Pause Young (Allocation Failure) 349M->349M(989M) 357.485ms
GC(8) Pause Initial Mark 354M->354M(989M) 3.605ms
GC(8) Concurrent Mark
GC(8) Concurrent Mark 1.711ms
GC(8) Concurrent Preclean
GC(8) Concurrent Preclean 0.523ms
GC(8) Concurrent Abortable Preclean
GC(8) Concurrent Abortable Preclean 935.176ms
GC(8) Pause Remark 512M->512M(989M) 512.290ms
GC(8) Concurrent Sweep
GC(8) Concurrent Sweep 310.167ms
GC(8) Concurrent Reset
GC(8) Concurrent Reset 0.404ms
GC(9) Pause Young (Allocation Failure) 349M->349M(989M) 369.925ms

看起来结果还可以。 遍历重载的字符串表需要一段时间。 蛋疼的事情会在使用-XX:-ClassUnloading禁用类卸载后发生。你猜猜接下来会发生什么:

$ java -XX:+UseConcMarkSweepGC -Xmx2g -Xms2g -verbose:gc -XX:-ClassUnloading -XX:StringTableSize=6661443 InternMuch

GC(11) Pause Young (Allocation Failure) 273M->308M(989M) 338.999ms
GC(12) Pause Initial Mark 314M->314M(989M) 66.586ms
GC(12) Concurrent Mark
GC(12) Concurrent Mark 175.625ms
GC(12) Concurrent Preclean
GC(12) Concurrent Preclean 0.539ms
GC(12) Concurrent Abortable Preclean
GC(12) Concurrent Abortable Preclean 2549.523ms
GC(12) Pause Remark 696M->696M(989M) 133.920ms
GC(12) Concurrent Sweep
GC(12) Concurrent Sweep 175.949ms
GC(12) Concurrent Reset
GC(12) Concurrent Reset 0.463ms
GC(14) Pause Full (Allocation Failure) 859M->0M(989M) 1541.465ms <---- !!!
GC(13) Pause Young (Allocation Failure) 859M->0M(989M) 1541.515ms

FULL GC! 对于CMS,假设用户会调用System.gc(),使用ExplicitGCInvokesConcurrentAndUnloadsClasses会缓解这一情况。

意见

在假设改进内存占用空间或低级==优化的情况下,我们讨论了实现intern的方法。

对于OpenJDK,String.intern()是本机JVM字符串表的代理,使用它需要注意:吞吐量,内存占用,暂停时间等问题。 很容易低估这些问题的影响。 手动控制的intern工作更加可靠,因为它们在Java端工作,只是普通Java对象,通常更容易调整大小,并且在不再需要时也可以完全丢弃。 GC辅助字符串去重复数据(http://openjdk.java.net/jeps/192)确实可以减少很多问题。

几乎在在我们进行每个项目中,从热路径中删除String.intern(),或者用手动方式替代它,都有很大的性能提升。 不要无脑使用String.intern(),好吗?

聊聊Java String.intern 背后你不知道的知识的更多相关文章

  1. 深入理解Java String#intern() 内存模型

    原文出处: codelog.me 大家知道,Java中string.intern()方法调用会先去字符串常量池中查找相应的字符串,如果字符串不存在,就会在字符串常量池中创建该字符串然后再返回. 字符串 ...

  2. Java String.intern()_学习笔记

    参考:https://www.jianshu.com/p/0d1c003d2ff5 String.intern() String.intern()是native方法,底层调用c++中的StringTa ...

  3. java String.intern();

    0.引言 什么都先不说,先看下面这个引入的例子: String str1 = new String("SEU")+ new String("Calvin"); ...

  4. 深入理解java String 及intern

    一.字符串问题 字符串在我们平时的编码工作中其实用的非常多,并且用起来也比较简单,所以很少有人对其做特别深入的研究.倒是面试或者笔试的时候,往往会涉及比较深入和难度大一点的问题.我在招聘的时候也偶尔会 ...

  5. 通过反编译深入理解Java String及intern(转)

    通过反编译深入理解Java String及intern 原文传送门:http://www.cnblogs.com/paddix/p/5326863.html 一.字符串问题 字符串在我们平时的编码工作 ...

  6. 通过反编译深入理解Java String及intern

    一.字符串问题 字符串在我们平时的编码工作中其实用的非常多,并且用起来也比较简单,所以很少有人对其做特别深入的研究.倒是面试或者笔试的时候,往往会涉及比较深入和难度大一点的问题.我在招聘的时候也偶尔会 ...

  7. 通过反编译看Java String及intern内幕--费元星站长

    通过反编译看Java String及intern内幕   一.字符串问题 字符串在我们平时的编码工作中其实用的非常多,并且用起来也比较简单,所以很少有人对其做特别深入的研究.倒是面试或者笔试的时候,往 ...

  8. (转)通过反编译深入理解Java String及intern

    原文链接:https://www.cnblogs.com/paddix/p/5326863.html 一.字符串问题 字符串在我们平时的编码工作中用的非常多,并且用起来非常简单,所以很少有人对其做特别 ...

  9. Java提高篇——理解String 及 String.intern() 在实际中的应用

    1. 首先String不属于8种基本数据类型,String是一个对象.   因为对象的默认值是null,所以String的默认值也是null:但它又是一种特殊的对象,有其它对象没有的一些特性. 2. ...

随机推荐

  1. SQL SERVER LEAD和LAG使用

    示例:获取在48小时之内重复的记录 SELECT * FROM ( SELECT b.* , LAG(b.OperatorTime, , b.OperatorTime) OVER ( PARTITIO ...

  2. GAC的一种非官方实现方式

    1.GAC简介 全局程序集缓存(Global Assembly Cache, GAC)计算机范围内的代码缓存,它存储专门安装的程序集,这些程序集由计算机上的许多应用程序共享.在全局程序集缓存中部署的应 ...

  3. Microsoft Enterprise Library 5.0 系列(四)

    企业库日志应用程序模块工作原理图: 从上图我们可以看清楚企业库日志应用程序模块的工作原理,其中LogFilter,Trace Source,Trace Listener,Log Formatter的信 ...

  4. CUDA配置

    你问这个有多恶心,是真的很恶心!!! 首先推出一个博客上的内容,里面内容很不错,都是前车之鉴,很有用.http://blog.csdn.net/masa_fish/article/details/51 ...

  5. TCanvas 类属性及方法

    delphi TCanvas类 类关系   TObject-> TPersistent   对那些作图对象,可使用TCanvas对象作为画布.标准的window控件,例如编辑控件和列表框控件,当 ...

  6. VS2010下编译配置Boost_1.53

    一.准备工作 1.下载最新版本的boost库.所在地址:boost_1_53_0.zip.官方推荐7z压缩格式的,因为其压缩效率更好,相应包的大小也比较小. 2.解压缩到指定目录,如C:\boost_ ...

  7. Linux软件安装及基本概念

    apt 基本用法 apt-get [options] install/remove/source 软件包1 [软件包2...] 注意:软件包不要带后缀.deb 常用命令及解释如下: apt下载软件是根 ...

  8. Python实现按键精灵(二)-找图找色

    一.实现功能 判断在指定坐标范围内,是否存在相似度大于n的图片,并返回坐标. 二.基本思路 A=你需要寻找的图片 B=截取当前页面中指定范围的图片 利用opencv 判断A在B中的位置, 在该位置截取 ...

  9. hgoi#20190514

    T1-Curriculum Vitae 给你一个长度为n的01序列a,删去其中的几个数,使得序列中左边是连续的0,右边是连续的1,可以没有0或1,求最多剩下几个数 解法 对于每个点看它左边几个0,右边 ...

  10. 626. Exchange Seats-(LeetCode之Database篇)

    问题表述 数据库表如下: id student 1 Abbot 2 Doris 3 Emerson 4 Green 5 Jeames 现在要通过SQL语句将表变换成如下: id student 1 D ...