記一次堆內外內存問題的排查和優化

b8d4f3037d601dd8aebdf7a0f5f95670.gif

為優化淘寶帶寬成本,我們在網關 SDK(Java)統一使用 ZSTD 替代 GZIP 壓縮以獲取更高的壓縮比,從而得到更小的響應包。具體實現采用官方推薦的?zstd-jni 庫。zstd-jni 會調用 zstd 的 c++ 庫。

7ef8ea30c42ea57cd5850b849e6ff943.png

背景

在性能壓測和優化過程中,遇到了以下三個問題:

  1. GC 次數不變,但耗時翻倍

  2. 進程內存泄漏,極限情況下會出現 OOM Killer 殺掉進程的情況

  3. Netty 堆外內存泄漏(在優化問題 1 時引入)

下面我會從這三個問題展開,分享排查、解決問題的思路和過程。

730950f46f26e5b1cc1e65c6a0d84216.png

GC 優化

???【GC 耗時翻倍問題】現象

在我們預期中,使用 ZSTD 壓縮,在大包場景下(20KB 以上),不僅能夠獲得比 GZIP 更高的壓縮比;同時壓縮性能也應有一定優化,具體優化程度取決于業務特征,但至少不會有性能劣化

但實際性能壓測發現相比于同級別的 Netty GZIP,ZSTD 壓縮下,GC 次數不變,但耗時幾乎翻倍,導致最終應用表現為幾乎無任何性能優化,甚至影響 RT(CMS 下)。

???【GC 耗時翻倍問題】分析

我們的 ZSTD 壓縮是通過 JNI 實現,流程是將堆內數據拷貝到堆外壓縮,再將拷回堆內。

使用 JNI 會在一定程度上影響 GC 的效率,這是我們已知的,但是耗時翻倍超出了我們的預期。因此我們嘗試分析壓縮的執行流程。

