Java 性能優(yōu)化
哪些資源,容易成為瓶頸?
? 計(jì)算機(jī)各個(gè)組件之間的速度往往很不均衡,比如 CPU 和硬盤,比兔子和烏龜?shù)乃俣炔钸€大,那么按照我們前面介紹的木桶理論,可以說這個(gè)系統(tǒng)是存在著短板的。
? 當(dāng)系統(tǒng)存在短板時(shí),就會對性能造成較大的負(fù)面影響,比如當(dāng) CPU 的負(fù)載特別高時(shí),任務(wù)就會排隊(duì),不能及時(shí)執(zhí)行。而其中,CPU、內(nèi)存、I/O 這三個(gè)系統(tǒng)組件,又往往容易成為瓶頸。
CPU

具體情況如下。
1.top 命令 —— CPU 性能
如下圖,當(dāng)進(jìn)入 top 命令后,按 1 鍵即可看到每核 CPU 的運(yùn)行指標(biāo)和詳細(xì)性能。

CPU 的使用有多個(gè)維度的指標(biāo),下面分別說明:
us 用戶態(tài)所占用的 CPU 百分比,即引用程序所耗費(fèi)的 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ù)器上這個(gè)值很少發(fā)生變動(dòng),因?yàn)樗鼫y量的是宿主機(jī)對虛擬機(jī)的影響,即虛擬機(jī)等待宿主機(jī) CPU 的時(shí)間占比,這在一些超賣的云服務(wù)器上,經(jīng)常發(fā)生;
id 空閑 CPU 百分比。
一般地,我們比較關(guān)注空閑 CPU 的百分比,它可以從整體上體現(xiàn) CPU 的利用情況。
2.負(fù)載 —— CPU 任務(wù)排隊(duì)情況
如果我們評估 CPU 任務(wù)執(zhí)行的排隊(duì)情況,那么需要通過負(fù)載(load)來完成。除了 top 命令,使用 uptime 命令也能夠查看負(fù)載情況,load 的效果是一樣的,分別顯示了最近 1min、5min、15min 的數(shù)值。


如上圖所示,以單核操作系統(tǒng)為例,將 CPU 資源抽象成一條單向行駛的馬路,則會發(fā)生以下三種情況:
馬路上的車只有 4 輛,車輛暢通無阻,load 大約是 0.5;
馬路上的車有 8 輛,正好能首尾相接安全通過,此時(shí) load 大約為 1;
馬路上的車有 12 輛,除了在馬路上的 8 輛車,還有 4 輛等在馬路外面,需要排隊(duì),此時(shí) load 大約為 1.5。
那 load 為 1 代表的是啥?針對這個(gè)問題,誤解還是比較多的。
很多人看到 load 的值達(dá)到 1,就認(rèn)為系統(tǒng)負(fù)載已經(jīng)到了極限。這在單核的硬件上沒有問題,但在多核硬件上,這種描述就不完全正確,它還與 CPU 的個(gè)數(shù)有關(guān)。例如:
單核的負(fù)載達(dá)到 1,總 load 的值約為 1;
雙核的每核負(fù)載都達(dá)到 1,總 load 約為 2;
四核的每核負(fù)載都達(dá)到 1,總 load 約為 4。
所以,對于一個(gè) load 到了 10,卻是 16 核的機(jī)器,你的系統(tǒng)還遠(yuǎn)沒有達(dá)到負(fù)載極限。
3.vmstat —— CPU 繁忙程度
要看 CPU 的繁忙程度,可以通過 vmstat 命令,下圖是 vmstat 命令的一些輸出信息。(Mac OS下面是vm_stat)

比較關(guān)注的有下面幾列:
b 如果系統(tǒng)有負(fù)載問題,就可以看一下 b 列(Uninterruptible Sleep),它的意思是等待 I/O,可能是讀盤或者寫盤動(dòng)作比較多;
si/so 顯示了交換分區(qū)的一些使用情況,交換分區(qū)對性能的影響比較大,需要格外關(guān)注;
cs 每秒鐘上下文切換(Context Switch)的數(shù)量,如果上下文切換過于頻繁,就需要考慮是否是進(jìn)程或者線程數(shù)開的過多。
ps -a

