自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

阿里一面:如何將重復(fù)性比較高的 String 類型的地址信息從 20GB 降到幾百兆?

開發(fā) 前端
Java 正則表達(dá)式使用的引擎實(shí)現(xiàn)是 NFA(Non deterministic Finite Automaton,確定型有窮自動機(jī))自動機(jī),這種正則表達(dá)式引擎在進(jìn)行字符匹配時會發(fā)生回溯(backtracking),而一旦發(fā)生回溯,那其消耗的時間就會變得很長,有可能是幾分鐘,也有可能是幾個小時,時間長短取決于回溯的次數(shù)和復(fù)雜度。

這次應(yīng)該是互聯(lián)網(wǎng)及軟件行業(yè)的第三次寒潮,大家在寒潮中一定要繼續(xù)保持學(xué)習(xí),寒潮挺過去以后還是會迎來新的發(fā)展機(jī)遇。

有粉絲去阿里面試,跟碼哥分享了其中一題面試問題「如何將重復(fù)性比較高的 String 類型的地址信息從 20GB 降到幾百兆?」。

今天,碼哥從多個角度帶你完全攻克這個知識點(diǎn),讓面試官眼前一亮。

切入正文......

莫慌,今天給大家見識一下不一樣的 String,從根上拿捏直達(dá) G 點(diǎn)。

并且碼哥分享一個例子:通過性能調(diào)優(yōu)我們能實(shí)現(xiàn)百兆內(nèi)存輕松存儲幾十 G 數(shù)據(jù)。

String對象是我們每天都「摸」的對象類型,但是她的性能問題我們卻總是忽略。

愛她,不能只會簡單一起玩耍,要深入了解String 的內(nèi)心深處,做一個「心有猛虎,細(xì)嗅薔薇」的暖男。

通過以下幾點(diǎn)分析,我們一步步揭開她的衣裳,直達(dá)內(nèi)心深處,提升一個 Level,讓 String 直接起飛。

String 身體解密

想要深入了解,就先從基本組成開始……

「String 締造者」對 String 對象做了大量優(yōu)化來節(jié)省內(nèi)存,從而提升 String 的性能:

圖片圖片

Java 6 及之前

數(shù)據(jù)存儲在 char[]數(shù)組中,String通過 offset 和 count兩個屬性定位 char[] 數(shù)據(jù)獲取字符串。

這樣可以高效快速的定位并共享數(shù)組對象,并且節(jié)省內(nèi)存,但是有可能導(dǎo)致內(nèi)存泄漏。

共享 char 數(shù)組為啥可能會導(dǎo)致內(nèi)存泄漏呢?

String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

public String substring(int beginIndex, int endIndex) {
    //check boundary
    return  new String(offset + beginIndex, endIndex - beginIndex, value);
}

調(diào)用 substring() 的時候雖然創(chuàng)建了新的字符串,但字符串的值 value 仍然指向的是內(nèi)存中的同一個數(shù)組,如下圖所示:

圖片圖片

如果我們僅僅是用 substring 獲取一小段字符,而原始 string字符串非常大的情況下,substring 的對象如果一直被引用。

此時 String 字符串也無法回收,從而導(dǎo)致內(nèi)存泄露。

如果有大量這種通過 substring 獲取超大字符串中一小段字符串的操作,會因?yàn)閮?nèi)存泄露而導(dǎo)致內(nèi)存溢出。

JDK7、8

去掉了 offset 和 count兩個變量,減少了 String 對象占用的內(nèi)存。

substring 源碼:

public String(char value[], int offset, int count) {
    this.value = Arrays.copyOfRange(value, offset, offset + count);
}

public String substring(int beginIndex, int endIndex) {
    int subLen = endIndex - beginIndex;
    return new String(value, beginIndex, subLen);
}

substring() 通過 new String() 返回了一個新的字符串對象,在創(chuàng)建新的對象時通過 Arrays.copyOfRange() 深度拷貝了一個新的字符數(shù)組。

如下圖所示:

圖片圖片