JDK 22 中通過在 G1 中實現 region pinning 來減少延遲,以在 JNI 執行期間無需禁用 GC,詳見 JEP?423:?Region?Pinning?for?G1(地址:https://openjdk.org/jeps/423)

相比 GZIP,ZSTD 在單次壓縮過程中,多了內存占用:

  1. 壓縮后數據占用的堆內內存

    a.ZSTD 壓縮原始數據和壓縮后數據分開保存,占用兩份內存。而 GZIP 會將壓縮后的數據寫回到壓縮前的 byte 數組,只占用一份內存。

    b.除此之外,尤其是在流式 ZSTD 場景下,多個響應復用同一個 OutputStream 以達到最優壓縮比,但 OutputStream 里的 buffer 會占用額外的堆內空間。

  2. 堆外壓縮需要的內存,保存 ZSTD 壓縮上下文(保存字典)

7fe61dfcde07102ccc3afe51365547f9.jpeg

據圖分析,可能存在兩個問題導致 GC 耗時變長:

  1. 不必要的堆內內存占用

  2. 不必要的堆內外數據拷貝

???【GC 耗時翻倍問題】解決

59253071319693a9eb781330c2aaf8b2.png

思路:為了能夠解決以上兩個問題,我們希望能夠將原始數據在堆外壓縮后直接寫出,一方面盡早釋放原始數據占用的堆內內存,另一方面減少不必要的堆內外拷貝。

實現:使用 zstd-jni 提供的堆外壓縮接口,直接原始數據拷貝到堆外進行壓縮,并通過 Netty 直接在堆外寫出(流程為上圖的 ZSTD Direct)。


???【Finalizer 問題】現象

但是,轉堆外壓縮后,再次進行壓測,發現 GC 并沒有如期下降,反而更加頻繁,堆內存使用更高。

于是 GC 后 dump 查看堆布局,分析 JVM 堆內存,發現整體堆使用大小 915M/4G,這很不正常,我們的測試應用沒有長壽命對象,預期 GC 后,堆大小應該只有幾十 M。

21f71677d9b7448232a2a89795ece90f.png

進一步查看堆內對象,發現有大量新增可疑對象:

  1. Finalizer

  2. ZstdJNIDirectByteBufCompressor(壓縮實例,JNI 調用入口。)

  3. DefaultInvocation(請求上下文,包含請求和響應的全部信息,為應用大對象。)

他們的引用關系為:

Finalizer -> ZstdJNIDirectByteBufCompressor <-> DefaultInvocation。

其中 ZstdJNIDirectByteBufCompressor 和 DefaultInvocation 有高達 1604 個,占用內存超 704M,占已使用堆內存的 77%,但之前并沒有這些對象。

09eb6f1476f52152c1dca492fcdfdcf6.png


???【Finalizer 問題】分析
  • 哪來這么多 Finalizer 對象,和 GC 耗時增長有什么關系?

要想知道 Finalizer 對象是什么,我們首先需要了解 JVM 的 finalize() 方法:

finalize() 方法定義在 Object 類中,對于實現了 finalize() 的對象,當垃圾回收器確定該對象沒有任何引用時,就會調用其 finalize()。

筆者建議大家盡量避免使用它,因為它并不能等同于 C 和 C++ 語言中的析構函數,而是 Java 剛誕生時為了使傳統 C、C++ 程序員更容易接受 Java 所做出的一項妥協。它的運行代價高昂,不確定性大,無法保證各個對象的調用順序,如今已被官方明確聲明為不推薦使用的語法。有些教材中描述它適合做“關閉外部資源”之類的清理性工作,這完全是對 finalize() 方法用途的一種自我安慰。finalize() 能做的所有工作,使用 try-finally 或者其他方式都可以做得更好、 更及時,所以筆者建議大家完全可以忘掉 Java 語言里面的這個方法。

--《深入理解 JVM》

多數同學對 finalize 方法的了解,可能都來自于以上這段話,知道其 “運行代價高昂”,“不推薦使用” ,那它到底會對我們應用產生什么影響?

  • JVM 是如何執行 finalize() 的?

  1. JVM 在加載類的時候,會去識別該類是否實現了 finalize() ;若是,則標記出該類為“ finalize class”。

  2. 在創建 “finalize Class” 對象時,會調用 Finalizer#register(),在該方法中創建一個 Finalizer 對象,Finalizer 對象會引用原始對象,然后將其注冊到名為 unfinalized 的全局隊列中(保證 Finalizer 對象及其引用的原始對象一直可達,以確保在被 GC 前,其 finalize() 能被執行)。

25dbd05609fe4b297440de98bb4a3fb2.png

8837d6ef74a11a98f788f5063a58840c.png

  1. 在一次 GC時,JVM 判斷原始對象除了 Finalizer 對象引用之外沒有其他對象引用之后,就把 Finalizer 對象從 “unfinalized” 隊列中取出,加入到 “Finalizer queue” 中。

  2. JVM 在啟動時,會啟動一個“finalize”線程,該線程會一直從“Finalizer queue”中取出對象,然后執行原始對象中的 finalize()。

feababe80a52976c8d0ec74d8fbe7ea2.png

287ab967231af351f907ab8a70a1f0f3.png

  1. 在完成步驟 4 后,Finalizer 對象以及其引用的原始對象,再無其他引用,屬于不可達對象,再次 GC 的時候他們將會被回收掉。(如果在 finalize() 使該對象重新可達,再次 GC 該對象不會被回收,即 finalize() 方法是對象逃脫死亡 (GC) 命運的最后一次機會)。

  • 使用 finalize() 帶來哪些影響?

  1. 創建一個實現 finalize() 的對象時,需要額外創建其 Finalizer 對象并且注冊到隊列中,因此需要額外的內存空間,且創建時間長于普通對象創建。

  2. 相比普通對象,實現 finalize() 的對象生存周期更長,至少需要兩次 GC 才可被回收。

  3. 在 GC 時需要對實現 finalize() 的對象做特殊處理(比如 Finalizer 對象的出隊入隊操作等), GC 耗時更長。

  4. 因為 finalize 線程優先級比較低,若 CPU 繁忙,可能會導致 “ Finalizer queue” 有積壓,在經歷多次 YGC 之后原始對象及其 Finalizer 對象就會進入 old 區域,那么這些對象只能等待 FGC 才能被 GC。

總的來說,使用 finalize() 方法本身會加重系統負擔、嚴重影響 GC 并且無法保證 finalize 的調用時機,其應用場景也僅僅是防止資源泄漏,finalize() 能做的所有工作,使用 try-finally 或者其他方式都可以做得更好、 更及時,所以我們還是忘記它的存在吧。

???【Finalizer 問題】解決

最佳實踐:

盡可能避免使用 finalize 機制。若實在無法避免,也應盡量避免其引用大對象。

JDK 18 中已經棄用 finalize 機制以在未來版本中刪除。詳見:Deprecate?Finalization?for?Removal(地址:https://openjdk.org/jeps/421)

在我們的 ZSTD 場景下,由于 zstd-jni 將 finalize() 作為堆外資源的兜底清理手段,因此我們斷開其對應用大對象的引用后,耗時翻倍的問題被成功解決。

我們的測試應用單機極限 QPS 較低(300),Finalizer 只要不引用大對象,對 GC 的影響不大;但在更高 QPS 場景下,Finalizer 對 GC 的影響會更加凸顯。

我們在另一線上應用使用 ZSTD 壓縮,在單機 QPS 1000 時,比起使用 NoFinalizer 的 Zstd Compressor,使用 Finalizer 的 Zstd Compressor GC 耗時漲了近 10 倍。

因此,我們最終決定直接使用 NoFinalizer 的 Zstd Compressor。

5b7e9c5a7aae1278cd977ab06970a0f4.png

Netty ByteBuf 內存泄漏

???現象

為了優化 GC,我們通過 Netty 的 DirectByteBuf 操作堆外內存,直接在堆外壓縮并響應。

但在性能壓測時,通過 Netty 的內存泄漏檢測工具,發現在極限情況下會產生內存泄漏,經過觀察,會伴隨著以下幾種現象:

  1. 施壓 QPS 達到單機極限,持續有 FGC 產生;

  2. 客戶端超時主動斷連,繼續往被關閉的 channel 里寫入內容失敗,會出現連接已關閉的報錯;

  3. Netty 堆外內存滿;

???分析

Step 1 泄漏堆棧顯示泄漏對象為響應內容的 DirectByteBuf

Step 2 通過增加埋點追溯業務代碼中可能的泄漏點,發現在寫給 netty ChannelOutboundHandler pipeline 之前,是沒有泄漏的。

Step 3 排查聚焦在 netty 的 ChannelOutboundHandler pipeline,排查我們自己實現的 ChannelOutboundHandler 內部也并未有泄漏。

Step 4 進一步分析 netty 內存泄漏檢測的堆棧,發現泄漏內存的最后訪問點有 netty 框架內部代碼,所以猜測泄漏可能是框架執行過程中產生。

Step 5 進一步分析 netty 寫出響應的代碼。

  1. 我們調用 netty 的 AbstractChannel#writeAndFlush(java.lang.Object) 寫出內容,會從 pipeline 的最后一個節點執行,最終進入到 next.invokeWriteAndFlush(m, promise)。

8c30f624cc33969e088d452c8fffdda9.png

  1. invokeHandler() 會檢查 handler 的狀態(如下圖),確認其是否可被執行。若 handler 被認為不可執行,則會直接嘗試執行下一個 handler (如 1 中圖)。

202f1e2d7ad1e2442c01031c80018019.png

  1. 嘗試追溯 handlerState 的更新。發現當 channel 被 deregister 后(連接關閉), pipeline 所有中間 handler 的狀態都會被置為 REMOVE_COMPLETE,即不可執行,這樣后續再寫入的消息都不會再進入到這些 handler 里了。(泄漏就是從這里開始)

setRemoved:911, AbstractChannelHandlerContext (io.netty.channel)
callHandlerRemoved:950, AbstractChannelHandlerContext (io.netty.channel)
callHandlerRemoved0:637, DefaultChannelPipeline (io.netty.channel)
destroyDown:876, DefaultChannelPipeline (io.netty.channel)
destroyUp:844, DefaultChannelPipeline (io.netty.channel)
destroy:836, DefaultChannelPipeline (io.netty.channel)
access$700:46, DefaultChannelPipeline (io.netty.channel)
channelUnregistered:1392, DefaultChannelPipeline$HeadContext (io.netty.channel)
invokeChannelUnregistered:198, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelUnregistered:184, AbstractChannelHandlerContext (io.netty.channel)
fireChannelUnregistered:821, DefaultChannelPipeline (io.netty.channel)
run:839, AbstractChannel$AbstractUnsafe$8 (io.netty.channel)
safeExecute$$$capture:164, AbstractEventExecutor (io.netty.util.concurrent)
safeExecute:-1, AbstractEventExecutor (io.netty.util.concurrent)- Async stack trace
addTask:-1, SingleThreadEventExecutor (io.netty.util.concurrent)
execute:825, SingleThreadEventExecutor (io.netty.util.concurrent)
execute:815, SingleThreadEventExecutor (io.netty.util.concurrent)
invokeLater:1042, AbstractChannel$AbstractUnsafe (io.netty.channel)
deregister:822, AbstractChannel$AbstractUnsafe (io.netty.channel)
fireChannelInactiveAndDeregister:782, AbstractChannel$AbstractUnsafe (io.netty.channel)
close:765, AbstractChannel$AbstractUnsafe (io.netty.channel)
close:620, AbstractChannel$AbstractUnsafe (io.netty.channel)
close:1352, DefaultChannelPipeline$HeadContext (io.netty.channel)
invokeClose:622, AbstractChannelHandlerContext (io.netty.channel)
close:606, AbstractChannelHandlerContext (io.netty.channel)
close:472, AbstractChannelHandlerContext (io.netty.channel)
close:957, DefaultChannelPipeline (io.netty.channel)
close:244, AbstractChannel (io.netty.channel)
close:92, DefaultHttpStream (com.alibaba.xxx.xxx.xxx.inbound.http)
onRequestReceived:111, DefaultHttpStreamTest$getHttpServerRequestListener$1 (com.alibaba.xxx.xxx.xxx.inbound.http)
onHttpRequestReceived:53, HttpServerStreamHandler (com.alibaba.xxx.xxx.xxx.inbound.http.server)
channelRead:44, HttpServerStreamHandler (com.alibaba.xxx.xxx.xxx.inbound.http.server)
invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)
channelRead:286, IdleStateHandler (io.netty.handler.timeout)
invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)
channelRead:103, MessageToMessageDecoder (io.netty.handler.codec)
invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)
channelRead:103, MessageToMessageDecoder (io.netty.handler.codec)
channelRead:111, MessageToMessageCodec (io.netty.handler.codec)
invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)
channelRead:103, MessageToMessageDecoder (io.netty.handler.codec)
invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:324, ByteToMessageDecoder (io.netty.handler.codec)
channelRead:296, ByteToMessageDecoder (io.netty.handler.codec)
invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)
channelRead:1410, DefaultChannelPipeline$HeadContext (io.netty.channel)
invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:919, DefaultChannelPipeline (io.netty.channel)
read:166, AbstractNioByteChannel$NioByteUnsafe (io.netty.channel.nio)
processSelectedKey:719, NioEventLoop (io.netty.channel.nio)
processSelectedKeysOptimized:655, NioEventLoop (io.netty.channel.nio)
processSelectedKeys:581, NioEventLoop (io.netty.channel.nio)
run:493, NioEventLoop (io.netty.channel.nio)
run:986, SingleThreadEventExecutor$4 (io.netty.util.concurrent)
run:74, ThreadExecutorMap$2 (io.netty.util.internal)
run:748, Thread (java.lang)
  1. 可以看到 pipeline 中間 handler 被跳過了,其中也包括我們自己實現的 handler。分析下圖代碼,我們寫給 netty pipeline 的 msg 實際是我們自己包裝的 HttpObject,是在我們自己實現的 handler 里才轉成 netty 的 ReferenceCounted 對象的,由于 handler 被跳過導致該對象并沒有被轉換成 ReferenceCounted,所以即使 netty 有兜底的 release ,實際并沒有產生作用,HttpObject 內部的 ByteBuf 并未真正被釋放,此時產生泄漏。