cat /proc/15115/status

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

MMU是Memory Management Unit的縮寫,中文名是內(nèi)存管理單元。MMU的作用是把虛擬地址轉(zhuǎn)換成物理地址。TLB其實(shí)就是一塊高速緩存。
邏輯地址可以映射到兩個(gè)內(nèi)存段上:物理內(nèi)存和虛擬內(nèi)存,那么整個(gè)系統(tǒng)可用的內(nèi)存就是兩者之和。比如你的物理內(nèi)存是 4GB,分配了 8GB 的 SWAP 分區(qū),那么應(yīng)用可用的總內(nèi)存就是 12GB。
1. top 命令

如上圖所示,我們看一下內(nèi)存的幾個(gè)參數(shù),從 top 命令可以看到幾列數(shù)據(jù),注意方塊框起來的三個(gè)區(qū)域,解釋如下:
- VIRT 這里是指虛擬內(nèi)存,一般比較大,不用做過多關(guān)注;
- RES 我們平常關(guān)注的是這一列的數(shù)值,它代表了進(jìn)程實(shí)際占用的內(nèi)存,平常在做監(jiān)控時(shí),主要監(jiān)控的也是這個(gè)數(shù)值;
- SHR 指的是共享內(nèi)存,比如可以復(fù)用的一些 so 文件等。
2. CPU 緩存
由于 CPU 和內(nèi)存之間的速度差異非常大,解決方式就是加入高速緩存。實(shí)際上,這些高速緩存往往會有多層,如下圖所示。

Java 有大部分知識點(diǎn)是圍繞多線程的,那是因?yàn)椋绻粋€(gè)線程的時(shí)間片跨越了多個(gè) CPU,那么就會存在同步問題。
在 Java 中,和 CPU 緩存相關(guān)的最典型的知識點(diǎn),就是在并發(fā)編程中,針對 Cache line 的偽共享(False Sharing)問題。
偽共享指的是在這些高速緩存中,以緩存行為單位進(jìn)行存儲,哪怕你修改了緩存行中一個(gè)很小很小的數(shù)據(jù),它都會整個(gè)刷新。所以,當(dāng)多線程修改一些變量的值時(shí),如果這些變量都在同一個(gè)緩存行里,就會造成頻繁刷新,無意中影響彼此的性能。
CPU 的每個(gè)核,基本是相同的,我們拿 CPU0 來說,可以通過以下的命令查看它的緩存行大小,這個(gè)值一般是 64。
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
當(dāng)然,通過 cpuinfo 也能得到一樣的結(jié)果:

在 JDK8 以上的版本,通過開啟參數(shù) -XX:-RestrictContended,就可以使用注解 @sun.misc.Contended 進(jìn)行補(bǔ)齊,來避免偽共享的問題。
3. 預(yù)先加載
另外,一些程序的默認(rèn)行為也會對性能有所影響,比如 JVM 的 -XX:+AlwaysPreTouch 參數(shù)。
默認(rèn)情況下,JVM 雖然配置了 Xmx、Xms 等參數(shù),指定堆的初始化大小和最大大小,但它的內(nèi)存在真正用到時(shí),才會分配;但如果加上 AlwaysPreTouch 這個(gè)參數(shù),JVM 會在啟動(dòng)的時(shí)候,就把所有的內(nèi)存預(yù)先分配。
這樣,啟動(dòng)時(shí)雖然慢了些,但運(yùn)行時(shí)的性能會增加。
I/O
I/O 設(shè)備可能是計(jì)算機(jī)里速度最慢的組件了,它指的不僅僅是硬盤,還包括外圍的所有設(shè)備。那硬盤有多慢呢?我們不去探究不同設(shè)備的實(shí)現(xiàn)細(xì)節(jié),直接看它的寫入速度(數(shù)據(jù)未經(jīng)過嚴(yán)格測試,僅作參考)。

如上圖所示,可以看到普通磁盤的隨機(jī)寫與順序?qū)懴嗖罘浅4?,但順序?qū)懪c CPU 內(nèi)存依舊不在一個(gè)數(shù)量級上。
1. iostat
最能體現(xiàn) I/O 繁忙程度的,就是 top 命令和 vmstat 命令中的 wa%。如果你的應(yīng)用寫了大量的日志,I/O wait 就可能非常高。

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

