Java 性能優(yōu)化

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

image-20201014145901815.png

具體情況如下撑瞧。

1.top 命令 —— CPU 性能

如下圖棵譬,當(dāng)進入 top 命令后,按 1 鍵即可看到每核 CPU 的運行指標(biāo)和詳細(xì)性能预伺。

image-20201014142818183.png

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ù)值。

image-20201014143406966.png
image-20201014143634492.png

如上圖所示逢渔,以單核操作系統(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)

image-20201014143837599.png

比較關(guān)注的有下面幾列:

  • b 如果系統(tǒng)有負(fù)載問題,就可以看一下 b 列(Uninterruptible Sleep)历等,它的意思是等待 I/O讨惩,可能是讀盤或者寫盤動作比較多;

  • si/so 顯示了交換分區(qū)的一些使用情況寒屯,交換分區(qū)對性能的影響比較大荐捻,需要格外關(guān)注;

cs 每秒鐘上下文切換(Context Switch)的數(shù)量寡夹,如果上下文切換過于頻繁处面,就需要考慮是否是進程或者線程數(shù)開的過多。

ps -a
image-20201014144822462.png
cat /proc/15115/status
image-20201014145004810.png

進程狀態(tài)是T—— Stopped菩掏。然后看看voluntary_ctxt_switches 和nonvoluntary_ctxt_switches的數(shù)值 —— 它可以告訴你進程占用(或者釋放)了多少次CPU魂角。等幾秒鐘之后,再次執(zhí)行該命令智绸,看看這些數(shù)值有沒有增加野揪。這些數(shù)值沒有增加,據(jù)此可以得出結(jié)論瞧栗,這個進程是掛死了

內(nèi)存

image-20201014150305504.png

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 命令

image-20201014142818183.png

如上圖所示唇辨,我們看一下內(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)存之間的速度差異非常大,解決方式就是加入高速緩存捧韵。實際上市咆,這些高速緩存往往會有多層,如下圖所示再来。

image-20201014151421124.png

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é)果:

image-20201014151549383.png

在 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)格測試,僅作參考)邢羔。

image-20201014154418546.png

如上圖所示驼抹,可以看到普通磁盤的隨機寫與順序?qū)懴嗖罘浅4螅樞驅(qū)懪c CPU 內(nèi)存依舊不在一個數(shù)量級上拜鹤。

1. iostat

最能體現(xiàn) I/O 繁忙程度的框冀,就是 top 命令和 vmstat 命令中的 wa%。如果你的應(yīng)用寫了大量的日志敏簿,I/O wait 就可能非常高明也。

image-20201014155909092.png

便捷好用的查看磁盤 I/O 的工具宣虾,iostat 就是

image-20201014160056730.png

上圖中的指標(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ā)送出去滋觉。
image-20201014161130644.png

沒有采取零拷貝手段的圖

(2)采取了零拷貝手段

零拷貝有多種模式签夭,我們用 sendfile 來舉例。如下圖所示椎侠,在內(nèi)核的支持下第租,零拷貝少了一個步驟,那就是內(nèi)核緩存向用戶空間的拷貝我纪,這樣既節(jié)省了內(nèi)存慎宾,也節(jié)省了 CPU 的調(diào)度時間,讓效率更高浅悉。

image-20201014161154536.png

采取了零拷貝手段的圖

如何獲取代碼性能數(shù)據(jù)

nmon —— 獲取系統(tǒng)性能數(shù)據(jù)

除了在上一課時中介紹的 top趟据、free 等命令,還有一些將資源整合在一起的監(jiān)控工具术健,

nmon 便是一個老牌的 Linux 性能監(jiān)控工具汹碱,它不僅有漂亮的監(jiān)控界面(如下圖所示),還能產(chǎn)出細(xì)致的監(jiān)控報表荞估。

image-20201014170308528.png

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

image-20201014174512330.png

jvisualvm —— 獲取 JVM 性能數(shù)據(jù)

jvisualvm 原是隨著 JDK 發(fā)布的一個工具按价,Java 9 之后開始單獨發(fā)布惭适。通過它,可以了解應(yīng)用在運行中的內(nèi)部情況楼镐。我們可以連接本地或者遠程的服務(wù)器癞志,監(jiān)控大量的性能數(shù)據(jù)。