717dcdf488bc70082c09a8c09d73c390.png


???解決
  1. 【最佳實踐】在寫入 channel 之前,一定要先判斷 channel 是否 active 。

  2. 【最佳實踐】我們寫給 netty 的內容,最好是實現了 ReferenceCounted 接口的對象,這樣即使 netty 內部出現不預期情況,我們也可以利用 netty 的兜底 release 來釋放資源。

  3. 控制 ByteBuf 的使用范圍。在我們的場景里,可以將壓縮的實現下移到 netty 層,但上述 1、2 也同樣必須改進才能確保不出問題。

  4. 好處:對 ByteBuf 的操作可以收口在傳輸層,應用層編程難度大大降低。

  5. 壞處:考慮到可能存在多個 傳輸層 (http server) 的實現,壓縮邏輯可能需要根據堆內堆外做兩份實現,每個 http server 都需要對接。

4582ee20ce9cc0cfb3cf965d6765218c.png

堆外內存


???現象

ef481d4288bd6dd8a26781a181e8c4d2.png

開啟 zstd 壓縮時:

  1. QPS 增加會導致操作系統內存持續增加,直到 OOM Killer。

  2. 在 QPS 調零數十小時后,內存也幾乎不會降低。

因此懷疑存在堆外內存泄漏。