上圖中的指標(biāo)詳細(xì)介紹如下所示。
- %util:我們非常關(guān)注這個(gè)數(shù)值,通常情況下,這個(gè)數(shù)字超過 80%,就證明 I/O 的負(fù)荷已經(jīng)非常嚴(yán)重了。
- Device:表示是哪塊硬盤,如果你有多塊磁盤,則會顯示多行。
- avgqu-sz:平均請求隊(duì)列的長度,這和十字路口排隊(duì)的汽車也非常類似。顯然,這個(gè)值越小越好。
- awai:響應(yīng)時(shí)間包含了隊(duì)列時(shí)間和服務(wù)時(shí)間,它有一個(gè)經(jīng)驗(yàn)值。通常情況下應(yīng)該是小于 5ms 的,如果這個(gè)值超過了 10ms,則證明等待的時(shí)間過長了。
- svctm:表示操作 I/O 的平均服務(wù)時(shí)間。你可以回憶一下第 01 課時(shí)的內(nèi)容,在這里就是 AVG 的意思。svctm 和 await 是強(qiáng)相關(guān)的,如果它們比較接近,則表示 I/O 幾乎沒有等待,設(shè)備的性能很好;但如果 await 比 svctm 的值高出很多,則證明 I/O 的隊(duì)列等待時(shí)間太長,進(jìn)而系統(tǒng)上運(yùn)行的應(yīng)用程序?qū)⒆兟?/li>
2. 零拷貝
硬盤上的數(shù)據(jù),在發(fā)往網(wǎng)絡(luò)之前,需要經(jīng)過多次緩沖區(qū)的拷貝,以及用戶空間和內(nèi)核空間的多次切換。如果能減少一些拷貝的過程,效率就能提升,所以零拷貝應(yīng)運(yùn)而生。
零拷貝是一種非常重要的性能優(yōu)化手段,比如常見的 Kafka、Nginx 等,就使用了這種技術(shù)。我們來看一下有無零拷貝之間的區(qū)別。
(1)沒有采取零拷貝手段
如下圖所示,傳統(tǒng)方式中要想將一個(gè)文件的內(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)核的支持下,零拷貝少了一個(gè)步驟,那就是內(nèi)核緩存向用戶空間的拷貝,這樣既節(jié)省了內(nèi)存,也節(jié)省了 CPU 的調(diào)度時(shí)間,讓效率更高。

采取了零拷貝手段的圖
如何獲取代碼性能數(shù)據(jù)
nmon —— 獲取系統(tǒng)性能數(shù)據(jù)
除了在上一課時(shí)中介紹的 top、free 等命令,還有一些將資源整合在一起的監(jiān)控工具,
nmon 便是一個(gè)老牌的 Linux 性能監(jiān)控工具,它不僅有漂亮的監(jiān)控界面(如下圖所示),還能產(chǎn)出細(xì)致的監(jiān)控報(bào)表。

