本博客來自個人新書Java性能優化(暫定名),第二章的節選2.1和2.2,2.10. 也歡迎購買個人書 《Spring Boot 2 精髓 》
程序員
字符串在Java裏是不可變的,不管是構造,仍是截取,獲得的老是一個新字符串。看一下構造一個字符串源碼spring
private final char value[]; public String(String original) { this.value = original.value; this.hash = original.hash; }
原有的字符串的value數組直接經過引用賦值給新的字符串value,也就是倆個字符串共享一個char數組,所以這種構造方法有着最快的構造。Java裏的String對象被設計爲不可變。意思是指一旦程序得到了字符串對象引用,沒必要擔憂這個字符串在別的地方被修改,不可變意味着線程安全,在第三章對不可變對象線程安全性又說明。數組
構造字符串更多的狀況構造字符串是經過一個字符串數組,或者在某些框架的反序列化,使用byte[] 來構造字符串,這種狀況下性能會很是低。 以下是經過char[]數組構造一個新的字符串源碼安全
public String(char value[]) { this.value = Arrays.copyOf(value, value.length); }
Arrays.copyOf 會從新拷貝一份新的數組,方法以下springboot
public static char[] copyOf(char[] original, int newLength) { char[] copy = new char[newLength]; System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
能夠看到經過數組構造字符串其實是會建立一個新的字符串數組。若是不這樣,仍是直接引用char數組,那麼外部若是更改char數組,則這個新的字符串就被改變了。性能優化
char[] cs = new char[]{'a','b'}; String str = new String(cs); cs[0] ='!'
上面的代碼最後一行,修改了cs數組,但不會影響str。由於str其實是新的字符串數組構成服務器
經過char數組構造新的字符串是最長用的方法,咱們後面看到幾乎每一個字符串API,都會調用這個方法構造新的字符串,好比subString,concat等方法。以下代碼驗證了經過字符串構造新的字符串,以及使用char數組構造字符串性能比較併發
String str= "你好,String"; char[] chars = str.toCharArray(); [@Benchmark](https://my.oschina.net/u/3268003) public String string(){ return new String(str); } [@Benchmark](https://my.oschina.net/u/3268003) public String stringByCharArray(){ return new String(chars); }
輸出按照ns/op來輸出,既每次調用所用的納秒數,能夠看到經過char構造字符串仍是先當耗時的,特別若是是數組特別長,那更加耗時app
Benchmark Mode Score Units c.i.c.c.NewStringTest.string avgt 4.235 ns/op c.i.c.c.NewStringTest.stringByCharArray avgt 11.704 ns/op
經過字節構造字符串,是一種很是常見的狀況,尤爲如今分佈式和微服務流行,字符串在客戶端序列化成字節數組,併發送給你給服務器端,服務器端會有一個反序列化,經過byte構造字符串框架
以下測試使用byte構造字符串性能測試
byte[] bs = "你好,String".getBytes("UTF-8"); [@Benchmark](https://my.oschina.net/u/3268003) public String stringByByteArray() throws Exception{ return new String(bs,"UTF-8"); }
測試結果能夠看到byte構造字符串太耗時了,尤爲是當要構造的字符串很是長的時候
Benchmark Mode Score Units c.i.c.c.NewStringTest.string avgt 4.649 ns/op c.i.c.c.NewStringTest.stringByByteArray avgt 82.166 ns/op c.i.c.c.NewStringTest.stringByCharArray avgt 12.138 ns/op
經過字節數組構造字符串,主要涉及到轉碼過程,內部會調用 StringCoding.decode轉碼
this.value = StringCoding.decode(charsetName, bytes, offset, length);
charsetName表示字符集,bytes是字節數組,offset和length表示字節數組
實際負責轉碼的是Charset子類,好比sun.nio.cs.UTF_8的decode方法負責實現字節轉碼,若是在深刻到這個類,你會發現,你看到的是冰上一角,冰上下面這是一個至關耗CPU計算轉碼的工做,屬於沒法優化的部分.
在我屢次的系統性能優化過程當中,都會發現經過字節數據組構造字符串老是排在消耗CPU比較靠前的位置,轉碼消耗的系統性能抵得上百行的業務代碼。 所以咱們系統在設計到分佈式的,須要仔細設計須要傳輸的字段,儘可能避免用String。好比時間能夠用long類型來表示,業務狀態也能夠用int來表示。以下須要序列化的對象
public class OrderResponse{ //訂單日期,格式'yyyy-MM-dd' private String createDate; //訂單狀態,"0"表示正常 private String status; }
能夠改進成更好的定義,以減少序列化和反序列化負擔。
public class OrderResponse{ //訂單日期 private long createDate; //訂單狀態,0表示正常 private int status; }
關於在微服務中,序列化和反序列化傳輸對象,會在第四章和五章再次介紹對象的序列化
JDK會自動將使用+號作的字符串拼接自動轉化爲StringBuilder,以下代碼:
String a="hello"; String b ="world " String str=a+b;
虛擬機會編譯成以下代碼
String str = new StringBuilder().append(a).append(b).toString();
若是你運行JMH測試這倆段代碼,性能其實同樣的,由於使用+鏈接字符串是一個常見操做,虛擬機對如上倆個代碼片斷都會作一些優化,虛擬使用-XX:+OptimizeStringConcat 打開字符串拼接優化,(默認狀況下是打開的)。 若是採用如下代碼,雖然看是跟上面的代碼片斷差很少,但虛擬機沒法識別這種字符串拼接模式,性能會降低不少
StringBuilder sb = new StringBuilder(); sb.append(a); sb.append(b);
運行StringConcatTest類,代碼以下
String a = "select u.id,u.name from user u"; String b=" where u.id=? " ; [@Benchmark](https://my.oschina.net/u/3268003) public String concat(){ String c = a+b; return c ; } [@Benchmark](https://my.oschina.net/u/3268003) public String concatbyOptimizeBuilder(){ String c = new StringBuilder().append(a).append(b).toString(); return c; } @Benchmark public String concatbyBuilder(){ //不會優化 StringBuilder sb = new StringBuilder(); sb.append(a); sb.append(b); return sb.toString(); }
有以下結果說明了虛擬機優化起了做用
Benchmark Mode Score Units c.i.c.c.StringConcatTest.concat avgt 25.747 ns/op c.i.c.c.StringConcatTest.concatbyBuilder avgt 90.548 ns/op c.i.c.c.StringConcatTest.concatbyOptimizeBuilder avgt 21.904 ns/op
能夠看到concatbyBuilder是最慢的,由於沒有被JVM優化
這裏說的JVM優化,指的是虛擬機JIT優化,咱們會在第8章JIT優化說明
讀者能夠本身驗證一下a+b+c這種字符串拼接性能,看一下是否被優化了
同StringBuilder相似的還有StringBuffer,主要功能都繼承AbstractStringBuilder, 提供了線程安全方法,好比append方法,使用了synchronized關鍵字
@Override public synchronized StringBuffer append(String str) { //忽略其餘代碼 super.append(str); return this; }
幾乎全部場景字符串拼接都不涉及到線程同步,所以StringBuffer已經不多使用了,如上的字符串拼接例子使用StringBuffer,
@Benchmark public String concatbyBuffer(){ StringBuffer sb = new StringBuffer(); sb.append(a); sb.append(b); return sb.toString(); }
輸出以下
Benchmark Mode Score Units c.i.c.c.StringConcatTest.concatbyBuffer avgt 111.417 ns/op c.i.c.c.StringConcatTest.concatbyBuilder avgt 94.758 ns/op
能夠看到,StringBuffer拼接性能跟StringBuilder相比性能並不差,這得益於虛擬機的"逃逸分析",也就是JIT在打開逃逸分析狀況以及鎖消除的狀況下,有可能消除該對象上的使用synchronzied限定的鎖。
逃逸分析 -XX:+DoEscapeAnalysis和 鎖消除-XX:+EliminateLocks,詳情參考本書第8章JIT優化
以下是一個鎖消除的例子,對象obj只在方法內部使用,所以能夠消除synchronized
void foo() { //建立一個對象 Object obj = new Object(); synchronized (obj) { doSomething(); } }
程序不該該依賴JIT的優化,儘管打開了逃逸分析和鎖消除,但不能保證全部代碼都會被優化,由於鎖消除是在JIT的C2階段優化的,做爲程序員,應該在無關線程安全狀況下,使用StringBuilder。
使用StringBuilder 拼接其餘類型,尤爲是數字類型,則性能會明顯降低,這是由於數字類型轉字符在JDK內部,須要作不少工做,一個簡單的Int類型轉爲字符串,須要至少50行代碼完成。咱們在第一章已經看到過了,這裏再也不詳細說明。當你用StringBuilder來拼接字符串,拼接數字的時候,你須要思考,是否須要一個這樣的字符串。
咱們都知道浮點型變量在進行計算的時候會出現丟失精度的問題。以下一段代碼
System.out.println(0.05 + 0.01); System.out.println(1.0 - 0.42);
輸出: 0.060000000000000005 0.5800000000000001
能夠看到在Java中進行浮點數運算的時候,會出現丟失精度的問題。那麼咱們若是在進行商品價格計算的時候,就會出現問題。頗有可能形成咱們手中有0.06元,卻沒法購買一個0.05元和一個0.01元的商品。由於如上所示,他們兩個的總和爲0.060000000000000005。這無疑是一個很嚴重的問題,尤爲是當電商網站的併發量上去的時候,出現的問題將是巨大的。可能會致使沒法下單,或者對帳出現問題。
一般有倆個方法來解決這種問題,若是能用long來表示帳戶餘額以分爲單位,這是效率最高的。若是不能,則只能使用BigDecimal類來解決這類問題。
BigDecimal a = new BigDecimal("0.05"); BigDecimal b = new BigDecimal("0.01"); BigDecimal ret = a.add(b); System.out.println(ret.toString());
經過字符串來構造BigDecimal,才能保證精度不丟失,若是使用new BigDecimal(0.05),則由於0.05自己精度丟失,使得構造出來的BigDecimal也丟失精度。
BigDecimal能保證精度,但計算會有必定性能影響,以下是測試餘額計算,用long表示分,用BigDecimal表示元的性能對比
BigDecimal a = new BigDecimal("0.05"); BigDecimal b = new BigDecimal("0.01"); long c = 5; long d = 1; @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public long addByLong() { return (c + d); } @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public BigDecimal addByBigDecimal() { return a.add(b); }
在個人機器行,上面代碼都能進行精確計算,經過JMH,測試結果以下
Benchmark Mode Score Units c.i.c.c.BigDecimalTest.addByBigDecimal avgt 8.373 ns/op c.i.c.c.BigDecimalTest.addByLong avgt 2.984 ns/op
因此在項目裏,若是涉及精度結算,不要使用double,能夠考慮用BigDecmal,也可使用long來完成精度計算,具備良好的性能,分佈式或者微服務場景,考慮到序列化和反序列化,long也是能被全部序列化框架識別的