???分析

1d8da8e9d135338a567d35a8a484b04e.png

首先,整個應用進程用到的堆外內存分兩塊:

  1. JVM 堆外內存:在我們的測試應用里,JVM 的堆外內存最大值均為 1g,主要是 netty 使用,即 25%。

  2. zstd 庫使用的原生內存:

    壓縮流程使用原生內存的過程可以簡單描述為:創建 zstd ctx 準備壓縮 -> 調用 malloc 分配操作系統內存 -> 執行壓縮 -> 調用 free 釋放內存 -> 釋放 zstd ctx。

首先分析源碼:

  • Java 代碼:在我們的應用里 zstd ctx 的生命周期為請求級別,且我們通過 Java 埋點確認了請求結束后一定會正確釋放。

  • zstd c++ 代碼:zstd 沒有額外的內存管理,直接使用 stdlib 的 malloc 和 free 操作內存。在 zstd ctx 創建的時候分配對應的內存塊,銷毀實例時釋放對應的內存塊。

理論上不會存在 zstd 相關的內存泄漏

其次,通過對比實驗分析:

  • 在未開啟 zstd 壓縮時,不會出現堆外內存疑似泄漏的問題。而開啟 zstd 壓縮時,內存會漲到 95%+,遠超過 JVM 占用的最大內存。