nmon 監(jiān)控界面
上一課時(shí)介紹的一些操作系統(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 次,它會把這一段時(shí)間之內(nèi)的數(shù)據(jù)記錄下來。比如本次生成了 localhost_200623_1633.nmon 這個(gè)文件,我們把它從服務(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ā)布的一個(gè)工具,Java 9 之后開始單獨(dú)發(fā)布。通過它,可以了解應(yīng)用在運(yùn)行中的內(nèi)部情況。我們可以連接本地或者遠(yuǎn)程的服務(wù)器,監(jiān)控大量的性能數(shù)據(jù)。
通過插件功能,jvisualvm 能獲得更強(qiáng)大的擴(kuò)展。如下圖所示,建議把所有的插件下載下來進(jìn)行體驗(yàn)。

要想監(jiān)控遠(yuǎ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,同時(shí)配置不需要 SSL 安全認(rèn)證方式連接。
對于性能優(yōu)化來說,我們主要用到它的采樣器。注意,由于抽樣分析過程對程序運(yùn)行性能有較大的影響,一般我們只在測試環(huán)境中使用此功能。

jvisualvm CPU 性能采樣圖
對于一個(gè) Java 應(yīng)用來說,除了要關(guān)注它的 CPU 指標(biāo),垃圾回收方面也是不容忽視的性能點(diǎn),我們主要關(guān)注以下三點(diǎn)。
- CPU 分析:統(tǒng)計(jì)方法的執(zhí)行次數(shù)和執(zhí)行耗時(shí),這些數(shù)據(jù)可用于分析哪個(gè)方法執(zhí)行時(shí)間過長,成為熱點(diǎn)等。
- 內(nèi)存分析:可以通過內(nèi)存監(jiān)視和內(nèi)存快照等方式進(jìn)行分析,進(jìn)而檢測內(nèi)存泄漏問題,優(yōu)化內(nèi)存使用情況。
- 線程分析:可以查看線程的狀態(tài)變化,以及一些死鎖情況。
JMC —— 獲取 Java 應(yīng)用詳細(xì)性能數(shù)據(jù)
對于我們常用的 HotSpot 來說,有更強(qiáng)大的工具,那就是 JMC。 JMC 集成了一個(gè)非常好用的功能:JFR(Java Flight Recorder)。
JFR 功能是建在 JVM 內(nèi)部的,不需要額外依賴,可以直接使用,它能夠監(jiān)測大量數(shù)據(jù)。比如,我們提到的鎖競爭、延遲、阻塞等;甚至在 JVM 內(nèi)部,比如 SafePoint、JIT 編譯等,也能去分析。
1線程
以 C2 編譯器線程為例,可以看到詳細(xì)的熱點(diǎn)類,以及方法內(nèi)聯(lián)后的代碼大小。如下圖所示,C2 此時(shí)正在瘋狂運(yùn)轉(zhuǎn)。

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

篇幅有限~~~暫時(shí)不介紹
案例分析
緩存
和緩沖類似,緩存可能是軟件中使用最多的優(yōu)化技術(shù)了,比如:在最核心的 CPU 中,就存在著多級緩存;為了消除內(nèi)存和存儲之間的差異,各種類似 Redis 的緩存框架更是層出不窮。
緩存的優(yōu)化效果是非常好的,它既可以讓原本載入非常緩慢的頁面,瞬間秒開,也能讓本是壓力山大的數(shù)據(jù)庫,瞬間清閑下來。
緩存,本質(zhì)上是為了協(xié)調(diào)兩個(gè)速度差異非常大的組件,如下圖所示,通過加入一個(gè)中間層,將常用的數(shù)據(jù)存放在相對高速的設(shè)備中。

在我們平常的應(yīng)用開發(fā)中,根據(jù)緩存所處的物理位置,一般分為進(jìn)程內(nèi)緩存和進(jìn)程外緩存。
今天主要聚焦在進(jìn)程內(nèi)緩存上,在 Java 中,進(jìn)程內(nèi)緩存,就是我們常說的堆內(nèi)緩存。Spring 的默認(rèn)實(shí)現(xiàn)里,就包含 Ehcache、JCache、Caffeine、Guava Cache 等。
Guava 的 LoadingCache
Guava 是一個(gè)常用的工具包,其中的 LoadingCache(下面簡稱 LC),是非常好用的堆內(nèi)緩存工具。通過學(xué)習(xí) LC 的結(jié)構(gòu),即可了解堆內(nèi)緩存設(shè)計(jì)的一般思路。
緩存一般是比較昂貴的組件,容量是有限制的,設(shè)置得過小,或者過大,都會影響緩存性能:
- 緩存空間過小,就會造成高命中率的元素被頻繁移出,失去了緩存的意義;
- 緩存空間過大,不僅浪費(fèi)寶貴的緩存資源,還會對垃圾回收產(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 的大小。一般,我們只需給緩存提供一個(gè)上限。
- maximumSize 這個(gè)參數(shù)用來設(shè)置緩存池的最大容量,達(dá)到此容量將會清理其他元素;
- initialCapacity 默認(rèn)值是 16,表示初始化大?。?/li>
- concurrencyLevel 默認(rèn)值是 4,和初始化大小配合使用,表示會將緩存的內(nèi)存劃分成 4 個(gè) segment,用來支持高并發(fā)的存取。
2.緩存操作
那么緩存數(shù)據(jù)是怎么放進(jìn)去的呢?有兩種模式:
- 使用 put 方法手動(dòng)處理,比如,我從數(shù)據(jù)庫里查詢出一個(gè) User 對象,然后手動(dòng)調(diào)用代碼進(jìn)去;
- 主動(dòng)觸發(fā)( 這也是 Loading 這個(gè)詞的由來),通過提供一個(gè) CacheLoader 的實(shí)現(xiàn),就可以在用到這個(gè)對象的時(shí)候,進(jì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";
}
上面是主動(dòng)觸發(fā)的示例代碼,你可以使用 get 方法獲取緩存的值。比如,當(dāng)我們執(zhí)行 lc.get("a") 時(shí),第一次會比較緩慢,因?yàn)樗枰綌?shù)據(jù)源進(jìn)行獲取;第二次就瞬間返回了,也就是緩存命中了。具體時(shí)序可以參見下面這張圖。

3.回收策略
緩存的大小是有限的,滿了以后怎么辦?這就需要回收策略進(jìn)行處理,接下來我會向你介紹三種回收策略。
(1)第一種回收策略基于容量
這個(gè)比較好理解,也就是說如果緩存滿了,就會按照 LRU 算法來移除其他元素。
(2)第二種回收策略基于時(shí)間
- 一種方式是,通過 expireAfterWrite 方法設(shè)置數(shù)據(jù)寫入以后在某個(gè)時(shí)間失效;
- 另一種是,通過 expireAfterAccess 方法設(shè)置最早訪問的元素,并優(yōu)先將其刪除。
(3)第三種回收策略基于 JVM 的垃圾回收
我們都知道對象的引用有強(qiáng)、軟、弱、虛等四個(gè)級別,通過 weakKeys 等函數(shù)即可設(shè)置相應(yīng)的引用級別。當(dāng) JVM 垃圾回收的時(shí)候,會主動(dòng)清理這些數(shù)據(jù)。
關(guān)于第三種回收策略,有一個(gè)高頻面試題:如果你同時(shí)設(shè)置了 weakKeys 和 weakValues函數(shù),LC 會有什么反應(yīng)?
答案:如果同時(shí)設(shè)置了這兩個(gè)函數(shù),它代表的意思是,當(dāng)沒有任何強(qiáng)引用,與 key 或者 value 有關(guān)系時(shí),就刪掉整個(gè)緩存項(xiàng)。這兩個(gè)函數(shù)經(jīng)常被誤解。
4.緩存造成內(nèi)存故障
LC 可以通過 recordStats 函數(shù),對緩存加載和命中率等情況進(jìn)行監(jiān)控。
值得注意的是:LC 是基于數(shù)據(jù)條數(shù)而不是基于緩存物理大小的,所以如果你緩存的對象特別大,就會造成不可預(yù)料的內(nèi)存占用。
圍繞這點(diǎn),我分享一個(gè)由于不正確使用緩存導(dǎo)致的常見內(nèi)存故障。
大多數(shù)堆內(nèi)緩存,都會將對象的引用設(shè)置成弱引用或軟引用,這樣內(nèi)存不足時(shí),可以優(yōu)先釋放緩存占用的空間,給其他對象騰出地方。這種做法的初衷是好的,但容易出現(xiàn)問題。
當(dāng)你的緩存使用非常頻繁,數(shù)據(jù)量又比較大的情況下,緩存會占用大量內(nèi)存,如果此時(shí)發(fā)生了垃圾回收(GC),緩存空間會被釋放掉,但又被迅速占滿,從而會再次觸發(fā)垃圾回收。如此往返,GC 線程會耗費(fèi)大量的 CPU 資源,緩存也就失去了它的意義。
所以在這種情況下,把緩存設(shè)置的小一些,減輕 JVM 的負(fù)擔(dān),是一個(gè)很好的方法。
緩存算法
1.算法介紹
堆內(nèi)緩存最常用的有 FIFO、LRU、LFU 這三種算法。
- FIFO
這是一種先進(jìn)先出的模式。如果緩存容量滿了,將會移除最先加入的元素。這種緩存實(shí)現(xiàn)方式簡單,但符合先進(jìn)先出的隊(duì)列模式場景的功能不多,應(yīng)用場景較少。
- LRU
LRU 是最近最少使用的意思,當(dāng)緩存容量達(dá)到上限,它會優(yōu)先移除那些最久未被使用的數(shù)據(jù),LRU是目前最常用的緩存算法,稍后我們會使用 Java 的 API 簡單實(shí)現(xiàn)一個(gè)。
- LFU
LFU 是最近最不常用的意思。相對于 LRU 的時(shí)間維度,LFU 增加了訪問次數(shù)的維度。如果緩存滿的時(shí)候,將優(yōu)先移除訪問次數(shù)最少的元素;而當(dāng)有多個(gè)訪問次數(shù)相同的元素時(shí),則優(yōu)先移除最久未被使用的元素。
2.實(shí)現(xiàn)一個(gè) LRU 算法
Java 里面實(shí)現(xiàn) LRU 算法可以有多種方式,其中最常用的就是 LinkedHashMap,*這也是一個(gè)需要你注意的*面試高頻考點(diǎn)**。
首先,我們來看一下 LinkedHashMap 的構(gòu)造方法:
復(fù)制代碼
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder)
accessOrder 參數(shù)是實(shí)現(xiàn) LRU 的關(guān)鍵。當(dāng) accessOrder 的值為 true 時(shí),將按照對象的訪問順序排序;當(dāng) accessOrder 的值為 false 時(shí),將按照對象的插入順序排序。我們上面提到過,按照訪問順序排序,其實(shí)就是 LRU。