通過插件功能框产,jvisualvm 能獲得更強大的擴展凄杯。如下圖所示,建議把所有的插件下載下來進行體驗茅信。

image-20201014175149887.png

要想監(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)境中使用此功能窑多。

image-20201014175334085.png

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)皆疹。

image-20201014175927631.png

2內(nèi)存

通過內(nèi)存界面疏橄,可以看到每個時間段內(nèi)內(nèi)存的申請情況。在排查內(nèi)存溢出略就、內(nèi)存泄漏等情況時捎迫,這個功能非常有用。

image-20201014175951617.png

篇幅有限~~~暫時不介紹

案例分析

緩存

和緩沖類似表牢,緩存可能是軟件中使用最多的優(yōu)化技術(shù)了窄绒,比如:在最核心的 CPU 中,就存在著多級緩存崔兴;為了消除內(nèi)存和存儲之間的差異彰导,各種類似 Redis 的緩存框架更是層出不窮。

緩存的優(yōu)化效果是非常好的敲茄,它既可以讓原本載入非常緩慢的頁面位谋,瞬間秒開,也能讓本是壓力山大的數(shù)據(jù)庫折汞,瞬間清閑下來倔幼。

緩存本質(zhì)上是為了協(xié)調(diào)兩個速度差異非常大的組件爽待,如下圖所示损同,通過加入一個中間層翩腐,將常用的數(shù)據(jù)存放在相對高速的設(shè)備中。

image-20201014180446328.png

在我們平常的應(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 的常用操作:

image-20201014180646566.png

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ù)源進行獲绕诰尽凤薛;第二次就瞬間返回了缤苫,也就是緩存命中了活玲。具體時序可以參見下面這張圖。

image-20201014182825908.png

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嚼鹉。

image-20201014183537730.png

如上圖锚赤,按照緩存的一般設(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。

image-20201014183500124.png

推薦使用 Guava Cache 或者 Caffeine 作為堆內(nèi)緩存解決方案号枕,然后通過它們提供的一系列監(jiān)控指標(biāo),來調(diào)整緩存的大小和內(nèi)容沟绪,一般來說:

緩存命中率達到 50% 以上,作用就開始變得顯著铆隘;

緩存命中率低于 10%匣屡,那就需要考慮緩存組件的必要性了。

引入緩存組件,能夠顯著提升系統(tǒng)性能,但也會引入新的問題镐牺。其中秽梅,最典型的問題:如何保證緩存與源數(shù)據(jù)的同步朵诫?

以后再說~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖符喝,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件郊尝,死亡現(xiàn)場離奇詭異流昏,居然都是意外死亡知市,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來泌神,“玉大人良漱,你說我怎么就攤上這事舞虱。” “怎么了母市?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵矾兜,是天一觀的道長。 經(jīng)常有香客問我患久,道長椅寺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任蒋失,我火速辦了婚禮返帕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘篙挽。我一直安慰自己荆萤,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布铣卡。 她就那樣靜靜地躺著链韭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪算行。 梳的紋絲不亂的頭發(fā)上梧油,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機與錄音州邢,去河邊找鬼儡陨。 笑死,一個胖子當(dāng)著我的面吹牛量淌,可吹牛的內(nèi)容都是我干的骗村。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼呀枢,長吁一口氣:“原來是場噩夢啊……” “哼胚股!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起裙秋,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤琅拌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后摘刑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體进宝,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年枷恕,在試婚紗的時候發(fā)現(xiàn)自己被綠了党晋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖未玻,靈堂內(nèi)的尸體忽然破棺而出灾而,到底是詐尸還是另有隱情,我是刑警寧澤扳剿,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布旁趟,位于F島的核電站,受9級特大地震影響舞终,放射性物質(zhì)發(fā)生泄漏轻庆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一敛劝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧纷宇,春花似錦夸盟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拓春,卻和暖如春释簿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背硼莽。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工庶溶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人懂鸵。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓偏螺,卻偏偏與公主長得像,于是被迫代替她去往敵國和親匆光。 傳聞我的和親對象是個殘疾皇子套像,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,781評論 2 354