Java9中String的优化

String类的实现方式

在Java 9之前,String类是由char数组实现的,每个char占用两个字节的内存空间。而在Java 9中,String类引入了一种称为"Compact Strings"的新实现方式,将字符串的表示方式从char数组改为byte数组,并使用一种编码方式将Unicode字符映射到一个或两个字节的表示方式。这种实现方式可以大大减少内存使用,尤其是对于包含大量ASCII字符的字符串。

为什么这么说呢?JDK9中,引入了一个coder标识,用来区分是普通的拉丁字母还是UTF16字符。

static final byte LATIN1 = 0;
static final byte UTF16 = 1;

我们在日常使用中可能很多情况下大量使用英文字母,较少使用一些中文或者其他复杂的字符,这时候JDK9中的这种优化就能有很大的用处,因为按照原本JDK8的方案,无论是什么内容,就统一按照char来存储,这样对于那些普通的英文字母根本不需要用两个字节的空间,一个字节就够了,如果涉及到大量的这种纯英文字母的字符串,此时JDK9的存储上的优化就大大体现了出来。

String类中的方法

  1. 构造方法
  • String(): 创建一个空字符串。

  • String(char[] value): 创建一个包含指定字符序列的字符串。

  • String(byte[] bytes): 使用默认字符集解码指定的字节数组,创建一个新的字符串。

  • String(String original): 创建一个与指定字符串内容相同的新字符串。

  1. 字符串操作方法
  • charAt(int index): 返回指定位置上的字符。

  • concat(String str): 将指定字符串连接到此字符串的末尾。

  • substring(int beginIndex): 返回一个新的字符串,它是此字符串的子字符串。

  • substring(int beginIndex, int endIndex): 返回一个新的字符串,它是此字符串的子字符串。

  • replace(char oldChar, char newChar): 返回一个新字符串,它是通过用新字符替换此字符串中出现的所有旧字符得到的。

  • replaceAll(String regex, String replacement): 用指定的字符串替换所有匹配给定的正则表达式的子字符串。

  • trim(): 返回字符串的副本,忽略前导空白和尾部空白。

  • toLowerCase(): 使用默认语言环境的规则将此字符串转换为小写。

  • toUpperCase(): 使用默认语言环境的规则将此字符串转换为大写。

  • getBytes(): 使用平台默认字符集将此字符串编码为字节数组。

  1. 字符串比较方法
  • equals(Object anObject): 将此字符串与指定对象进行比较。

  • equalsIgnoreCase(String anotherString): 将此字符串与指定字符串进行比较,忽略大小写差异。

  • compareTo(String anotherString): 按字典顺序比较两个字符串。

  • compareToIgnoreCase(String str): 按字典顺序比较两个字符串,忽略大小写差异。

  1. 其他方法
  • length(): 返回此字符串的长度。

  • isEmpty(): 当且仅当字符串长度为 0 时返回 true。

  • valueOf(int i): 返回 int 参数的字符串表示形式。

  • join(CharSequence delimiter, CharSequence... elements): 将给定的字符串序列以指定的分隔符拼接起来,并返回结果字符串。

新增的repeat以及strip方法

  1. repeat()方法

repeat(int count)方法可以将原字符串重复指定次数,并返回一个新字符串。例如:

javaCopy code
String str = "hello";
String repeatedStr = str.repeat(3);
System.out.println(repeatedStr); // "hellohellohello"

在这个例子中,我们通过调用repeat()方法将字符串"hello"重复了三次,并返回了一个新字符串"hellohellohello"。

  1. strip()方法

strip()方法用于去除字符串两端的空白字符,包括空格、制表符和换行符等,返回一个新字符串。例如:

javaCopy code
String str = "  hello  \n";
String strippedStr = str.strip();
System.out.println(strippedStr); // "hello"

在这个例子中,原字符串为" hello \n",包含两个前导空格和一个换行符,调用strip()方法后返回的新字符串为"hello",两端的空格和换行符都被去掉了。

除了strip()方法,Java 9还新增了stripLeading()stripTrailing()方法,分别用于去除字符串的前导空格和尾部空格。这些方法对于处理输入数据和进行字符串比较时非常有用。

字符串常量池的优化

在Java 9之前,字符串常量池是在永久代(PermGen)中实现的。这意味着在运行时,所有的字符串常量都被存储在一块固定的内存区域中。这种实现方式存在一些问题,比如常量池容易被填满,导致OutOfMemoryError异常;并且,永久代是Java虚拟机中一个相对较小的区域,因此可能会导致内存不足的问题。

在Java 8中,永久代被移除,字符串常量池被转移到了堆(Heap)中。这种实现方式解决了一些问题,但仍然存在一些性能和内存使用方面的问题。在Java 9中,字符串常量池进行了优化,主要包括以下几个方面:

  1. 字符串常量池被移到了元空间(Metaspace)中,这是一个更大的内存区域,可以动态调整大小,从而避免了OutOfMemoryError异常。

  2. 在元空间中,字符串常量池使用了一种新的数据结构,称为“G1特殊化常量池”。这种数据结构在性能和内存使用方面都有所优化,能够更快地查找和添加常量。

  3. 对于使用字符串常量的程序,编译器现在会生成更高效的字节码,以利用这些优化。例如,编译器可以使用ldc2指令来加载常量池中的双字节字符串,而不是使用两个ldc指令。

