擺脫性能問題和內(nèi)存泄漏!
如何確定 Java 線程池大小:綜合指南
線程池可減少創(chuàng)建線程的開銷(JVM 和操作系統(tǒng)的延遲和額外工作)。但是管理線程會增加開銷,因此是否使用線程池并不明確。線程池還可以幫助管理線程使用的資源。
調(diào)整線程池大小以從系統(tǒng)中提取最佳性能并平穩(wěn)應(yīng)對峰值工作負(fù)載。
線程池大小不應(yīng)超過數(shù)據(jù)存儲請求的連接池大?。ǚ駝t線程將等待連接,這是低效的);池大小也不應(yīng)超過池正在使用的任何外部服務(wù)的處理能力(以免壓垮該服務(wù));此外,線程池大小應(yīng)配置為低于壓垮可用 CPU 所需的閾值,但這取決于線程處理的任務(wù)的性質(zhì),即 CPU 密集型(小池,不超過核心數(shù))與 IO 密集型(池大小低于壓垮整體 CPU 和整體 IO 處理的大?。?。監(jiān)控上下文切換可以幫助決定這些情況。
如果有空閑的 CPU,您可以將 CPU 密集型任務(wù)劃分為更小的子任務(wù),并將這些子任務(wù)分布到多個 CPU 核心上,從而優(yōu)化 CPU 密集型任務(wù)。
您可以通過以下方式優(yōu)化 IO 密集型任務(wù):緩存經(jīng)常訪問的數(shù)據(jù);負(fù)載平衡:跨多個線程執(zhí)行任務(wù);使用 SSD 而不是旋轉(zhuǎn)磁盤;使用高效的數(shù)據(jù)結(jié)構(gòu),例如哈希表和 B 樹;消除不必要的文件操作(例如,不要多次打開和關(guān)閉同一個文件)。
對于 CPU 密集型任務(wù),確定線程池大小的常見經(jīng)驗(yàn)規(guī)則是使用可用的 CPU 核心數(shù)。
對于 I/O 密集型任務(wù),調(diào)整線程池的大小是為了擁有足夠多的線程,使 I/O 設(shè)備保持繁忙但又不會過載。
確定線程池大小的公式是線程數(shù) = 可用核心數(shù) * 目標(biāo) CPU 利用率 * (1 + (等待時間 / 服務(wù)時間)),其中目標(biāo) CPU 利用率是您希望應(yīng)用程序使用的 CPU 時間百分比,等待時間是線程等待 I/O 操作完成所花費(fèi)的時間,服務(wù)時間是線程執(zhí)行計算所花費(fèi)的時間。
Java 性能工具箱一覽
為了最小化容器大小,您可以使用剝離的基礎(chǔ)映像(例如 jre-slim)并安裝應(yīng)用程序所需的最小組件。
jlink 讓您生成自定義的剝離 JRE,它可以是您的應(yīng)用程序的最小運(yùn)行時(但請注意,如果您剝離我們的可服務(wù)性工具,如 jcmd,您將難以對生成的 JVM 進(jìn)行故障排除)。jdeps 讓您找到運(yùn)行您的應(yīng)用程序所需的剝離圖像的最小模塊集。
jcmd 可以從正在運(yùn)行的 JVM 獲取統(tǒng)計信息(例如 GC 統(tǒng)計信息),但過于頻繁地運(yùn)行 jcmd 會增加 JVM 的開銷。
僅在需要時啟用 -XX:NativeMemoryTracking,因?yàn)樗鼤o JVM 增加相當(dāng)大的開銷。
您可以使用任何 JMX 客戶端(例如 jconsole)觸發(fā)堆轉(zhuǎn)儲。
jstat 從正在運(yùn)行的 JVM 獲取統(tǒng)計數(shù)據(jù)。
jmap 讓您從正在運(yùn)行的 JVM 中獲取堆直方圖和堆轉(zhuǎn)儲。
可以啟用 JFR 記錄并讓您調(diào)查性能。您可以將 JFR 記錄從 JVM 中流出。
減少 Java 應(yīng)用程序中的網(wǎng)絡(luò)調(diào)用
緩存網(wǎng)絡(luò)調(diào)用,其中來自前一次調(diào)用的數(shù)據(jù)至少在某些時間/某些調(diào)用中足夠。緩存元素過期可以基于時間,也可以基于其他一些失效標(biāo)準(zhǔn)。
批量網(wǎng)絡(luò)調(diào)用,即對一個端點(diǎn)進(jìn)行多次調(diào)用,并且調(diào)用可以等待很短的時間。這減少了往返次數(shù)并提高了網(wǎng)絡(luò)效率。
在通過網(wǎng)絡(luò)發(fā)送數(shù)據(jù)之前對其進(jìn)行壓縮可以減少傳輸?shù)臄?shù)據(jù)量。
異步調(diào)用允許應(yīng)用程序在網(wǎng)絡(luò)調(diào)用進(jìn)行時繼續(xù)執(zhí)行。這減少了應(yīng)用程序被阻塞的時間。
優(yōu)化數(shù)據(jù)庫查詢以消除不必要的數(shù)據(jù)傳輸并最大限度地減少檢索的數(shù)據(jù)量。
檢查網(wǎng)絡(luò)調(diào)用,看是否完全沒有必要,并將其消除。
解決 Java 應(yīng)用程序中的本機(jī)內(nèi)存問題
可以通過堆轉(zhuǎn)儲來查找內(nèi)存泄漏的原因,但堆直方圖(例如通過
jmap -histo ...)可能會提供提示并且成本較低。本機(jī)內(nèi)存不足錯誤可能是由于其他進(jìn)程耗盡了系統(tǒng)內(nèi)存而導(dǎo)致的。您可以移除或減少它們的內(nèi)存使用量,和/或減少 JVM 上使用的堆以提供更多本機(jī)內(nèi)存。
JVM 內(nèi)存包括:堆、元空間(類和元數(shù)據(jù))、代碼緩存(字節(jié)碼和編譯后的代碼)、JVM 管理空間、已加載的 jar 和本機(jī)庫、線程堆棧和線程本地存儲、JNI/JVMTI 分配的內(nèi)存、NIO 分配、直接字節(jié)緩沖區(qū)。
要確認(rèn)是否存在本機(jī)內(nèi)存泄漏,您需要監(jiān)視 JVM 的駐留集大小(如果使用多映射,則監(jiān)視成比例的集大?。?。查找持續(xù)增加的內(nèi)存大小,然后通過將其與變化的進(jìn)程內(nèi)存大小進(jìn)行比較來排除堆內(nèi)存泄漏。
本機(jī)內(nèi)存跟蹤 (-XX:NativeMemoryTracking=summary / -XX:NativeMemoryTracking=detail) 可用于查看 JVM 已知的本機(jī)內(nèi)存。這會對性能造成 5%-10% 的影響。可以使用 獲取
jcmd報告VM.native_memory [baseline, detail.diff, summary.diff]。本機(jī)內(nèi)存泄漏的一個常見原因是類生成。在這種情況下,限制元空間 -XX:MaxMetaspaceSize 可以防止主機(jī)耗盡資源(盡管 JVM 仍會遇到 OutOfMemroy 錯誤)對于 JVM 不管理的內(nèi)存的本機(jī)內(nèi)存跟蹤,您需要使用本機(jī)內(nèi)存跟蹤工具,如 jemalloc、valgrind、dbx、purify、pmap 和核心轉(zhuǎn)儲文件。
本機(jī)內(nèi)存泄漏分析的總體過程是:1. 確定機(jī)器 / 容器是否具有足夠的本機(jī)內(nèi)存可用于預(yù)期的內(nèi)存使用量;2. 檢查 JVM 進(jìn)程內(nèi)存是否持續(xù)增加;3. 強(qiáng)制進(jìn)行垃圾收集并消除堆內(nèi)存作為原因(或修復(fù)它);4. 使用 NativeMemoryTracking 消除 JVM 管理的本機(jī)內(nèi)存作為原因(或修復(fù)它);5. 使用本機(jī)內(nèi)存分析器或轉(zhuǎn)儲分析來查找 JVM 管理的本機(jī)內(nèi)存之外的本機(jī)內(nèi)存泄漏的原因。
常見的本機(jī)內(nèi)存泄漏情況:使用與低內(nèi)存基數(shù)中的可用空間沖突的壓縮 oop(使用 -XX:HeapBaseMinAdress=n 修復(fù));直接字節(jié)緩沖區(qū)(確保 Java 包裝器已被釋放);NIO API(確保 Java 包裝器已被釋放);膨脹器/收縮器(確保你使用 end() 結(jié)束它們);終結(jié)器(確保終結(jié)器處理線程已運(yùn)行)。
防止內(nèi)存泄漏的策略包括:確保對象僅在需要時才處于范圍內(nèi);謹(jǐn)慎使用靜態(tài)字段;避免無限增長的靜態(tài)集合;在不再需要時,始終取消注冊偵聽器和回調(diào);限制緩存的大?。辉诓辉傩枰獙ο髸r,從集合中刪除它們;謹(jǐn)慎使用內(nèi)部類實(shí)例;使用后始終關(guān)閉資源(例如文件,流,連接);分析應(yīng)用程序的內(nèi)存使用情況;單元和集成測試以檢查內(nèi)存泄漏。
Java 內(nèi)存泄漏幾大原因總結(jié)
內(nèi)存泄漏的一個常見原因是無限增長的靜態(tài)集合。為防止泄漏,請限制集合的大小或定期清除它。
內(nèi)存泄漏的一個常見原因是未關(guān)閉流、連接或文件句柄等資源。為防止泄漏,請確保使用 finally 塊或 try-with-resources 關(guān)閉資源。
內(nèi)存泄漏的一個常見原因是非靜態(tài)內(nèi)部類比外部類存在時間長 - 因?yàn)檫@些內(nèi)部類隱式引用了外部類。要防止泄漏,請使用靜態(tài)內(nèi)部類或單獨(dú)的類。
內(nèi)存泄漏的一個常見原因是將緩存對象留在緩存中的時間過長。為防止泄漏,請使用支持過期的緩存庫,如 EhCache 或 Guava 的 Cache。
內(nèi)存泄漏的一個常見原因是線程池中不會終止的線程中的 ThreadLocal 變量。為防止泄漏,請在不再需要 ThreadLocal 變量時將其刪除。
內(nèi)存泄漏的一個常見原因是注冊偵聽器而不注銷它們。為了防止泄漏,請始終提供注銷偵聽器的機(jī)制。
內(nèi)存泄漏的一個常見原因是單例類持有大對象。為了防止泄漏,請確保單例類持有對象的時間不要超過必要的時間。
內(nèi)存泄漏的一個常見原因是未關(guān)閉數(shù)據(jù)庫連接。為防止泄漏,請在使用后始終關(guān)閉數(shù)據(jù)庫連接。
內(nèi)存泄漏的一個常見原因是頻繁加載和卸載類。為了防止泄漏,請謹(jǐn)慎處理類加載器和已加載類的生命周期。
每個開發(fā)人員和架構(gòu)師都必須知道的最重要的性能問題 - 第 2 部分 - 并發(fā)
運(yùn)行多個同時線程是一項簡單的任務(wù),只要它們不與可變共享對象(由多個線程共享或訪問但也可以被多個線程更改的對象)交互。
不可變的共享對象不會給多線程代碼帶來問題。
當(dāng)兩個或多個線程需要多個共享資源來完成其任務(wù),并且它們以不同的順序或不同的方式訪問這些資源時,就會發(fā)生死鎖。
過多的同步會導(dǎo)致大量線程緩慢且停滯。
在活鎖中,兩個或多個線程不斷在彼此之間傳輸狀態(tài)。它們不會像死鎖那樣等待,而是執(zhí)行無進(jìn)展的無用工作。通過檢查無效狀態(tài)(例如,當(dāng)隊列拋出異常時,重新將消息添加到隊列)來避免活鎖。
線程池的大小對性能很重要。如果線程池太小,那么當(dāng)有資源可用來處理請求時,請求將不必要地等待;但如果線程池太大,那么太多線程將同時執(zhí)行,爭奪處理資源,從而導(dǎo)致上下文切換并降低整體效率。
處理 Java 中對共享資源的并發(fā)訪問
當(dāng)您擁有共享資源時,并發(fā)性會變得很困難。多個線程對共享資源的訪問和更新是一種競爭條件。如果沒有并發(fā)協(xié)調(diào),訪問和更新的結(jié)果將是不確定的。
Java
synchronized和鎖(例如 ReentrantReadWriteLock)提供對共享資源的互斥訪問,這使得它們的同時使用具有確定性。volatile變量提供對共享資源的獨(dú)占訪問和更新,但僅適用于一項操作,而不是一組操作。要使復(fù)合操作確定性地并發(fā)執(zhí)行,您需要使用synchronized或鎖。當(dāng)多個線程嘗試以不同的順序鎖定多個
synchronized監(jiān)視器時,就會發(fā)生死鎖。一個線程獲取了第一個監(jiān)視器并嘗試獲取第二個監(jiān)視器,而第二個線程已經(jīng)獲取了第二個監(jiān)視器并正在嘗試獲取第一個監(jiān)視器 - 死鎖。如果使用,虛擬線程將固定底層平臺線程
synchronized,因此請小心避免將其synchronized用于任何非短暫操作。樂觀鎖定是一種不同的并發(fā)技術(shù),在這種技術(shù)中,嘗試執(zhí)行操作,如果在此期間沒有其他任何東西改變共享資源,則該操作會成功,但如果共享資源已發(fā)生改變,則該操作會失敗。
Java 死鎖預(yù)防和故障排除技巧
死鎖是指兩個或多個線程被阻塞并等待對方釋放資源的情況。死鎖可以通過以下方式檢測:線程堆棧轉(zhuǎn)儲;JConsole、VisualVM 等許多工具;以及 LockSupport 中的代碼。
防止死鎖的策略:始終以固定順序獲取鎖;避免長時間持有鎖;減少持有鎖的代碼塊的范圍;使用 ReentrantLock 和類似的并發(fā)鎖類來管理鎖,例如使用 tryLock() 來獲取鎖而不阻塞;獲取鎖時使用超時以防止線程無限期等待;使用非阻塞算法和數(shù)據(jù)結(jié)構(gòu)來完全避免死鎖。
Java 堆內(nèi)存優(yōu)化以改善查詢延遲
堆外內(nèi)存映射的速度可能很快,但仍然比堆內(nèi)內(nèi)存慢。因此,請根據(jù)延遲要求對數(shù)據(jù)進(jìn)行分段,并僅將延遲要求最低的數(shù)據(jù)保留在堆上。
-XX:+StringDeduplication(僅適用于 G1 GC)在空閑 CPU 周期內(nèi)刪除重復(fù)字符串(將重復(fù)字符串的 char[] 設(shè)置為指向相同的 char[])。這有利于減少內(nèi)存使用量,但會產(chǎn)生一些 CPU 開銷,并且會影響低延遲響應(yīng)(由于競爭并發(fā) CPU 開銷)。
Guava interners 支持隔離緩存,是一種廣泛使用的重復(fù)數(shù)據(jù)刪除技術(shù)。
FALF - 固定大小數(shù)組,無鎖 - 用于重復(fù)數(shù)據(jù)刪除對象的內(nèi)部器 - 代碼。
較小的堆往往可以通過降低 GC 成本來改善延遲。
低延遲模式
通用代碼很容易產(chǎn)生嚴(yán)重的尾部延遲。由于延遲會累積,如果最終用戶請求擊中多個內(nèi)部應(yīng)用請求,許多用戶將達(dá)到中間請求的尾部延遲。
最大延遲很難優(yōu)化,因此請重點(diǎn)關(guān)注接近最大延遲進(jìn)行優(yōu)化。
使用對數(shù) x 軸或 eCDF 可視化來可視化延遲。
通過以下方式減少延遲:避免移動數(shù)據(jù)、最小化操作和避免等待。
通過以下方式避免數(shù)據(jù)移動:避免網(wǎng)絡(luò)調(diào)用或盡量減少調(diào)用所產(chǎn)生的網(wǎng)絡(luò)距離;共置數(shù)據(jù);將數(shù)據(jù)復(fù)制到需要的地方;以及緩存數(shù)據(jù)。
通過減少操作來減少延遲:使用更簡單的算法;使用最小化操作開銷的內(nèi)存結(jié)構(gòu)(鏈表和圖表通常是低延遲的糟糕選擇);優(yōu)化代碼;避免 CPU 密集型計算;避免在快速/關(guān)鍵代碼路徑中分配內(nèi)存;避免需求分頁(確保正在使用的內(nèi)存已經(jīng)分頁)。
優(yōu)化代碼以實(shí)現(xiàn)低延遲意味著:減少 CPU 周期、減少 CPU 緩存未命中等。將長時間運(yùn)行的任務(wù)拆分為多個短任務(wù)。使用分析器查找效率低下的代碼。請注意,您經(jīng)常會用性能來換取其他東西(內(nèi)存或整體延遲)。
通過以下方式避免等待:消除同步(例如,按核心劃分?jǐn)?shù)據(jù)并僅在核心上處理);使用無等待算法;將代碼保留在用戶空間(避免內(nèi)核調(diào)用或盡可能繞過它);避免上下文切換(使用專用的線程到核心);使用異步/非阻塞 IO;使用忙輪詢;使共享數(shù)據(jù)結(jié)構(gòu)只讀;使用單生產(chǎn)者+單消費(fèi)者隊列在核心之間傳輸;使用 TCP_NODELAY;不要處理來自網(wǎng)絡(luò)的請求,將它們從隊列中取出并單獨(dú)處理,以便較長的延遲請求不會阻塞隊列。
通過以下方式隱藏延遲:并行化請求處理;將請求發(fā)送到多個服務(wù)器并采取最快的響應(yīng);使用輕量級線程。
調(diào)整系統(tǒng)以實(shí)現(xiàn)低延遲:配置 CPU 頻率為恒定(性能),將 CPU 隔離到特定線程,禁用交換,配置網(wǎng)絡(luò)堆棧中斷親和性。
為了降低延遲,事務(wù)性需要盡可能簡單。當(dāng)你必須進(jìn)行復(fù)雜的事務(wù)時,冪等性可以顯著降低恢復(fù)的復(fù)雜性。
針對低延遲應(yīng)用程序優(yōu)化 Java
低延遲應(yīng)用程序垃圾收集的關(guān)鍵問題是停止世界暫停。
選擇正確的垃圾收集器對于優(yōu)化應(yīng)用程序性能至關(guān)重要:串行 GC 適用于內(nèi)存占用低、CPU 要求低的小型應(yīng)用程序,但由于 GC 暫停時間較長,因此不適用于低延遲應(yīng)用程序;并行 GC(吞吐量收集器)通過使用多個線程進(jìn)行垃圾收集來最大化應(yīng)用程序吞吐量,但由于 GC 暫停時間較長,因此也不適用于低延遲應(yīng)用程序;CMS(并發(fā)標(biāo)記清除)旨在通過與應(yīng)用程序線程并發(fā)工作來最大限度地減少暫停時間,雖然暫停時間較短,但容易受到碎片的影響,最終導(dǎo)致長時間暫停;G1(垃圾優(yōu)先收集器)試圖在吞吐量和暫停時間之間提供良好的平衡,但并不能提供最短的暫停時間;ZGC(Z 垃圾收集器)和 Shenandoah 專為低延遲應(yīng)用程序設(shè)計,旨在實(shí)現(xiàn)少于 10 毫秒的暫停時間,即使在大型堆上也是如此。
垃圾收集調(diào)優(yōu)的最佳實(shí)踐:優(yōu)化堆大小,太小的堆會導(dǎo)致頻繁的 GC 循環(huán),太大的堆可能會導(dǎo)致更長的 GC 暫停;了解應(yīng)用程序的分配模式以調(diào)整不同 GC 代的大小,避免填充舊代對于避免大多數(shù) GC 算法的長 GC 至關(guān)重要;使用 GC 日志記錄和監(jiān)控來了解應(yīng)用程序中 GC 的行為,識別頻繁或長時間暫停等問題。
高效的編碼可提高執(zhí)行速度并減少垃圾收集開銷。技術(shù)包括:選擇正確的數(shù)據(jù)結(jié)構(gòu);在適當(dāng)?shù)那闆r下使用原始類型以避免使用裝箱類型;根據(jù)特定需求構(gòu)建自定義數(shù)據(jù)結(jié)構(gòu)。
避免內(nèi)存泄漏:確保流、連接和其他 I/O 對象等資源在使用后正確關(guān)閉;使用 try-with-resources;對可以重新創(chuàng)建的緩存和大型數(shù)據(jù)結(jié)構(gòu)使用弱引用;進(jìn)行分析以識別和修復(fù)內(nèi)存泄漏。
算法效率的微小變化可能會對性能產(chǎn)生重大影響。
最小化循環(huán)內(nèi)的工作、避免循環(huán)內(nèi)的方法調(diào)用和循環(huán)展開等簡單的技術(shù)可以提高性能。
對于性能熱點(diǎn),延遲計算或?qū)ο髣?chuàng)建直到絕對必要為止。
使用細(xì)粒度鎖或無鎖數(shù)據(jù)結(jié)構(gòu)。
定期進(jìn)行概要分析和性能測試。
正確設(shè)置初始(-Xms)和最大(-Xmx)堆大小以防止頻繁的垃圾收集,但請注意,過大的堆大小可能會導(dǎo)致更長的 GC 暫停。
為應(yīng)用程序選擇最佳的垃圾收集器,例如 -XX:+UseG1GC、-XX:+UseConcMarkSweepGC 或 -XX:+UseZGC 等。
啟用 GC 日志記錄。
高級 JVM 調(diào)優(yōu)技術(shù)包括:微調(diào)垃圾收集器;使用堆外內(nèi)存;調(diào)整 JVM 的代碼緩存(例如 -XX:InitialCodeCacheSize 和 -XX:ReservedCodeCacheSize);其他編譯器標(biāo)志,如 -XX:CompileThreshold。
調(diào)優(yōu)時:進(jìn)行單一更改并根據(jù)監(jiān)控結(jié)果逐步調(diào)整;在真實(shí)負(fù)載條件下進(jìn)行測試;定期檢查和更新 JVM 設(shè)置以適應(yīng)應(yīng)用程序更改和 JVM 更新。
設(shè)計應(yīng)用程序以有效利用多個核心和線程是低延遲系統(tǒng)的關(guān)鍵。確保線程高效安全地協(xié)同工作,在并發(fā)執(zhí)行以加快處理速度和管理并發(fā)帶來的開銷之間取得適當(dāng)?shù)钠胶狻?/p>
在網(wǎng)絡(luò)和 I/O 密集型應(yīng)用程序中,使用異步模式防止線程在等待數(shù)據(jù)時處于空閑狀態(tài)。
有效緩存:確定緩存什么、何時緩存以及緩存多長時間,以優(yōu)化應(yīng)用程序的數(shù)據(jù)訪問需求,同時確保緩存的數(shù)據(jù)保持相關(guān)性和最新性。
事件驅(qū)動架構(gòu)在對外部變化的實(shí)時響應(yīng)至關(guān)重要的應(yīng)用程序中特別有用。
編寫可擴(kuò)展 Java 應(yīng)用程序:最佳實(shí)踐和策略
可擴(kuò)展性是指在不影響性能的情況下處理增加的負(fù)載 - 以高效可靠的方式處理更多請求??蓴U(kuò)展性可以垂直實(shí)現(xiàn)(向單個節(jié)點(diǎn)添加更多資源)和水平實(shí)現(xiàn)(通過向系統(tǒng)添加更多節(jié)點(diǎn))。水平擴(kuò)展通常因其在可用性和容錯方面的優(yōu)勢而受到青睞。
一些有助于擴(kuò)展的架構(gòu)實(shí)踐是:將系統(tǒng)分解為提供特定服務(wù)子集的模塊化單元,以獨(dú)立擴(kuò)展這些服務(wù)(例如微服務(wù));松散耦合,例如與事件驅(qū)動的請求流;和容器化。
異步處理(例如支持背壓的反應(yīng)系統(tǒng))提高了擴(kuò)展能力。
高效的內(nèi)存管理對于擴(kuò)展組件至關(guān)重要:消除內(nèi)存泄漏,使用適當(dāng)?shù)臄?shù)據(jù)結(jié)構(gòu),并通過選擇和調(diào)整垃圾收集器來優(yōu)化代碼中的垃圾收集。
有效的并發(fā)管理對于可擴(kuò)展性至關(guān)重要:使用 java.util.concurrent 數(shù)據(jù)結(jié)構(gòu)和控制機(jī)制,高效地管理線程,并且優(yōu)先使用不可變對象。
定期分析和優(yōu)化有助于保持可擴(kuò)展性:識別和優(yōu)化代碼瓶頸。
調(diào)整 JVM:調(diào)整堆大小 - 更多的內(nèi)存可以減少垃圾收集的頻率,但太多會導(dǎo)致資源浪費(fèi)和長時間的 GC 暫停;選擇和調(diào)整垃圾收集器;。
有效緩存。根據(jù)數(shù)據(jù)和請求類型適當(dāng)?shù)剡x擇本地緩存和分布式緩存,并確保緩存失效策略提供有效的緩存命中率。
高效的數(shù)據(jù)庫交互通常涉及連接池和優(yōu)化查詢,以及針對應(yīng)用程序的更新和訪問模式優(yōu)化數(shù)據(jù)庫結(jié)構(gòu)。
有效的監(jiān)控對于理解和提高性能至關(guān)重要:確保系統(tǒng)具有應(yīng)用程序性能監(jiān)控、日志框架、日志分析工具、收集指標(biāo)并執(zhí)行健康檢查。
https://www.youtube.com/watch?v=ZKe4jGe1sL4
Kubernetes pod 中運(yùn)行的 Java 進(jìn)程的內(nèi)存設(shè)置
由于 JVM 僅遵守堆大小限制,而不遵守非堆內(nèi)存限制,因此無法保證 Java 進(jìn)程的完整內(nèi)存邊界。非堆內(nèi)存很難預(yù)測,因?yàn)樗Q于多種因素(代碼生成、線程數(shù)、GC 算法、加載的類數(shù)等)。
如果使用的本機(jī)內(nèi)存超出容器內(nèi)存限制,容器將被 OOM Killer 終止。由于 JVM 使用的內(nèi)存量是可變的,并且只有堆內(nèi)存受到限制,因此您需要確保 JVM 堆限制充分低于容器限制,以使 JVM 非堆內(nèi)存使用量不會使容器本機(jī)內(nèi)存使用量超出限制。
需要注意的是,-XX:MaxRAMPercentage 不會限制 Java 進(jìn)程可以使用的內(nèi)存總大小。它具體指的是 JVM 堆大小。
已提交內(nèi)存是 JVM 已分配的系統(tǒng)內(nèi)存的一部分。Xms 堆內(nèi)存在啟動時已提交,并根據(jù) JVM+應(yīng)用程序的需求增加到 Xmx。如果堆中不再需要已提交的內(nèi)存,某些垃圾收集器可以將其返回,但除非發(fā)生這種情況,否則堆使用量可能遠(yuǎn)低于已提交的內(nèi)存。
設(shè)置容器 JVM 內(nèi)存的建議步驟是:1. 從 MaxRAMPercentage 的 75% 開始;2. 如果最大堆使用率保持較高水平,則需要增加容器內(nèi)存,但如果堆使用率正常但進(jìn)程大小接近容器限制,則需要減少 MaxRAMPercentage(或提供更多容器內(nèi)存)。
https://medium.com/codex/running-jvm-applications-on-kubernetes-beyond-java-jar-a095949f3e34
在 Kubernetes 上運(yùn)行 JVM 應(yīng)用程序:超越 java -jar
JVM GC 選擇(如果未設(shè)置顯式 GC 算法標(biāo)志):如果 CPU 數(shù)量大于或等于 2,且內(nèi)存量大于 1792MB,則所選 GC 為 G1 GC(或 Java 9 之前的 ParallelGC)。如果這兩個條件中的任何一個低于上述值,則所選 GC 為 SerialGC。
當(dāng)未設(shè)置 -Xmx 時,JVM 將最大堆值配置為可用(容器)內(nèi)存的四分之一,除非當(dāng)最大堆值為 50% 時可用內(nèi)存低于 256MB,或者當(dāng)最大堆值約為 127MB 時可用內(nèi)存介于 256MB 和 512MB 之間。
要找出您的 JVM 正在使用哪種 GC 實(shí)現(xiàn),請執(zhí)行
java -XX:+PrintFlagsFinal -version | grep "Use" | grep "GC"(或在 Windows 上執(zhí)行 findstr 而不是 grep)。同樣,過濾“MaxHeapSize”將顯示 Xmx 值。確保 JVM 不限于僅有 1 個可用 CPU,從而避免在高并發(fā)應(yīng)用中使用 SerialGC。如有必要,請將 -XX:ActiveProcessorCount 標(biāo)志設(shè)置為大于 1 的值,但請注意,實(shí)際可用 CPU 與提供的值之間的差異可能會導(dǎo)致 JVM 效率低下。
指定所需的 GC 實(shí)現(xiàn)。一個簡單的建議是,對于 4GB 以下的堆使用 ParallelGC,對于 4GB 以上的堆使用 G1。選項包括 -XX:+UseSerialGC / -XX:+UseParallelGC / -XX:+UseG1GC / -XX:+UseShenandoahGC / -XX:+UseZGC。
最好使用 CPU 限制為 2000m 或更高的容器。通常,在具有 2000m CPU 限制的容器中安裝單個 JVM 比在具有 1000m CPU 限制的兩個容器中安裝兩個單獨(dú)的 JVM 更好。
最好使用 -Xmx 參數(shù)或 -XX:MaxRAMPercentage 明確配置 JVM 堆大小,而不是讓 JVM 選擇一個值。
JVM 內(nèi)存區(qū)域包括堆和非堆(本機(jī)內(nèi)存/堆外)。非堆中包括元空間、代碼緩存、堆棧內(nèi)存、GC 數(shù)據(jù)等。因此,您應(yīng)該將 JVM 的堆大小配置為可用內(nèi)存的 50% 到 75% 之間,將剩余值保留給非堆和操作系統(tǒng)。
使用負(fù)載測試來驗(yàn)證配置的堆大小是否適合您的應(yīng)用程序,檢查是否出現(xiàn) OOM(包括來自 OOM 殺手的)。
如果要避免 JVM 處理內(nèi)存分配和釋放任務(wù),請將 Xms 設(shè)置為等于 Xmx 的值。但這意味著 JVM 進(jìn)程大小永遠(yuǎn)不會最小化。
對于 Kubernetes 容器,請使用內(nèi)存請求相等限制。如果您的 JVM 將進(jìn)行非同步處理,則使用可突發(fā)容器(CPU 限制大于請求),讓 JVM 使用其他容器未同時使用的備用 CPU 資源。
基于 CPU 的水平 Pod 自動擴(kuò)縮可以適用于 JVM,因?yàn)樨?fù)載較高的 JVM 通常會生成更多對象,因此會使用更多 CPU 密集型的 GC。
實(shí)用性能分析
在衡量績效時,您需要在開始之前設(shè)定一個目標(biāo),以便知道要衡量什么以及目標(biāo)是什么。
穩(wěn)定狀態(tài)負(fù)載測試是在正常生產(chǎn)負(fù)載下進(jìn)行測試;極限負(fù)載測試是增加負(fù)載直到資源飽和,以找出應(yīng)用程序的極限并確定哪些資源限制了規(guī)模。它還會告訴您當(dāng)負(fù)載隨后減少時系統(tǒng)是否會恢復(fù),或者極限負(fù)載是否破壞了服務(wù)。
有用的操作系統(tǒng)級別變化:增加打開文件的數(shù)量,擴(kuò)展臨時端口范圍,將 CPU 調(diào)節(jié)器更改為性能(阻止 CPU 頻率動態(tài)改變)。
一個好的 JVM 配置是 JVM 21 或更高版本,具有生成 ZGC(-XX:+UseZGC -XX:+ZGenerational)和 -XX:+DebugNonSafepoints(當(dāng)然 -Xmx 足夠大,適合您的應(yīng)用程序,這樣它就不會因?yàn)槎烟《艿较拗疲?/p>
至少跟蹤這些資源:CPU、網(wǎng)絡(luò)、SSD、性能、故障、GC、線程、連接、響應(yīng)(時間、類型、錯誤)。
始終測量基線 - 在逐漸增加且系統(tǒng)沒有問題之后測量正常負(fù)載的測試 - 以便您可以將未來的異常測量結(jié)果與之進(jìn)行比較。
USE 方法:利用率、飽和度、錯誤。首先查找錯誤。然后是飽和度 - 每種資源的最大容量。最后是利用率。利用率是一段時間內(nèi)資源使用量的平均值,飽和度是確定資源在一段時間內(nèi)何時達(dá)到容量。當(dāng)利用率不是 100% 時,您可以擁有飽和資源,因?yàn)轱柡桶l(fā)生的時間段比利用率的平均計算時間更短。
如果負(fù)載測試發(fā)現(xiàn)存在不足,則應(yīng)分析導(dǎo)致不足的組件。使用沒有安全點(diǎn)偏差的分析器。
確保在負(fù)載測試中產(chǎn)生負(fù)載的客戶端本身沒有飽和,從而導(dǎo)致客戶端限制。
尋找 Java 的隱藏性能陷阱
要找到性能問題的原因,請按照以下步驟操作:1. 查看分布式跟蹤以了解請求延遲的位置;2. 查看影響步驟 1 中已識別組件的指標(biāo)并查找異常值;3. 對已識別的組件進(jìn)行分析(例如使用 JFR)。
分布式跟蹤讓您看到低效率,例如重復(fù)的相同查詢,不必要的額外跳數(shù),可以并行化的子請求。
性能問題的常見原因包括:線程池太小、GC 暫停、等待從池中獲取連接的時間太長、緩存命中率太低、錯誤太多、日志記錄效率低下(例如構(gòu)建從未使用過的字符串)。檢查您的指標(biāo)是否存在常見問題。
一旦您解決了性能問題,您就應(yīng)該公開一個可以識別類似情況的指標(biāo),并在再次發(fā)生時發(fā)出警報。
避免在鎖定的方法(例如事務(wù))中進(jìn)行網(wǎng)絡(luò)調(diào)用,因?yàn)榉椒ㄖ械馁Y源(例如 JDBC 連接)在調(diào)用時處于空閑狀態(tài)但被鎖定。
馴服野外性能問題:JVM 分析實(shí)用指南
性能調(diào)優(yōu)就是以更高效的方式利用資源。通過分析來識別低效之處是實(shí)現(xiàn)此目標(biāo)的好方法之一。
僅從考慮系統(tǒng)的角度來看,抽象的層次和級別太多而無法知道什么是低效的——您需要衡量資源使用情況并找出低效率。
任何受到安全點(diǎn)偏差影響的分析器所提供的信息量都會比沒有受到安全點(diǎn)偏差影響的分析器少——僅從安全點(diǎn)進(jìn)行分析會錯過潛在的重要信息并導(dǎo)致誤導(dǎo)性分析。
async-profiler 是更好的 Java 分析器之一,它沒有安全點(diǎn)偏差,可以配置為低影響,可以看到本機(jī)堆棧幀以及 Java 堆棧幀,具有集成的火焰圖,并且具有多種分析模式(包括 CPU、掛鐘、鎖、分配、緩存未命中等)。
如果線程過多,掛鐘分析將使用線程采樣 - 否則采樣將花費(fèi)太長時間,并且分析將無效。這意味著您可能會丟失重要數(shù)據(jù),尤其是在較短的分析周期內(nèi)。
使用正則表達(dá)式來調(diào)查火焰圖以查看哪些框架占用時間。
事件驅(qū)動框架有來自抽象層的開銷。
對錯誤的資源(即不會導(dǎo)致效率低下的資源)進(jìn)行分析會提供誤導(dǎo)性或無用的信息。您可能需要在不同級別進(jìn)行多次調(diào)查,以確定哪些資源需要進(jìn)行分析才能進(jìn)行有效調(diào)整。
確保測試時不會測量熱身效果,除非您特別想測量熱身效果。
高 CPU 使用率需要 CPU 分析,但低 CPU 使用率需要其他分析,例如掛鐘分析。
對于任何需求量很大的資源,緩存都是一個很好的解決方案,通過提供預(yù)先計算的數(shù)據(jù)可以減少需求。
日志記錄的開銷可能很高,并且誤用日志記錄框架或使用效率低下的現(xiàn)象很常見。
瓶頸可能會掩蓋其他瓶頸,因此請注意,提高瓶頸的效率可能不會使應(yīng)用程序更高效。您需要反復(fù)進(jìn)行調(diào)整,直到實(shí)現(xiàn)目標(biāo)。
使用錯誤的系統(tǒng)時鐘源會產(chǎn)生非常低效的計時代碼。深入到內(nèi)核調(diào)用的分析器將顯示此類低效。
最強(qiáng)大的優(yōu)化是刪除代碼和緩存。
TLAB 中的分配比 TLAB 外的分配效率高得多,因此使用顯示這些不同分配的分析器很有用。
通過垂直可擴(kuò)展性和有效測試最大化金融交易系統(tǒng)的性能和效率
分配少量內(nèi)存是高效的,但您嘗試分配的內(nèi)存越多、越大或同時分配的內(nèi)存越多,就越有可能達(dá)到 CPU 緩存共享限制并大大減慢分配時間(數(shù)量級!)。對于在高并發(fā)性下分配的小對象,分配開銷遠(yuǎn)大于 GC 開銷。
所有 JVM 和線程的最大總分配率為 10-15 GB/秒。降低分配率通??梢蕴岣咄掏铝?。
使用中央服務(wù)器提供時間戳的分布式唯一時間戳至少需要 100 微秒。UUID 需要 300 納秒,但不能直接映射到時間戳。通過采用納秒時間戳并用主機(jī) ID 替換最低 2 位數(shù)字,您可以在 40 納秒內(nèi)獲得一個沒有中央服務(wù)器的唯一時間戳(使用共享內(nèi)存使其在每個主機(jī)上都是唯一的)。
延遲的最大來源往往是持久性(DB、持久消息、HA、磁盤存儲等)。通過最小化持久性,您可以優(yōu)化延遲。另一臺機(jī)器上的冗余副本通常比本地磁盤上的副本快得多。
將 Java 推向極限:在 2 秒內(nèi)處理 10 億行數(shù)據(jù)
整數(shù)運(yùn)算比浮點(diǎn)運(yùn)算快得多。如果小數(shù)點(diǎn)后有固定數(shù)量的小數(shù)值,則可以使用整數(shù)來表示它,并在需要最終結(jié)果時只需除以數(shù)量級即可。
如果并行處理數(shù)據(jù),則每個核心加載一個塊來處理比加載一個大塊并并行處理更有效率,因?yàn)橐粋€大塊將在 CPU 緩存之間共享,效率要低得多。
通過內(nèi)存映射加載文件數(shù)據(jù)比使用文件讀取來處理文件要快得多。
一次從內(nèi)存(而非磁盤)讀取 8 個字節(jié)比逐字節(jié)讀取更有效,因?yàn)橛幸粋€ CPU 指令可用于此(參考 SWAR)。
如果除以 2 的冪,則移位比除法更快。
如果您只需要一系列任務(wù)末尾的字符串,則以長整型表示字符串會非常有效。
無分支編程(沒有 if 語句,沒有替代類實(shí)現(xiàn))非常適合 CPU 管道,因此效率極高。
使用前向探測的開放地址哈希映射比帶有鏈接節(jié)點(diǎn)的哈希映射更快,因?yàn)槟苊饬祟~外的節(jié)點(diǎn)創(chuàng)建和鏈接操作。
每個核心使用一個數(shù)據(jù)結(jié)構(gòu),然后在處理結(jié)束時合并數(shù)據(jù)結(jié)構(gòu)意味著 CPU 緩存使用單獨(dú)的數(shù)據(jù)并且不會爭用,從而速度更快。
避免創(chuàng)建對象非常有效,但編碼困難并且往往會降低可維護(hù)性。