String.substring 方法不再共享 char[]數(shù)組的數(shù)據(jù),解決了可能內(nèi)存泄漏的問題。

Java 9

將 char[]字段改為 byte[],新增 coder屬性。

碼哥,為什么這么改呢?

一個 char 字符占 2 個字節(jié),16 位。存儲單字節(jié)編碼內(nèi)的字符(占一個字節(jié)的字符)就顯得非常浪費(fèi)。

為了節(jié)約內(nèi)存空間,于是使用了 1 個字節(jié)占 8 位的 byte 數(shù)組來存放字符串。

勤儉節(jié)約的女神,誰不愛……

新屬性 coder 的作用是:在計(jì)算字符串長度或者使用 indexOf()方法時,我們需要根據(jù)編碼類型來計(jì)算字符串長度。

coder 的值分別表示不同編碼類型:

  • 0:表示使用 Latin-1 (單字節(jié)編碼);
  • 1:使用UTF-16。

String 的不可變性

了解了String 的基本組成之后,發(fā)現(xiàn) String 還有一個比外在更性感的特性,她被 final關(guān)鍵字修飾,char 數(shù)組也是。

圖片圖片

我們知道類被 final 修飾代表該類不可繼承,而 char[]被 final+private 修飾,代表了 String 對象不可被更改。

String 對象一旦創(chuàng)建成功,就不能再對它進(jìn)行改變。

Chaya:“String class 對象使用 final 修飾有什么好處?”

安全性

當(dāng)你在調(diào)用其他方法時,比如調(diào)用一些系統(tǒng)級操作指令之前,可能會有一系列校驗(yàn)。

如果是可變類的話,可能在你校驗(yàn)過后,它的內(nèi)部的值又被改變了,這樣有可能會引起嚴(yán)重的系統(tǒng)崩潰問題。

高性能緩存

String不可變之后就能保證 hash值得唯一性,使得類似 HashMap容器才能實(shí)現(xiàn)相應(yīng)的 key-value 緩存功能。

實(shí)現(xiàn)字符串常量池

由于不可變,才得以實(shí)現(xiàn)字符串常量池。

字符串常量池指的是在創(chuàng)建字符串的時候,先去「常量池」查找是否創(chuàng)建過該「字符串」;

如果有,則不會開辟新空間創(chuàng)建字符串,而是直接把常量池中該字符串的引用返回給此對象。

創(chuàng)建字符串的兩種方式:

  • String str1 = “碼哥字節(jié)”;
  • String str2 = new String(“碼哥字節(jié)”);

當(dāng)代碼中使用第一種方式創(chuàng)建字符串對象時,JVM 首先會檢查該對象是否在字符串常量池中,如果在,就返回該對象引用。

否則新的字符串將在常量池中被創(chuàng)建,并返回該引用。

這樣可以減少同一個值的字符串對象的重復(fù)創(chuàng)建,節(jié)約內(nèi)存。

第二種方式創(chuàng)建,在編譯類文件時,"碼哥字節(jié)" 字符串將會放入到常量結(jié)構(gòu)中,在類加載時,“碼哥字節(jié)" 將會在常量池中創(chuàng)建;

在調(diào)用 new 時,JVM 命令將會調(diào)用 String 的構(gòu)造函數(shù),在堆內(nèi)存中創(chuàng)建一個 String 對象,同時該對象指向「常量池」中的“碼哥字節(jié)”字符串,str 指向剛剛在堆上創(chuàng)建的 String 對象;

如下圖:

圖片圖片

什么是對象和對象引用呀?

str 屬于方法棧的字面量,它指向堆中的 String 對象,并不是對象本。

對象在內(nèi)存中是一塊內(nèi)存地址,str 則是指向這個內(nèi)存地址的引用。

也就是說 str 并不是對象,而只是一個對象引用。

碼哥,字符串的不可變到底指的是什么呀?

String str = "Java";
str = "Java,yyds"

第一次賦值 「Java」,第二次賦值「Java,yyds」,str 值確實(shí)改變了,為什么我還說 String 對象不可變呢?