因此,基本排除 1 的泄漏可能。

接著,分析進程實際內存使用:

  1. 使用 jemalloc 對壓測到 95% 內存的進程進行內存分析,發現堆外內存主要由 zstd 庫持有(其實這個 case 進程內存最終降下來了,但當時未查明原因。)

  2. 使用 jemalloc 內存泄漏檢測工具,未檢測到 zstd 庫代碼的內存泄漏。

因此,我們認為 zstd 庫對內存的操作大概率沒有泄漏。

直到最后,我們嘗試升級 JDK 版本,重新壓測發現 QPS 調零后,內存能夠降下來了。而 JDK 版本升級前后的區別在于使用的內存分配器不同:glibc 默認的 ptmalloc vs jemalloc。因此我們懷疑內存泄漏和內存分配器有關。

  • 內存分配器是什么?

內存分配器是用來為應用分配和管理操作系統內存的,分配器從操作系統獲取內存再自行管理,具體分配、管理、回收策略取決于內存分配器的具體實現。

我們通常使用的內存分配器,即 malloc/free 函數,由 C 標準庫 (libc) 提供的,也被稱為動態內存分配器,不同的內存分配器對函數有不同的實現。

80ea7547ede657535df729187746b0a7.jpeg

  • 內存分配器的核心思想?

內存分配器的核心是 平衡內存分配的性能和內存使用的效率。前者保證響應快、時間短,后者保證有足夠內存可用、不浪費。

  • 內存分配器百家爭鳴,但是核心思想都是相似的,只是差在具體的算法和元數據的存儲上。內存分配器的核心思想概括起來三條:

  • 內存分配及管理:將內存分為多種固定大小的內存塊(Chunk),通過內存塊管理和元信息存儲策略,使對每個 size class 或大內存區域的訪問的性能最優。

  • 內存回收及預測:當用戶釋放內存時,要能夠合并小內存為大內存,根據一些條件,該保留的就保留起來,在下次使用時可以快速的響應。不需要保留時,則釋放回系統,避免長期占用。

  • 多線程內存分配:比如通過線程獨占內存區間(TLS Thread Local Storage)以降低鎖競爭對性能的影響。

  • 幾種常見的應用層內存分配器對比