总之,Java 9中对字符串常量池的优化使得它更加高效和可靠,能够更好地满足大规模应用程序的需要。

性能分析

前面说了这么多,都是关于JDK9性能上的一些优化介绍,但是具体提升了多少呢?下面使用一些直观的例子来感受一下到底提升了多少:

字符串拼接

首先来看看字符串拼接,一直以来我们编程中有一个原则:对于频繁拼接字符串的操作,不要直接使用String,而是考虑使用StringBuilder或者StringBuffer,这是因为String的不可变特性,导致在拼接过程中会产生大量的String对象从而导致内存浪费:

运行下面这段代码:

long startTime = System.nanoTime();
String s = "";
for (int i = 0; i < 100000; i++) {
    s += "a";
}
long endTime = System.nanoTime();
System.out.println("cost time: " + (endTime - startTime) + "ns");

将这段代码分别放到JDK1.8和JDK1.9版本对比一下执行时间,这里推荐一个在线的Java编译运行网站,支持动态选择JDK版本:https://www.jdoodle.com/online-java-compiler

image.png

为了稍微准确一点,排除一定的偶然性,我JDK1.8和JDK1.9各自都运行了三遍:

// JDK1.8
cost time: 8043104665ns
cost time: 8029676317ns
cost time: 8084159060ns
//JDK1.9
cost time: 1596969483ns
cost time: 2130420129ns
cost time: 2150930645ns

这里可以看到,在十万级别的字符串拼接操作中,JDK1.9的性能提升属于倍数级别的提升了,基本都在四倍左右的提升。

同时每次执行时,页面上结果输出框的上方也有对应的cpu time和memory数据:

//JDK1.8
1394148 kilobyte(s)
1394176 kilobyte(s)
1394388 kilobyte(s)
//JDK1.9
660312 kilobyte(s)
660324 kilobyte(s)
660500 kilobyte(s)

可以看到,这内存占用上直接下降了一个量级,由此可见JDK1.9在字符串的拼接这块,性能上得到了巨大的提升。

字符串替换

long startTime = System.nanoTime();
for (int i =  0; i < 100000; i++) {
    String str = "abcdefg";
    str.replace("c", "x");
}
long endTime = System.nanoTime();
System.out.println("cost time: " + (endTime - startTime) + "ns");

同样的操作,各自执行3次看看情况:

//JDK1.8
cost time: 130509721ns
cost time: 226153962ns
cost time: 152726076ns
//JDK1.9
cost time: 53052617ns
cost time: 52959038ns
cost time: 66009582ns
/////////////memory////////
//JDK1.8
97120 kilobyte(s)
97096 kilobyte(s)
96936 kilobyte(s)
//JDK1.9
48488 kilobyte(s)
49688 kilobyte(s)
49732 kilobyte(s)

这里可以看到在效率上,直接下降了一个量级,同时在哪存上也有接近50%的节约。

至于其它一些常规的方法,比如:查找(indexOf)、分割(split)这里JDK1.8和JDK1.9并没有多少提升,甚至可能出现JDK1.9的执行效率上反而更低,这并不是说9版本中出现了倒退,而是内部实现的算法不一样,可能在某些比较特殊的场景下,JDK1.9更有优势,就像一些排序算法一样,虽然时间复杂度可以衡量,但是在数据量不大的时候,可能一些复杂度高的算法反而效果更好。

总体来说,JDK1.9中String得split方法相比于JDK1.8,一般认为有如下改进:

  1. 使用新的UTF-16字符串匹配器,代替了JDK 8中使用的正则表达式引擎。

  2. 改进了分隔符的识别,当分隔符为单个字符时,使用位运算进行匹配,避免了使用正则表达式引擎的开销。

  3. 引入了一种优化,当字符串不包含分隔符时,不进行任何操作直接返回原字符串。这里的好处就是避免一些极端情况下产生大量冗余重复的字符串对象

  4. 一些内部的实现细节上做了微调。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 首先String不属于8种基本数据类型,String是一种引用数据类型,其默认值是null。String类在Jav...
    Yaan9阅读 206评论 0 1
  • Java之String 开篇 下面这段代码的输出: String对象的内部实现 图示: 在 Java6 以及之前的...
    Cool_Pomelo阅读 850评论 2 13
  • 前提回顾 java.lang.String类用于描述字符串,Java程序中所有的字符串字面值都可以使用该类的对象加...
    洛神灬殇阅读 198评论 0 1
  • String 对象是我们使用最频繁的一个对象类型,但它的性能问题却是最容易被忽略的。String 对象作为 Jav...
    han741阅读 785评论 0 1
  • 所有字符串用“”进行定义,也可以利用“+”实现字符串的连接 字符串不是基本数据类型。 范例:String 类的实例...
    六艺str阅读 275评论 0 0