這是因?yàn)?str 只是 String 對象的引用,并不是對象本身。

真正的對象依然還在內(nèi)存中,沒有被改變。

優(yōu)化實(shí)戰(zhàn)

了解了 String 的對象實(shí)現(xiàn)原理和特性,是時候要深入女神內(nèi)心,結(jié)合實(shí)際場景,如何更上一層樓優(yōu)化 String 對象的使用。

大量字符串拼接對象如何優(yōu)化

既然 String 對象是不可變,所以我們在頻繁拼接字符串的時候是否意味著創(chuàng)建多個對象呢?

String str = "癩蛤蟆撩青蛙" + "長的丑" + "玩的花";

上面你的代碼,你是不是以為先生成「癩蛤蟆撩青蛙」對象,再生成「癩蛤蟆撩青蛙長的丑」對象,最后生成「癩蛤蟆撩青蛙長得丑玩的花」對象。

實(shí)際運(yùn)行中,只有一個對象生成。

Chaya:這是為什么呢?

雖然代碼寫的丑陋,但是編譯器自動優(yōu)化了代碼。再看下面例子:

String str = "小青蛙";

for(int i=0; i<1000; i++) {
     str += i;
}

上面的代碼編譯后,你可以看到編譯器同樣對這段代碼進(jìn)行了優(yōu)化。

Java 在進(jìn)行字符串的拼接時,JVM 編譯器會把上述代碼優(yōu)化,偏向使用 StringBuilder,這樣可以提高程序的效率。優(yōu)化后的代碼如下。

String str = "小青蛙";

for(int i=0; i<1000; i++) {
            str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}

即使如此,還是循環(huán)內(nèi)重復(fù)創(chuàng)建 StringBuilder對象。

敲黑板

所以做字符串拼接的時候,我建議你還是要顯示地使用 String Builder 來提升系統(tǒng)性能。

如果在多線程編程中,String 對象的拼接涉及到線程安全,你可以使用 StringBuffer。

重復(fù)性高的 String 信息優(yōu)化

重點(diǎn)在于使用運(yùn)用 intern 節(jié)省內(nèi)存。直接看intern() 方法的定義與源碼:

圖片圖片

intern() 是一個本地方法,它的定義中說的是,當(dāng)調(diào)用 intern 方法時,如果字符串常量池中已經(jīng)包含此字符串,則直接返回此字符串的引用。

否則將此字符串添加到常量池中,并返回字符串的引用。

如果不包含此字符串,先將字符串添加到常量池中,再返回此對象的引用。

Chaya:什么情況下適合使用 intern() 方法?

Twitter 工程師曾分享過一個 String.intern() 的使用示例,Twitter 每次發(fā)布消息狀態(tài)的時候,都會產(chǎn)生一個地址信息,以當(dāng)時 Twitter 用戶的規(guī)模預(yù)估,服務(wù)器需要 20G 的內(nèi)存來存儲地址信息。

public class Location {
    private String city;
    private String region;
    private String countryCode;
    private double longitude;
    private double latitude;
}

考慮到其中有很多用戶在地址信息上是有重合的,比如,國家、省份、城市等,這時就可以將這部分信息單獨(dú)列出一個類,以減少重復(fù),代碼如下:

public class SharedLocation {

  private String city;
  private String region;
  private String countryCode;
}

public class Location {

  private SharedLocation sharedLocation;
  double longitude;
  double latitude;
}

通過優(yōu)化,數(shù)據(jù)存儲大小減到了 20G 左右。

但對于內(nèi)存存儲這個數(shù)據(jù)來說,依然很大,怎么辦呢?

Twitter 工程師使用 String.intern() 使重復(fù)性非常高的地址信息存儲大小從 20G 降到幾百兆,從而優(yōu)化了 String 對象的存儲。

核心代碼如下:

SharedLocation sharedLocation = new SharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern());
sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());

弄個簡單例子方便理解:

String a =new String("abc").intern();
String b = new String("abc").intern();

System.out.print(a==b);

