Java 性能優(yōu)化
哪些資源蕾盯,容易成為瓶頸朗若?
? 計算機各個組件之間的速度往往很不均衡,比如 CPU 和硬盤镰矿,比兔子和烏龜?shù)乃俣炔钸€大,那么按照我們前面介紹的木桶理論俘种,可以說這個系統(tǒng)是存在著短板的秤标。
? 當(dāng)系統(tǒng)存在短板時,就會對性能造成較大的負(fù)面影響宙刘,比如當(dāng) CPU 的負(fù)載特別高時苍姜,任務(wù)就會排隊,不能及時執(zhí)行悬包。而其中衙猪,CPU、內(nèi)存布近、I/O 這三個系統(tǒng)組件垫释,又往往容易成為瓶頸。
CPU
具體情況如下撑瞧。
1.top 命令 —— CPU 性能
如下圖棵譬,當(dāng)進入 top 命令后,按 1 鍵即可看到每核 CPU 的運行指標(biāo)和詳細(xì)性能预伺。
CPU 的使用有多個維度的指標(biāo)订咸,下面分別說明:
us 用戶態(tài)所占用的 CPU 百分比,即引用程序所耗費的 CPU酬诀;
sy 內(nèi)核態(tài)所占用的 CPU 百分比脏嚷,需要配合 vmstat 命令,查看上下文切換是否頻繁料滥;
ni 高優(yōu)先級應(yīng)用所占用的 CPU 百分比然眼;
wa 等待 I/O 設(shè)備所占用的 CPU 百分比艾船,經(jīng)常使用它來判斷 I/O 問題葵腹,過高輸入輸出設(shè)備可能存在非常明顯的瓶頸高每;
hi 硬中斷所占用的 CPU 百分比;
si 軟中斷所占用的 CPU 百分比践宴;
st 在平常的服務(wù)器上這個值很少發(fā)生變動鲸匿,因為它測量的是宿主機對虛擬機的影響,即虛擬機等待宿主機 CPU 的時間占比阻肩,這在一些超賣的云服務(wù)器上带欢,經(jīng)常發(fā)生;
id 空閑 CPU 百分比烤惊。
一般地乔煞,我們比較關(guān)注空閑 CPU 的百分比,它可以從整體上體現(xiàn) CPU 的利用情況柒室。
2.負(fù)載 —— CPU 任務(wù)排隊情況
如果我們評估 CPU 任務(wù)執(zhí)行的排隊情況渡贾,那么需要通過負(fù)載(load)來完成。除了 top 命令雄右,使用 uptime 命令也能夠查看負(fù)載情況空骚,load 的效果是一樣的,分別顯示了最近 1min擂仍、5min囤屹、15min 的數(shù)值。
如上圖所示逢渔,以單核操作系統(tǒng)為例肋坚,將 CPU 資源抽象成一條單向行駛的馬路,則會發(fā)生以下三種情況:
馬路上的車只有 4 輛复局,車輛暢通無阻冲簿,load 大約是 0.5;
馬路上的車有 8 輛亿昏,正好能首尾相接安全通過峦剔,此時 load 大約為 1;
馬路上的車有 12 輛角钩,除了在馬路上的 8 輛車吝沫,還有 4 輛等在馬路外面,需要排隊递礼,此時 load 大約為 1.5惨险。
那 load 為 1 代表的是啥?針對這個問題脊髓,誤解還是比較多的辫愉。
很多人看到 load 的值達到 1,就認(rèn)為系統(tǒng)負(fù)載已經(jīng)到了極限将硝。這在單核的硬件上沒有問題恭朗,但在多核硬件上屏镊,這種描述就不完全正確,它還與 CPU 的個數(shù)有關(guān)痰腮。例如:
單核的負(fù)載達到 1而芥,總 load 的值約為 1;
雙核的每核負(fù)載都達到 1膀值,總 load 約為 2棍丐;
四核的每核負(fù)載都達到 1,總 load 約為 4沧踏。
所以歌逢,對于一個 load 到了 10,卻是 16 核的機器翘狱,你的系統(tǒng)還遠沒有達到負(fù)載極限趋翻。
3.vmstat —— CPU 繁忙程度
要看 CPU 的繁忙程度,可以通過 vmstat 命令盒蟆,下圖是 vmstat 命令的一些輸出信息踏烙。(Mac OS下面是vm_stat)
比較關(guān)注的有下面幾列:
b 如果系統(tǒng)有負(fù)載問題,就可以看一下 b 列(Uninterruptible Sleep)历等,它的意思是等待 I/O讨惩,可能是讀盤或者寫盤動作比較多;
si/so 顯示了交換分區(qū)的一些使用情況寒屯,交換分區(qū)對性能的影響比較大荐捻,需要格外關(guān)注;
cs 每秒鐘上下文切換(Context Switch)的數(shù)量寡夹,如果上下文切換過于頻繁处面,就需要考慮是否是進程或者線程數(shù)開的過多。
ps -a
cat /proc/15115/status
進程狀態(tài)是T—— Stopped菩掏。然后看看voluntary_ctxt_switches 和nonvoluntary_ctxt_switches的數(shù)值 —— 它可以告訴你進程占用(或者釋放)了多少次CPU魂角。等幾秒鐘之后,再次執(zhí)行該命令智绸,看看這些數(shù)值有沒有增加野揪。這些數(shù)值沒有增加,據(jù)此可以得出結(jié)論瞧栗,這個進程是掛死了
內(nèi)存
MMU是Memory Management Unit的縮寫斯稳,中文名是內(nèi)存管理單元。MMU的作用是把虛擬地址轉(zhuǎn)換成物理地址迹恐。TLB其實就是一塊高速緩存挣惰。
邏輯地址可以映射到兩個內(nèi)存段上:物理內(nèi)存和虛擬內(nèi)存,那么整個系統(tǒng)可用的內(nèi)存就是兩者之和。比如你的物理內(nèi)存是 4GB憎茂,分配了 8GB 的 SWAP 分區(qū)唆涝,那么應(yīng)用可用的總內(nèi)存就是 12GB。
1. top 命令
如上圖所示唇辨,我們看一下內(nèi)存的幾個參數(shù),從 top 命令可以看到幾列數(shù)據(jù)能耻,注意方塊框起來的三個區(qū)域赏枚,解釋如下:
- VIRT 這里是指虛擬內(nèi)存,一般比較大晓猛,不用做過多關(guān)注饿幅;
- RES 我們平常關(guān)注的是這一列的數(shù)值,它代表了進程實際占用的內(nèi)存戒职,平常在做監(jiān)控時栗恩,主要監(jiān)控的也是這個數(shù)值;
- SHR 指的是共享內(nèi)存洪燥,比如可以復(fù)用的一些 so 文件等磕秤。
2. CPU 緩存
由于 CPU 和內(nèi)存之間的速度差異非常大,解決方式就是加入高速緩存捧韵。實際上市咆,這些高速緩存往往會有多層,如下圖所示再来。
Java 有大部分知識點是圍繞多線程的蒙兰,那是因為,如果一個線程的時間片跨越了多個 CPU芒篷,那么就會存在同步問題搜变。
在 Java 中,和 CPU 緩存相關(guān)的最典型的知識點针炉,就是在并發(fā)編程中挠他,針對 Cache line 的偽共享(False Sharing)問題。
偽共享指的是在這些高速緩存中篡帕,以緩存行為單位進行存儲绩社,哪怕你修改了緩存行中一個很小很小的數(shù)據(jù),它都會整個刷新赂苗。所以愉耙,當(dāng)多線程修改一些變量的值時,如果這些變量都在同一個緩存行里拌滋,就會造成頻繁刷新朴沿,無意中影響彼此的性能。
CPU 的每個核,基本是相同的赌渣,我們拿 CPU0 來說魏铅,可以通過以下的命令查看它的緩存行大小,這個值一般是 64坚芜。
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
當(dāng)然览芳,通過 cpuinfo 也能得到一樣的結(jié)果:
在 JDK8 以上的版本,通過開啟參數(shù) -XX:-RestrictContended鸿竖,就可以使用注解 @sun.misc.Contended 進行補齊沧竟,來避免偽共享的問題。
3. 預(yù)先加載
另外缚忧,一些程序的默認(rèn)行為也會對性能有所影響悟泵,比如 JVM 的 -XX:+AlwaysPreTouch 參數(shù)。
默認(rèn)情況下闪水,JVM 雖然配置了 Xmx糕非、Xms 等參數(shù),指定堆的初始化大小和最大大小球榆,但它的內(nèi)存在真正用到時朽肥,才會分配;但如果加上 AlwaysPreTouch 這個參數(shù)持钉,JVM 會在啟動的時候鞠呈,就把所有的內(nèi)存預(yù)先分配。
這樣右钾,啟動時雖然慢了些蚁吝,但運行時的性能會增加。
I/O
I/O 設(shè)備可能是計算機里速度最慢的組件了舀射,它指的不僅僅是硬盤窘茁,還包括外圍的所有設(shè)備。那硬盤有多慢呢脆烟?我們不去探究不同設(shè)備的實現(xiàn)細(xì)節(jié)山林,直接看它的寫入速度(數(shù)據(jù)未經(jīng)過嚴(yán)格測試,僅作參考)邢羔。
如上圖所示驼抹,可以看到普通磁盤的隨機寫與順序?qū)懴嗖罘浅4螅樞驅(qū)懪c CPU 內(nèi)存依舊不在一個數(shù)量級上拜鹤。
1. iostat
最能體現(xiàn) I/O 繁忙程度的框冀,就是 top 命令和 vmstat 命令中的 wa%。如果你的應(yīng)用寫了大量的日志敏簿,I/O wait 就可能非常高明也。
便捷好用的查看磁盤 I/O 的工具宣虾,iostat 就是
上圖中的指標(biāo)詳細(xì)介紹如下所示。
- %util:我們非常關(guān)注這個數(shù)值温数,通常情況下绣硝,這個數(shù)字超過 80%,就證明 I/O 的負(fù)荷已經(jīng)非常嚴(yán)重了撑刺。
- Device:表示是哪塊硬盤鹉胖,如果你有多塊磁盤,則會顯示多行够傍。
- avgqu-sz:平均請求隊列的長度甫菠,這和十字路口排隊的汽車也非常類似。顯然王带,這個值越小越好。
- awai:響應(yīng)時間包含了隊列時間和服務(wù)時間市殷,它有一個經(jīng)驗值愕撰。通常情況下應(yīng)該是小于 5ms 的,如果這個值超過了 10ms醋寝,則證明等待的時間過長了搞挣。
- svctm:表示操作 I/O 的平均服務(wù)時間。你可以回憶一下第 01 課時的內(nèi)容音羞,在這里就是 AVG 的意思囱桨。svctm 和 await 是強相關(guān)的,如果它們比較接近嗅绰,則表示 I/O 幾乎沒有等待舍肠,設(shè)備的性能很好;但如果 await 比 svctm 的值高出很多窘面,則證明 I/O 的隊列等待時間太長翠语,進而系統(tǒng)上運行的應(yīng)用程序?qū)⒆兟?/li>
2. 零拷貝
硬盤上的數(shù)據(jù),在發(fā)往網(wǎng)絡(luò)之前财边,需要經(jīng)過多次緩沖區(qū)的拷貝肌括,以及用戶空間和內(nèi)核空間的多次切換。如果能減少一些拷貝的過程酣难,效率就能提升谍夭,所以零拷貝應(yīng)運而生。
零拷貝是一種非常重要的性能優(yōu)化手段憨募,比如常見的 Kafka紧索、Nginx 等,就使用了這種技術(shù)菜谣。我們來看一下有無零拷貝之間的區(qū)別齐板。
(1)沒有采取零拷貝手段
如下圖所示,傳統(tǒng)方式中要想將一個文件的內(nèi)容通過 Socket 發(fā)送出去,則需要經(jīng)過以下步驟:
- 將文件內(nèi)容拷貝到內(nèi)核空間甘磨;
- 將內(nèi)核空間內(nèi)存的內(nèi)容橡羞,拷貝到用戶空間內(nèi)存,比如 Java 應(yīng)用讀取 zip 文件济舆;
- 用戶空間將內(nèi)容寫入到內(nèi)核空間的緩存中卿泽;
- Socket 讀取內(nèi)核緩存中的內(nèi)容,發(fā)送出去滋觉。
沒有采取零拷貝手段的圖
(2)采取了零拷貝手段
零拷貝有多種模式签夭,我們用 sendfile 來舉例。如下圖所示椎侠,在內(nèi)核的支持下第租,零拷貝少了一個步驟,那就是內(nèi)核緩存向用戶空間的拷貝我纪,這樣既節(jié)省了內(nèi)存慎宾,也節(jié)省了 CPU 的調(diào)度時間,讓效率更高浅悉。
采取了零拷貝手段的圖
如何獲取代碼性能數(shù)據(jù)
nmon —— 獲取系統(tǒng)性能數(shù)據(jù)
除了在上一課時中介紹的 top趟据、free 等命令,還有一些將資源整合在一起的監(jiān)控工具术健,
nmon 便是一個老牌的 Linux 性能監(jiān)控工具汹碱,它不僅有漂亮的監(jiān)控界面(如下圖所示),還能產(chǎn)出細(xì)致的監(jiān)控報表荞估。
nmon 監(jiān)控界面
上一課時介紹的一些操作系統(tǒng)性能指標(biāo)咳促,都可從 nmon 中獲取。它的監(jiān)控范圍很廣勘伺,包括 CPU等缀、內(nèi)存、網(wǎng)絡(luò)娇昙、磁盤尺迂、文件系統(tǒng)、NFS冒掌、系統(tǒng)資源等信息噪裕。
nmon 在 sourceforge 發(fā)布,我已經(jīng)下載下來并上傳到了倉庫中股毫。比如我的是 CentOS 7 系統(tǒng)膳音,選擇對應(yīng)的版本即可執(zhí)行。
./nmon_x86_64_centos7
按 C 鍵可加入 CPU 面板铃诬;按 M 鍵可加入內(nèi)存面板祭陷;按 N 鍵可加入網(wǎng)絡(luò)苍凛;按 D 鍵可加入磁盤等。
通過下面的命令兵志,表示每 5 秒采集一次數(shù)據(jù)醇蝴,共采集 12 次,它會把這一段時間之內(nèi)的數(shù)據(jù)記錄下來想罕。比如本次生成了 localhost_200623_1633.nmon 這個文件悠栓,我們把它從服務(wù)器上下載下來。
./nmon_x86_64_centos7 -f -s 5 -c 12 -m .
scp -r root@10.162.12.96:/root/nmon/xs-cci-zhuji-sv_201014_1729.html /Users/chandler/Downloads
jvisualvm —— 獲取 JVM 性能數(shù)據(jù)
jvisualvm 原是隨著 JDK 發(fā)布的一個工具按价,Java 9 之后開始單獨發(fā)布惭适。通過它,可以了解應(yīng)用在運行中的內(nèi)部情況楼镐。我們可以連接本地或者遠程的服務(wù)器癞志,監(jiān)控大量的性能數(shù)據(jù)。
通過插件功能框产,jvisualvm 能獲得更強大的擴展凄杯。如下圖所示,建議把所有的插件下載下來進行體驗茅信。
要想監(jiān)控遠程的應(yīng)用盾舌,還需要在被監(jiān)控的 App 上加入 jmx 參數(shù)墓臭。
-Dcom.sun.management.jmxremote.port=14000
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
上述配置的意義是開啟 JMX 連接端口 14000蘸鲸,同時配置不需要 SSL 安全認(rèn)證方式連接。
對于性能優(yōu)化來說窿锉,我們主要用到它的采樣器酌摇。注意,由于抽樣分析過程對程序運行性能有較大的影響嗡载,一般我們只在測試環(huán)境中使用此功能窑多。
jvisualvm CPU 性能采樣圖
對于一個 Java 應(yīng)用來說,除了要關(guān)注它的 CPU 指標(biāo)洼滚,垃圾回收方面也是不容忽視的性能點埂息,我們主要關(guān)注以下三點。
- CPU 分析:統(tǒng)計方法的執(zhí)行次數(shù)和執(zhí)行耗時遥巴,這些數(shù)據(jù)可用于分析哪個方法執(zhí)行時間過長千康,成為熱點等。
- 內(nèi)存分析:可以通過內(nèi)存監(jiān)視和內(nèi)存快照等方式進行分析铲掐,進而檢測內(nèi)存泄漏問題拾弃,優(yōu)化內(nèi)存使用情況。
- 線程分析:可以查看線程的狀態(tài)變化摆霉,以及一些死鎖情況豪椿。
JMC —— 獲取 Java 應(yīng)用詳細(xì)性能數(shù)據(jù)
對于我們常用的 HotSpot 來說奔坟,有更強大的工具,那就是 JMC搭盾。 JMC 集成了一個非常好用的功能:JFR(Java Flight Recorder)咳秉。
JFR 功能是建在 JVM 內(nèi)部的,不需要額外依賴增蹭,可以直接使用滴某,它能夠監(jiān)測大量數(shù)據(jù)。比如滋迈,我們提到的鎖競爭霎奢、延遲、阻塞等饼灿;甚至在 JVM 內(nèi)部幕侠,比如 SafePoint、JIT 編譯等碍彭,也能去分析晤硕。
1線程
以 C2 編譯器線程為例,可以看到詳細(xì)的熱點類庇忌,以及方法內(nèi)聯(lián)后的代碼大小舞箍。如下圖所示,C2 此時正在瘋狂運轉(zhuǎn)皆疹。
2內(nèi)存
通過內(nèi)存界面疏橄,可以看到每個時間段內(nèi)內(nèi)存的申請情況。在排查內(nèi)存溢出略就、內(nèi)存泄漏等情況時捎迫,這個功能非常有用。
篇幅有限~~~暫時不介紹
案例分析
緩存
和緩沖類似表牢,緩存可能是軟件中使用最多的優(yōu)化技術(shù)了窄绒,比如:在最核心的 CPU 中,就存在著多級緩存崔兴;為了消除內(nèi)存和存儲之間的差異彰导,各種類似 Redis 的緩存框架更是層出不窮。
緩存的優(yōu)化效果是非常好的敲茄,它既可以讓原本載入非常緩慢的頁面位谋,瞬間秒開,也能讓本是壓力山大的數(shù)據(jù)庫折汞,瞬間清閑下來倔幼。
緩存,本質(zhì)上是為了協(xié)調(diào)兩個速度差異非常大的組件爽待,如下圖所示损同,通過加入一個中間層翩腐,將常用的數(shù)據(jù)存放在相對高速的設(shè)備中。
在我們平常的應(yīng)用開發(fā)中膏燃,根據(jù)緩存所處的物理位置茂卦,一般分為進程內(nèi)緩存和進程外緩存。
今天主要聚焦在進程內(nèi)緩存上组哩,在 Java 中等龙,進程內(nèi)緩存,就是我們常說的堆內(nèi)緩存伶贰。Spring 的默認(rèn)實現(xiàn)里蛛砰,就包含 Ehcache、JCache黍衙、Caffeine、Guava Cache 等琅翻。
Guava 的 LoadingCache
Guava 是一個常用的工具包,其中的 LoadingCache(下面簡稱 LC)聂抢,是非常好用的堆內(nèi)緩存工具。通過學(xué)習(xí) LC 的結(jié)構(gòu)棠众,即可了解堆內(nèi)緩存設(shè)計的一般思路琳疏。
緩存一般是比較昂貴的組件,容量是有限制的摄欲,設(shè)置得過小轿亮,或者過大疮薇,都會影響緩存性能:
- 緩存空間過小胸墙,就會造成高命中率的元素被頻繁移出,失去了緩存的意義按咒;
- 緩存空間過大迟隅,不僅浪費寶貴的緩存資源,還會對垃圾回收產(chǎn)生一定的壓力励七。
通過 Maven智袭,即可引入 guava 的 jar 包:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
下面介紹一下 LC 的常用操作:
1.緩存初始化
首先,我們可以通過下面的參數(shù)設(shè)置一下 LC 的大小掠抬。一般吼野,我們只需給緩存提供一個上限。
- maximumSize 這個參數(shù)用來設(shè)置緩存池的最大容量两波,達到此容量將會清理其他元素瞳步;
- initialCapacity 默認(rèn)值是 16闷哆,表示初始化大小单起;
- concurrencyLevel 默認(rèn)值是 4抱怔,和初始化大小配合使用,表示會將緩存的內(nèi)存劃分成 4 個 segment嘀倒,用來支持高并發(fā)的存取屈留。
2.緩存操作
那么緩存數(shù)據(jù)是怎么放進去的呢?有兩種模式:
- 使用 put 方法手動處理测蘑,比如,我從數(shù)據(jù)庫里查詢出一個 User 對象乍狐,然后手動調(diào)用代碼進去固逗;
- 主動觸發(fā)( 這也是 Loading 這個詞的由來)惜傲,通過提供一個 CacheLoader 的實現(xiàn)盗誊,就可以在用到這個對象的時候哈踱,進行延遲加載开镣。
public static void main(String[] args) {
LoadingCache<String, String> lc = CacheBuilder
.newBuilder()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return slowMethod(key);
}
});
}
static String slowMethod(String key) throws Exception {
Thread.sleep(1000);
return key + ".result";
}
上面是主動觸發(fā)的示例代碼邪财,你可以使用 get 方法獲取緩存的值树埠。比如伐厌,當(dāng)我們執(zhí)行 lc.get("a") 時,第一次會比較緩慢,因為它需要到數(shù)據(jù)源進行獲绕诰尽凤薛;第二次就瞬間返回了缤苫,也就是緩存命中了活玲。具體時序可以參見下面這張圖。
3.回收策略
緩存的大小是有限的镀迂,滿了以后怎么辦探遵?這就需要回收策略進行處理箱季,接下來我會向你介紹三種回收策略藏雏。
(1)第一種回收策略基于容量
這個比較好理解诉稍,也就是說如果緩存滿了,就會按照 LRU 算法來移除其他元素努酸。
(2)第二種回收策略基于時間
- 一種方式是获诈,通過 expireAfterWrite 方法設(shè)置數(shù)據(jù)寫入以后在某個時間失效舔涎;
- 另一種是亡嫌,通過 expireAfterAccess 方法設(shè)置最早訪問的元素于购,并優(yōu)先將其刪除肋僧。
(3)第三種回收策略基于 JVM 的垃圾回收
我們都知道對象的引用有強嫌吠、軟居兆、弱泥栖、虛等四個級別吧享,通過 weakKeys 等函數(shù)即可設(shè)置相應(yīng)的引用級別钢颂。當(dāng) JVM 垃圾回收的時候,會主動清理這些數(shù)據(jù)尼桶。
關(guān)于第三種回收策略趾盐,有一個高頻面試題:如果你同時設(shè)置了 weakKeys 和 weakValues函數(shù)救鲤,LC 會有什么反應(yīng)本缠?
答案:如果同時設(shè)置了這兩個函數(shù)犹赖,它代表的意思是峻村,當(dāng)沒有任何強引用粘昨,與 key 或者 value 有關(guān)系時张肾,就刪掉整個緩存項吞瞪。這兩個函數(shù)經(jīng)常被誤解芍秆。
4.緩存造成內(nèi)存故障
LC 可以通過 recordStats 函數(shù)妖啥,對緩存加載和命中率等情況進行監(jiān)控。
值得注意的是:LC 是基于數(shù)據(jù)條數(shù)而不是基于緩存物理大小的朽们,所以如果你緩存的對象特別大,就會造成不可預(yù)料的內(nèi)存占用菜枷。
圍繞這點犁跪,我分享一個由于不正確使用緩存導(dǎo)致的常見內(nèi)存故障坷衍。
大多數(shù)堆內(nèi)緩存枫耳,都會將對象的引用設(shè)置成弱引用或軟引用迁杨,這樣內(nèi)存不足時铅协,可以優(yōu)先釋放緩存占用的空間狐史,給其他對象騰出地方。這種做法的初衷是好的尼斧,但容易出現(xiàn)問題楼咳。
當(dāng)你的緩存使用非常頻繁爬橡,數(shù)據(jù)量又比較大的情況下糙申,緩存會占用大量內(nèi)存柜裸,如果此時發(fā)生了垃圾回收(GC)疙挺,緩存空間會被釋放掉铐然,但又被迅速占滿沥阳,從而會再次觸發(fā)垃圾回收桐罕。如此往返功炮,GC 線程會耗費大量的 CPU 資源薪伏,緩存也就失去了它的意義嫁怀。
所以在這種情況下眶掌,把緩存設(shè)置的小一些朴爬,減輕 JVM 的負(fù)擔(dān),是一個很好的方法逸爵。
緩存算法
1.算法介紹
堆內(nèi)緩存最常用的有 FIFO构韵、LRU疲恢、LFU 這三種算法显拳。
- FIFO
這是一種先進先出的模式杂数。如果緩存容量滿了,將會移除最先加入的元素次和。這種緩存實現(xiàn)方式簡單斯够,但符合先進先出的隊列模式場景的功能不多,應(yīng)用場景較少抓督。
- LRU
LRU 是最近最少使用的意思铃在,當(dāng)緩存容量達到上限阳液,它會優(yōu)先移除那些最久未被使用的數(shù)據(jù)揣炕,LRU是目前最常用的緩存算法鹰溜,稍后我們會使用 Java 的 API 簡單實現(xiàn)一個曹动。
- LFU
LFU 是最近最不常用的意思墓陈。相對于 LRU 的時間維度贡必,LFU 增加了訪問次數(shù)的維度赊级。如果緩存滿的時候理逊,將優(yōu)先移除訪問次數(shù)最少的元素晋被;而當(dāng)有多個訪問次數(shù)相同的元素時羡洛,則優(yōu)先移除最久未被使用的元素欲侮。
2.實現(xiàn)一個 LRU 算法
Java 里面實現(xiàn) LRU 算法可以有多種方式刁俭,其中最常用的就是 LinkedHashMap韧涨,*這也是一個需要你注意的*面試高頻考點**如孝。
首先第晰,我們來看一下 LinkedHashMap 的構(gòu)造方法:
復(fù)制代碼
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder)
accessOrder 參數(shù)是實現(xiàn) LRU 的關(guān)鍵但荤。當(dāng) accessOrder 的值為 true 時腹躁,將按照對象的訪問順序排序纺非;當(dāng) accessOrder 的值為 false 時赘方,將按照對象的插入順序排序炕淮。我們上面提到過涂圆,按照訪問順序排序润歉,其實就是 LRU嚼鹉。
如上圖锚赤,按照緩存的一般設(shè)計方式宴树,和 LC 類似,當(dāng)你向 LinkedHashMap 中添加新對象的時候翠霍,就會調(diào)用 removeEldestEntry 方法。這個方法默認(rèn)返回 false锄弱,表示永不過期会宪。我們只需要覆蓋這個方法掸鹅,當(dāng)超出容量的時候返回 true巍沙,觸發(fā)移除動作就可以了句携。關(guān)鍵代碼如下:
public class LRU extends LinkedHashMap {
int capacity;
public LRU(int capacity) {
super(16, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > capacity;
}
}
相比較 LC牡辽,這段代碼實現(xiàn)的功能是比較簡陋的态辛,它甚至不是線程安全的奏黑,但它體現(xiàn)了緩存設(shè)計的一般思路熟史,是 Java 中最簡單的 LRU 實現(xiàn)方式。
緩存優(yōu)化的一般思路
一般限寞,緩存針對的主要是讀操作履植。當(dāng)你的功能遇到下面的場景時,就可以選擇使用緩存組件進行性能優(yōu)化:
- 存在數(shù)據(jù)熱點妈橄,緩存的數(shù)據(jù)能夠被頻繁使用庶近;
- 讀操作明顯比寫操作要多;
- 下游功能存在著比較懸殊的性能差異眷蚓,下游服務(wù)能力有限鼻种;
- 加入緩存以后,不會影響程序的正確性溪椎,或者引入不可預(yù)料的復(fù)雜性普舆。
緩存組件和緩沖類似,也是在兩個組件速度嚴(yán)重不匹配的時候校读,引入的一個中間層沼侣,但它們服務(wù)的目標(biāo)是不同的:
- 緩沖轧膘,數(shù)據(jù)一般只使用一次蟆淀,等待緩沖區(qū)滿了疑苔,就執(zhí)行 flush 操作趁餐;
- 緩存,數(shù)據(jù)被載入之后候学,可以多次使用,數(shù)據(jù)將會共享多次。
緩存最重要的指標(biāo)就是命中率,有以下幾個因素會影響命中率。
(1)緩存容量
緩存的容量總是有限制的,所以就存在一些冷數(shù)據(jù)的逐出問題牧嫉。但緩存也不是越大越好鳍置,它不能明顯擠占業(yè)務(wù)的內(nèi)存辟拷。
(2)數(shù)據(jù)集類型
如果緩存的數(shù)據(jù)是非熱點數(shù)據(jù)邻奠,或者是操作幾次就不再使用的冷數(shù)據(jù),那命中率肯定會低干跛,緩存也會失去了它的作用遥赚。
(3)緩存失效策略
緩存算法也會影響命中率和性能愧薛,目前效率最高的算法是 Caffeine 使用的 W-TinyLFU 算法鲸郊,它的命中率非常高,內(nèi)存占用也更小舒裤。新版本的 spring-cache节值,已經(jīng)默認(rèn)支持 Caffeine。
推薦使用 Guava Cache 或者 Caffeine 作為堆內(nèi)緩存解決方案号枕,然后通過它們提供的一系列監(jiān)控指標(biāo),來調(diào)整緩存的大小和內(nèi)容沟绪,一般來說:
緩存命中率達到 50% 以上,作用就開始變得顯著铆隘;
緩存命中率低于 10%匣屡,那就需要考慮緩存組件的必要性了。
引入緩存組件,能夠顯著提升系統(tǒng)性能,但也會引入新的問題镐牺。其中秽梅,最典型的問題:如何保證緩存與源數(shù)據(jù)的同步朵诫?
以后再說~~~