Java 性能優(yōu)化

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

image-20201014145901815.png

具體情況如下。

1.top 命令 —— CPU 性能

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

image-20201014142818183.png

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

image-20201014143406966.png
image-20201014143634492.png

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

image-20201014143837599.png

比較關(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
image-20201014144822462.png
cat /proc/15115/status
image-20201014145004810.png

進(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)存

image-20201014150305504.png

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

image-20201014142818183.png

如上圖所示,我們看一下內(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í)際上,這些高速緩存往往會有多層,如下圖所示。

image-20201014151421124.png

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

image-20201014151549383.png

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

image-20201014154418546.png

如上圖所示,可以看到普通磁盤的隨機(jī)寫與順序?qū)懴嗖罘浅4?,但順序?qū)懪c CPU 內(nèi)存依舊不在一個(gè)數(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)注這個(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ā)送出去。
image-20201014161130644.png

沒有采取零拷貝手段的圖

(2)采取了零拷貝手段

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

image-20201014161154536.png

采取了零拷貝手段的圖

如何獲取代碼性能數(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)表。

image-20201014170308528.png

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

image-20201014174512330.png

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

image-20201014175149887.png

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

image-20201014175334085.png

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

image-20201014175927631.png

2內(nèi)存

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

image-20201014175951617.png

篇幅有限~~~暫時(shí)不介紹

案例分析

緩存

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

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

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

image-20201014180446328.png

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

image-20201014180646566.png

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í)序可以參見下面這張圖。

image-20201014182825908.png

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。

image-20201014183537730.png

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

image-20201014183500124.png

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

緩存命中率達(dá)到 50% 以上,作用就開始變得顯著;

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

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

以后再說~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容