輸出結(jié)果:true。

在加載類的時候會在常量池中創(chuàng)建一個字符串對象,內(nèi)容是「abc」。

創(chuàng)建局部 a 變量時,調(diào)用 new Sting() 會在堆內(nèi)存中創(chuàng)建一個 String 對象,String 對象中的 char 數(shù)組將會引用常量池中字符串。

在調(diào)用 intern 方法之后,會去常量池中查找是否有等于該字符串對象的引用,有就返回引用。

創(chuàng)建 b 變量時,調(diào)用 new Sting() 會在堆內(nèi)存中創(chuàng)建一個 String 對象,String 對象中的 char 數(shù)組將會引用常量池中字符串。

在調(diào)用 intern 方法之后,會去常量池中查找是否有等于該字符串對象的引用,有就返回引用給局部變量。

而剛在堆內(nèi)存中的兩個對象,由于沒有引用指向它,將會被垃圾回收。

所以 a 和 b 引用的是同一個對象。

字符串分割優(yōu)化

split() 方法使用了正則表達(dá)式實(shí)現(xiàn)了其強(qiáng)大的分割功能,而正則表達(dá)式的性能是非常不穩(wěn)定的。

使用不恰當(dāng)會引起回溯問題,很可能導(dǎo)致 CPU 居高不下。

Java 正則表達(dá)式使用的引擎實(shí)現(xiàn)是 NFA(Non deterministic Finite Automaton,確定型有窮自動機(jī))自動機(jī),這種正則表達(dá)式引擎在進(jìn)行字符匹配時會發(fā)生回溯(backtracking),而一旦發(fā)生回溯,那其消耗的時間就會變得很長,有可能是幾分鐘,也有可能是幾個小時,時間長短取決于回溯的次數(shù)和復(fù)雜度。

所以我們應(yīng)該慎重使用 split() 方法,我們可以用String.indexOf()方法代替 split() 方法完成字符串的分割。

最后,出一個問題給大家,歡迎在評論區(qū)留言。

通過三種不同的方式創(chuàng)建了三個對象,再依次兩兩匹配,每組被匹配的兩個對象是否相等?代碼如下:

String str1 = "abc";
String str2 = new String("abc");
String str3 = str2.intern();
assertSame(str1 == str2);
assertSame(str2 == str3);
assertSame(str1 == str3)

博主簡介

碼哥,9 年互聯(lián)網(wǎng)公司后端工作經(jīng)驗(yàn),InfoQ 簽約作者、51CTO Top 紅人,阿里云開發(fā)者社區(qū)專家博主,目前擔(dān)任后端架構(gòu)師主責(zé),擅長 Redis、Spring、Kafka、MySQL技術(shù)和云原生微服務(wù)。

責(zé)任編輯:武曉燕 來源: 碼哥跳動
相關(guān)推薦

2019-02-11 13:55:03

Linux重復(fù)性壓迫損傷命令

2023-03-29 14:02:24

RPA自動化

2009-07-27 10:11:20

2021-03-18 14:26:25

AI 數(shù)據(jù)人工智能

2022-05-10 08:11:15

MySQL技巧結(jié)構(gòu)

2024-11-11 16:40:04

2019-01-02 09:10:56

2022-05-11 15:57:16

優(yōu)化SQL

2009-07-30 14:38:36

云計(jì)算

2020-09-19 17:46:20

React Hooks開發(fā)函數(shù)

2011-12-23 09:43:15

開源開放

2011-12-22 20:53:40

Android

2023-09-21 09:28:23

編寫代碼

2019-10-31 13:58:32

阿里電商系統(tǒng)

2023-12-01 09:11:33

大數(shù)據(jù)數(shù)據(jù)庫

2025-04-15 10:00:00

Feign負(fù)載均衡微服務(wù)

2024-10-22 15:25:20

2022-01-17 07:59:13

SpringSpringMVCSpringBoot

2025-03-20 09:59:55

Spring@ProfileJava

2022-05-11 22:15:51

云計(jì)算云平臺
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號