如上圖,按照緩存的一般設(shè)計(jì)方式,和 LC 類似,當(dāng)你向 LinkedHashMap 中添加新對象的時(shí)候,就會調(diào)用 removeEldestEntry 方法。這個(gè)方法默認(rèn)返回 false,表示永不過期。我們只需要覆蓋這個(gè)方法,當(dāng)超出容量的時(shí)候返回 true,觸發(fā)移除動(dòng)作就可以了。關(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,這段代碼實(shí)現(xiàn)的功能是比較簡陋的,它甚至不是線程安全的,但它體現(xiàn)了緩存設(shè)計(jì)的一般思路,是 Java 中最簡單的 LRU 實(shí)現(xiàn)方式。
緩存優(yōu)化的一般思路
一般,緩存針對的主要是讀操作。當(dāng)你的功能遇到下面的場景時(shí),就可以選擇使用緩存組件進(jìn)行性能優(yōu)化:
- 存在數(shù)據(jù)熱點(diǎn),緩存的數(shù)據(jù)能夠被頻繁使用;
- 讀操作明顯比寫操作要多;
- 下游功能存在著比較懸殊的性能差異,下游服務(wù)能力有限;
- 加入緩存以后,不會影響程序的正確性,或者引入不可預(yù)料的復(fù)雜性。
緩存組件和緩沖類似,也是在兩個(gè)組件速度嚴(yán)重不匹配的時(shí)候,引入的一個(gè)中間層,但它們服務(wù)的目標(biāo)是不同的:
- 緩沖,數(shù)據(jù)一般只使用一次,等待緩沖區(qū)滿了,就執(zhí)行 flush 操作;
- 緩存,數(shù)據(jù)被載入之后,可以多次使用,數(shù)據(jù)將會共享多次。
緩存最重要的指標(biāo)就是命中率,有以下幾個(gè)因素會影響命中率。
(1)緩存容量
緩存的容量總是有限制的,所以就存在一些冷數(shù)據(jù)的逐出問題。但緩存也不是越大越好,它不能明顯擠占業(yè)務(wù)的內(nèi)存。
(2)數(shù)據(jù)集類型
如果緩存的數(shù)據(jù)是非熱點(diǎn)數(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)容,一般來說:
緩存命中率達(dá)到 50% 以上,作用就開始變得顯著;
緩存命中率低于 10%,那就需要考慮緩存組件的必要性了。
引入緩存組件,能夠顯著提升系統(tǒng)性能,但也會引入新的問題。其中,最典型的問題:如何保證緩存與源數(shù)據(jù)的同步?
以后再說~~~