內存分配及管理

內存回收及預測

鏈表維護空閑內存塊,每次分配時從鏈首遍歷嘗試尋找大小合適(但不相等,因此容易產生內存碎片)的空閑內存塊,若無,則嘗試繼續向 OS 申請新內存塊(內存擴張,64 位系統下每次申請 64M,Linux 64M 內存塊問題就來源于此

  1. 維護空閑鏈表:合并后的空閑塊會被插入到相應大小類別的空閑鏈表中。

  2. 合并空閑塊:free()被調用時,釋放的內存塊會嘗試與相鄰的空閑塊合并,形成更大的空閑塊。

  3. 延遲歸還:只有堆頂的空閑塊足夠大且達到歸還條件才會收縮內存。

內存管理分為線程 Cache 和中央堆兩部分。

為每個線程分配一份線程 Cache,小內存分配從線程 Cache 獲取,大內存從中央堆分配。

在需要時從中央堆獲取內存補充線程 Cache。

  1. 定期 回收 ThreadCache 內存到 CentralCache 里,解決 PTMalloc 內存不能跨分配區遷移的問題。

  2. 定期從中央堆的內存歸還給操作系統。

使用多級大小來優化小塊內存的分配;

  • (small < 54KB、large < 4MB 、huge)

使用分配區(arena)來維護內存,每個分配區都維護了一系列分頁,來提供 small 和 large 的內存分配請求;

每個線程有線程 Cache,且固定選擇一個分配區,small 和 large 對象優先從 tcache 分配,其次從線程固定的 arena 分配。

從一個分配區分配出去的內存塊,在釋放的時候一定會回到該分配區。

有兩個層面的回收:

  1. 定期將線程緩存的空閑內存回收到 arena 中;

  2. 定期將 arena 中的空閑內存歸還給操作系統;

總的來說,不同的內存分配器有不同的策略,需要根據場景選擇:

  • PTMalloc:是 glibc 默認的內存分配器;存在內存浪費、內存碎片、以及加鎖導致的性能問題。

  • TCMalloc:適合線程的數量、創建,銷毀等是動態的場景;在一些內存需求較大的服務(如推薦系統),小內存上限過低,當請求量上來,鎖沖突嚴重,CPU 使用率將指數暴增。

  • JEMalloc:適合線程的數量、創建、銷毀等是靜態的(比如線程池)的場景。當 JEMalloc 為了容納更多的線程時,它會去申請新的 Cache,這會導致出現瞬間的性能劇烈抖動。

???解決

由于測試應用使用的 JDK 版本較低,底層 malloc 實現為 glibc 默認的 ptmalloc,存在內存碎片問題,底層使用 jemalloc 即可解決內存碎片問題。

c5cf1079e0d58b75e00a3a789d773f29.png

總結與感想

  1. 時刻關注代碼對應用性能的影響。比如一些容易被忽略的點,堆內外拷貝操作、長時間引用大對象等。

  2. 最好不要用 Finalizer,避免降低 GC 回收效率。

  3. 堆外內存使用得當,一定程度上能夠優化性能,但要注意由此引發的泄漏風險。

  • 盡量控制堆外內存的使用范圍,降低業務層編碼難度。

  • 在使用堆外內存時可以需要通過顯式約定來盡量降低內存泄漏的風險,比如在代碼中明確說明 ByteBuf 的使用原則:

    a.誰消費誰釋放,如果 A 組件將 ByteBuf 傳遞給 B 組件,則通常交由 B 組件決定是否釋放。

b.如果不希望使用者釋放,在傳給使用者之前,調用一次 .retain() 方法。

  • 寫給 netty 的內容,最好是實現了 ReferenceCounted 接口的對象,這樣即使 netty 內部出現不預期情況,我們也可以利用 netty 的兜底 release 來釋放資源。

¤?拓展閱讀?¤

3DXR技術?|?終端技術?|?音視頻技術

服務端技術?|?技術質量?|?數據算法

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/210725.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/210725.shtml
英文地址,請注明出處:http://en.pswp.cn/news/210725.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

React和Preact 這樣處理className更優雅

React和Preact寫className&#xff0c;我不太習慣使用模板字符串&#xff0c;不好看&#xff0c;看起來也不直觀&#xff0c;寫了如下兩個庫&#xff1a; react-runtime-clsx 和 preact-runtime-clsx&#xff0c;來輔助開發&#xff0c;可以更方便的處理className的問題&#x…

nodejs微信小程序+python+PHP北京地鐵票務APP-計算機畢業設計推薦 -安卓

目 錄 摘 要 I ABSTRACT II 目 錄 II 第1章 緒論 1 1.1背景及意義 1 1.2 國內外研究概況 1 1.3 研究的內容 1 第2章 相關技術 3 2.1 nodejs簡介 4 2.2 express框架介紹 6 2.4 MySQL數據庫 4 第3章 系統分析 5 3.1 需求分析 5 3.2 系統可行性分析 5 3.2.1技術可行性&#xff1a;…

STM32F103

提示&#xff1a;來源正點原子&#xff0c;參考STM32F103 戰艦開發指南V1.3PDF資料 文章目錄 前言一、pandas是什么&#xff1f;二、使用步驟 1.引入庫2.讀入數據總結 前言 提示&#xff1a;這里可以添加本文要記錄的大概內容&#xff1a; 開發環境硬件普中科技&#xff0c;接…

C++相關閑碎記錄(6)

1、使用shared_ptr #include <iostream> #include <memory> #include <set> #include <deque> #include <algorithm> #include <string>class Item { private:std::string name;float price; public:Item(const std::string& n, floa…

【Centos】

一、Virtualbox安裝Centos 1、Virtualbox 下載地址: Virtualbox 2、Centos 下載地址: Centos 3、Virtualbox安裝Centos教程 Virtualbox安裝Centos教程: Virtualbox安裝Centos教程

光學遙感顯著目標檢測初探筆記總結

目錄 觀看地址介紹什么是顯著性目標檢測根據不同的輸入會有不同的變體(顯著性目標檢測家族)目前這個領域的挑戰 技術方案論文1(2019)論文2(2021)論文3(2022) 未來展望 觀看地址 b站鏈接 介紹 什么是顯著性目標檢測 一張圖片里最吸引注意力的部分就是顯著性物體&#xff0c;…

vue3 vue-router 導航守衛 (五)

在Vue 3中&#xff0c;導航守衛仍然是一個重要的概念&#xff0c;用于在路由切換時執行一些特定的邏輯。Vue Router提供了多個導航守衛&#xff0c;包括全局守衛、路由獨享守衛和組件內守衛。可以在路由切換時執行一些特定的邏輯&#xff0c;例如身份驗證、權限控制、數據加載等…

C++STL詳解+代碼分析+典例講解

vector 的介紹&#xff1a; 1、vector是表示可變大小數組的序列容器。 2、vector就像數組一樣&#xff0c;也采用的連續空間來存儲元素&#xff0c;這也意味著可以采用下標對vector的元素進行訪問。 3、vector與普通數組不同的是&#xff0c;vector的大小是可以動態改變的。 4、…

基于物聯網的智能倉管理系統方案

基于物聯網的智能倉管理系統方案 一、項目背景 隨著企業業務的快速發展&#xff0c;傳統的人工倉庫管理方式已經無法滿足現代企業的需求。倉庫運營效率低下、貨物出入庫錯誤、庫存不準確等問題不斷涌現。因此&#xff0c;我們提出一個基于物聯網技術的智能倉管理系統方案&…

Redis 五大經典業務問題

一 緩存穿透 緩存穿透是指當請求的數據既不在緩存中也不存在于數據庫中時&#xff0c;請求會直接穿透緩存層&#xff0c;到達數據庫層。這通常是由于惡意攻擊或者程序錯誤造成的&#xff0c;比如攻擊者故意請求不存在的大量數據&#xff0c;導致緩存不命中&#xff0c;所有的請…

智能優化算法應用:基于被囊群算法3D無線傳感器網絡(WSN)覆蓋優化 - 附代碼

智能優化算法應用&#xff1a;基于被囊群算法3D無線傳感器網絡(WSN)覆蓋優化 - 附代碼 文章目錄 智能優化算法應用&#xff1a;基于被囊群算法3D無線傳感器網絡(WSN)覆蓋優化 - 附代碼1.無線傳感網絡節點模型2.覆蓋數學模型及分析3.被囊群算法4.實驗參數設定5.算法結果6.參考文…

Moco框架的搭建使用

一、前言   之前一直聽mock&#xff0c;也大致了解mock的作用&#xff0c;但沒有具體去了解過如何用工具或框架實現mock&#xff0c;以及也沒有考慮過落實mock&#xff0c;因為在實際的工作中&#xff0c;很少會考慮用mock。最近在學java&#xff0c;剛好了解到moco框架是用于…

城市基礎設施智慧路燈改造的特點

智慧城市建設穩步有序推進。作為智慧城市的基礎設施&#xff0c;智能照明是智慧城市的重要組成部分&#xff0c;而叁仟智慧路燈是智慧城市理念下的新產品。隨著物聯網和智能控制技術的飛速發展&#xff0c;路燈被賦予了新的任務和角色。除了使道路照明智能化和節能化外&#xf…

安全掃描五項簡介

目錄 安掃五項 1.代碼檢測 2.主機基線 nginx合規檢查 麒麟基線 3.WEB掃描 4.滲透測試 用戶枚舉漏洞 漏洞描述 修復建議 點擊劫持漏洞 漏洞描述 修復建議 XSS漏洞 漏洞描述 修復建議 3.主機漏洞 超高危漏洞 高危漏洞 中危漏洞 低危漏洞 信息漏洞 參考信息…

用電商API接口獲取拼多多的商品詳情數據

pinduoduo.item_get_app_pro-根據ID取商品詳情原數據 公共參數 API請求地址 名稱類型必須描述keyString是調用key&#xff08;必須以GET方式拼接在URL中&#xff09;secretString是調用密鑰api_nameString是API接口名稱&#xff08;包括在請求地址中&#xff09;[item_searc…

一步一步寫線程之一簡單的開始

一、多線程 多線程不管是在前面的文章分析中還是在網上還是大書籍上&#xff0c;學習C/C多線程編程是無法繞過的&#xff0c;即使使用別人封裝好的框架&#xff0c;包括使用STL的一些庫&#xff0c;如果僅僅是簡單的應用&#xff0c;可能也就過去了。不過&#xff0c;稍微復雜…

VBA_MF系列技術資料1-237

MF系列VBA技術資料 為了讓廣大學員在VBA編程中有切實可行的思路及有效的提高自己的編程技巧&#xff0c;我參考大量的資料&#xff0c;并結合自己的經驗總結了這份MF系列VBA技術綜合資料&#xff0c;而且開放源碼&#xff08;MF04除外&#xff09;&#xff0c;其中MF01-04屬于定…

[Linux] 用LNMP網站框架搭建論壇

一、nginx在其中工作原理 原理&#xff1a; php-fpm.conf是控制php-fpm守護進程 它是php.ini是一個php解析器 工作過程&#xff1a; 1.當客戶端通過域名請求訪問時&#xff0c;Nginx會找到對應的虛擬主機 2. Nginx將確定請求。 對于靜態請求&#xff0c;Nginx會自行處理…

結構體和位段

結構體&#xff1a; C語言中&#xff0c;我們之前使用的都是C語言中內置的類型&#xff0c;比如整形&#xff08;int&#xff09;、字符型&#xff08;char&#xff09;、單精度浮點型&#xff08;float&#xff09;等。但是我們知道&#xff0c;我們現實世界中&#xff0c;還…

聊聊spring.mvc.servlet.load-on-startup

序 本文主要研究一下spring.mvc.servlet.load-on-startup spring.mvc.servlet.load-on-startup org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java ConfigurationProperties(prefix "spring.mvc") public class WebMvcProperties {//.…