Java SE API know how
字符串
紧凑字符串
java8
无论字符串的编码,最终都会编码为16位字符数组。
java11
字符串会编码为8位字节数组,这些字符串被称为紧凑字符串(compact string)
一个普通的java字符串的大小约是java8相同字符串的一半。
3-10倍的性能提升对比。
-XX:+CompactString 默认开启
except instance
应用程序中所有字符串都需要16位编码,和非紧凑字符串相比,在紧凑字符串中对这些字符串操作会稍长时间
重复字符串和字符串保留
创建很多包含相同字符序列的对象,基于对象不可变占用额外空间。
要知道是否有大量重复对象需要进行堆分析
jcmd <process id/main class> GC.class_histogram
jcmd <process id/main class> Thread.print
字符串去重
让jvm找到重复字符串后去重(deduplicate)
只有使用G1 GC + -XX:UseStringDeduplication标志 默认false
pros and cons
- 需要在G1 GC的新生代回收和混合回收阶段进行额外处理,延长回收时间
- 需要一个额外线程与应用线程同时运行,占用应用程序线程CPU周期
- 如果重复字符串很少,可能达不到效果
java工程师估计预期受益10%
#jdk8
-XX:PrintStringDeduplicationStatistics
#jdk11 after
-Xlog:gc+stringdebup*=debug
[0.896s][debug][gc,stringdedup] Last Exec: 110.434ms, Idle: 729.700ms,
Blocked: 0/0.000ms
[0.896s][debug][gc,stringdedup] Inspected: 62420
[0.896s][debug][gc,stringdedup] Skipped: 0( 0.0%)
[0.896s][debug][gc,stringdedup] Hashed: 62420(100.0%)
[0.896s][debug][gc,stringdedup] Known: 0( 0.0%)
[0.896s][debug][gc,stringdedup] New: 62420(100.0%)
3291.7K
[0.896s][debug][gc,stringdedup] Deduplicated: 15604( 25.0%)
731.4K( 22.2%)
[0.896s][debug][gc,stringdedup] Young: 0( 0.0%)
0.0B( 0.0%)
[0.896s][debug][gc,stringdedup] Old: 15604(100.0%)
731.4K(100.0%)
晋升的字符串可以进行去重的时间点是通过
-XX:StringDeduplicationAgeThreshold=N标志 默认3
字符串保留
编程层面处理字符串重复的方法是使用原生方法intern()方法
保留字符串存在特殊的哈希表中——哈希表存储在原生内存中,字符串存储在堆中
-XX:StringTableSize=N设置
【固定大小的哈希表】
哈希表包含一个数组,该数组容纳一定数量的条目,数组中每个元素被称为一个桶
当某个对象被存储在哈希表中是,存储索引是通过hashCode % numberOfBuckets计算
这个方案可能让两个具有不同hashCode值的对象很可能被映射到同一个桶
每个桶实际上是映射到该桶的所有存储项的链表
当两个对象被映射到同一个桶,这种情况被称为碰撞(collision)
当哈希表有更多对象之后,碰撞就会更多,更多的项存储到每个链表中
查找某个项需要遍历链表搜索,如果链表很长则增加搜索市场
通过调整哈希表可以有更多桶(减少碰撞),通过动态调整哈希表的大小的设计java中的HashMap便是如此。
【字符串保留和相等比较】
String.intern()更快
保留字符串可以通过==比较,是一个更快的引用比较
保留字符串的成本:计算字符串的hash码,也就是扫描整个字符串,并对每个字符进行hash操作
String.equals比较方式
不等长不相同
等长进行比较找到未必配字符
自定义字符串保留
Your mileage may very
字符串连接
jdk8 and jdk11 and optimal
@Benchmarkpublic void testSingleStringBuilder(Blackhole bh) {
String s = new StringBuilder(prefix).append(strings[0]).toString();
bh.consume(s);
}
@Benchmark
public void testSingleJDK11Style(Blackhole bh) {
String s = prefix + strings[0];
bh.consume(s);
}
@Benchmark
public void testSingleJDK8Style(Blackhole bh) {
String s = new StringBuilder().append(prefix).append(strings[0]).toString();
bh.consume(s);
}
- 手动操作进行编码
- jdk11编译进行最新的编译器优化
- jdk8和11都是一样的编译方式
最终数据显示发现两种优化都比手动编码好,只是编译器没有发现特定的模式。
如果jdk8对字符串和其他类型进行链接则情况
@Benchmark
public void testDoubleJDK11Style(Blackhole bh) {
double d = 1.0;
String s = prefix + strings[0] + d;
bh.consume(s);
}
@Benchmark
public void testDoubleJDK8Style(Blackhole bh) {
double d = 1.0;
String s = new StringBuilder().append(prefix).
append(strings[0]).append(d).toString();
bh.consume(s);
}
jdk8在处理非字符串链接类型时候对整形处理的较好,但是双精度情况下则会跳过优化,复原了手动编码的性能。
多次链接操作或者循环中执行链接操作的测试数据
@Benchmark
public void testJDK11Style(Blackhole bh) {
String s = "";
for (int i = 0; i < nStrings; i++) {
s = s + strings[i];
}
bh.consume(s);
}
@Benchmark
public void testJDK8Style(Blackhole bh) {
String s = "";
for (int i = 0; i < nStrings; i++) {
s = new StringBuilder().append(s).append(strings[i]).toString();
}
bh.consume(s);
}
@Benchmark
public void testStringBuilder(Blackhole bh) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < nStrings; i++) {
sb.append(strings[i]);
}
bh.consume(sb.toString());
}
测试结果显示最好的性能是手动编码方式
当在多次拼接或者循环中链接字符串最好显式使用StringBuilder对象获取更好的性能