前言
JDK9将String底层的数据结构从private final char value[];
改成了private final byte[] value;
,
JEP 254: Compact Strings(紧凑字符串),这要修改的目的就是为了节省空间1。我们先看一下JDK9和JDK8中String源码的变化。
JDK9中String源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
@Stable
private final byte[] value;
* LATIN1
* UTF16
private final byte coder;
@Native static final byte LATIN1 = 0;
@Native static final byte UTF16 = 1;
JDK8中String源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash;
}
对比源码可以看出,在JDK9中使用了byte[]代替了char[]同时还增加了一个 coder标志位表示使用的是LATIN1还是UTF16编码。
节省了多大空间
既然这个升级是为了节省内存空间,那么我们就先来测试一下看看节省了多少空间。我们创建一个简单的Demo,创建一个List然后向里面添加1-20000000个String,再对比一下JDK9和JDK8占用内存空间大小。
public static void main(String[] args) throws InterruptedException {
List<String> name= IntStream.range(0,20000000).mapToObj(String::valueOf).collect(Collectors.toList());;
System.out.printf("add complete "+name.size());
Thread.sleep(100000000L);
}
JDK8内存使用情况
JDK9内存使用情况
从图中可以看出同样是1-20000000字符串JDK8中char[]占用了742MB,JDK9中byte[]占用了559MB,节省了32.7%内存空间。
没有Jprofile 也可以使用jps+jmap查看内存占用情况,以下为Windows操作
- .\jps.exe 获取出PID
PS C:\Program Files\Java\jdk1.8.0_191\bin> .\jps.exe
9328 RemoteMavenServer36
9744 Launcher
10548
2196 Jps
7224 RemoteMavenServer36
8120 Main
- 通过Jmap查看内存中对象统计
JDK8:
PS C:\Program Files\Java\jdk1.8.0_191\bin> .\jmap.exe -histo:live 8120
num #instances #bytes class name
----------------------------------------------
1: 20004394 640375664 [C
2: 20004267 480102408 java.lang.String
3: 639 83113224 [Ljava.lang.Object;
4: 685 77712 java.lang.Class
5: 791 31640 java.util.TreeMap$Entry
6: 26 25752 [B
7: 657 21024 java.util.HashMap$Node
8: 315 13488 [Ljava.lang.String;
9: 126 8272 [I
10: 123 7872 java.net.URL
JDK9:
PS C:\Program Files\Java\jdk1.8.0_191\bin> .\jmap.exe -histo:live 4116
num #instances #bytes class name (module)
-------------------------------------------------------
1: 20003758 480237256 [B (java.base@17.0.7)
2: 20003674 480088176 java.lang.String (java.base@17.0.7)
3: 1042 83141776 [Ljava.lang.Object; (java.base@17.0.7)
4: 137 142376 [C (java.base@17.0.7)
5: 818 100864 java.lang.Class (java.base@17.0.7)
6: 1242 39744 java.util.HashMap$Node (java.base@17.0.7)
7: 1084 34688 java.util.concurrent.ConcurrentHashMap$Node (java.base@17.0.7)
8: 330 30240 [Ljava.util.HashMap$Node; (java.base@17.0.7)
9: 336 16128 java.util.HashMap (java.base@17.0.7)
10: 22 15392 [Ljava.util.concurrent.ConcurrentHashMap$Node; (java.base@17.0.7)
从Jmap输出内存对象统计来看 JDK8中[C就是 char[] ,实例数有2000万多点,占用内存610MB,JDK9 [B 就是byte[],实例数也是2000万多点,占用内存457MB,节省内存33.4%。值的注意的是如果不想使用字符串压缩可以使用 -XX:-CompactStrings
JVM参数来禁用此功能
是怎么节省空间的
JAVA中一个char到底占用多大空间?在JVM规范中char是使用一个16-bit无符号整型来表示一个Unicode code2,所以一个char可以理解占用两个字节空间,但是我们可以想一下常用的ascii码其实只要一个字节就能表示了。JDK9里对Latin-1(又称ISO-8859-1)字符使用一个byte存储,只占用了一个字节,如果不是Latin-1字符就使用UTF-16存储,占用两个字节,为此JDK9中增加了coder标志位,这样对于使用最频繁的abcd… 1234…等字母、数字来说就节省了50%空间,然而对于中文或者不在Latin-1字符来说还是和以前一样,并不能节省内存空间。那么Latin-1到底有哪些字符呢?Latin-1(也称为ISO 8859-1),是一种字符编码标准,用于表示拉丁字母语系的字符集。它包含了256个字符3。
- 26个基本拉丁字母,包括大小写字母 a-z,A-Z
- 数字0-9
- 逗号、句号、问号等常见标点符号
- 特殊符号,如@、#、$、%等
- 一些重音符号和其他符号,如é、ç、ñ等
具体可以看以下图片:
总结
1. 更少的内存占用:
采用 Compact Strings 后,只包含 ASCII 字符或者Latin-1的字符串在内存中的占用空间会减少一半,从而可以减少内存的使用量,提高程序的性能。
2.String内置方法都重写了
String内置的方法像equals、compareTo等都重写了。
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
public int compareTo(String anotherString) {
byte v1[] = value;
byte v2[] = anotherString.value;
byte coder = coder();
if (coder == anotherString.coder()) {
return coder == LATIN1 ? StringLatin1.compareTo(v1, v2)
: StringUTF16.compareTo(v1, v2);
}
return coder == LATIN1 ? StringLatin1.compareToUTF16(v1, v2)
: StringUTF16.compareToLatin1(v1, v2);
}
3.更快的字符串操作:
由于 Compact Strings 的内部表示方式采用了byte[]存储数据,可以更快地执行字符串操作,例如字符串比较、拼接等。
String生成测试Demo:
Long startTime=System.currentTimeMillis();
List<String> name= IntStream.range(0,20000000).mapToObj(String::valueOf).collect(Collectors.toList());;
System.out.println("add "+name.size()+" complete ,cast "+(System.currentTimeMillis()-startTime)+"ms");
//JDK9:
add 20000000 complete ,cast 1313ms
//JDK8
add 20000000 complete ,cast 13777ms
从生成2000万个String速度来看,JDK9只花了1.3S,而JDK8花了13S,确实快了不少。
String拼接Demo:
Long startTime = System.currentTimeMillis();
List<String> name = IntStream.range(0, 20000000).mapToObj(x -> String.valueOf(x) + x).collect(Collectors.toList());
System.out.println("add " + name.size() + " complete ,cast " + (System.currentTimeMillis() - startTime) + "ms");
//JDK9
add 20000000 complete ,cast 1832ms
//JDK8
add 20000000 complete ,cast 25147ms
从拼接2000万个String速度来看,JDK9只花了1.8S,而JDK8花了25S,确实也快了不少。
4.更高的兼容性:
Compact Strings 不会影响现有的 Java 代码,因此可以与现有的 Java 应用程序兼容,无需修改代码,这点是肯定的,如果要修改Java String操作的代码全球有多少人要抓狂,最后肯定都选择不升级了。
JEP 254: Compact Strings (openjdk.org) ↩︎
(11 封私信) Java中关于Char存储中文到底是2个字节还是3个还是4个? - 知乎 (zhihu.com) ↩︎
ISO/IEC 8859-1 - Wikipedia ↩︎