調(diào)優(yōu)經(jīng)驗(yàn)總結(jié)

剖析器

擺脫性能問題和內(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ā)

https://medium.com/javarevisited/top-performance-issues-every-developer-architect-must-know-part-2-concurrency-a15bd0b2b3b6

  • 運(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é)果將是不確定的。

  • Javasynchronized和鎖(例如 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)用程序中特別有用。

https://medium.com/@AlexanderObregon/writing-scalable-java-applications-best-practices-and-strategies-718a338e41c0

編寫可擴(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ù)性。

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

相關(guān)閱讀更多精彩內(nèi)容

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