當前位置:首頁 » 操作系統 » netty架構源碼

netty架構源碼

發布時間: 2022-11-30 12:25:12

1. Netty WebSocket 拆包淺析

最近項目中運用 WebSocket 的場景越來越多,自然而然。踩到的坑也就越來越多,例如最經常遇到的拆包,粘包問題。而當發現這個問題的時候,起初我認為與我之前通過 mina  實現自定義協議頭,解決 socket 進行數據傳輸時遇到拆包問題的場景是一樣的。而當我慢慢深入了解 WebSocket的時候才發現,實際上,WebSocket 的出現,就是為了解決拆包,粘包的問題的。因為這些事情,本身就是應該由運用層去解決,而不是在TCP/IP層解決。

通過網路搜索,我們可以知道 WebSocket 協議是根據 RFC6455 規范實現的,而最新版本是13(距離現今也接近10年了)。

以下是 WebScoket 定義的協議幀片段。

通過以上的幀定義片段。我也就明白了,為什麼說 WebSocket 是為了解決拆包,粘包問題等一系列應用層問題而誕生的。從 RCF6455 規范定義第 5.3 章節中可以發現,每次發送或者接收的幀報文中,都會有定義協議的一些信息,例如頭部大小,數據大小,以及幀類型,標識位等信息。

看完協議定義,那麼開始查看  Netty  源碼

以下是  Netty  定義的相關幀類型以供開發者使用。

當服務端接收到協議由 HTTP 握手升級協議 WebSocket 時,我們需要通過  類新建 WebSocketHandshaker ,此時會判斷 WebSocket 協議版本以及相關信息進行校驗。

而  WebSocketServerHandshaker 中,分別定義了  WebSocketFrameDecoder , WebSocketFrameEncoder 進行解碼,編碼調用。

那麼從現實開始,大概也就清楚了, Netty  中  WebSocket  和  Socket  實現,實際上都是一樣的,都是一個解碼器(負責接收數據,處理成需要的類型,例如文本,二進制),一個編碼器(負責根據協議版本,進行幀封裝)的結構。

從  WebSocket13FrameDecoder  我們可以得知,實際上  WebSocket13FrameDecoder 是繼承自  WebSocket08FrameDecoder (由於實現細節一致),而  WebSocket08FrameDecoder 是繼承自  ByteToMessageDecoder  ,實際上都是通過二進制數據進行解碼處理的。

查看  WebSocket08FrameDecoder  我們可以了解到具體的實現細節,就是每次解碼的時候,都會讀取首部信息,然後依次對數據進行處理。而拆包等操作,都是已經進行的相應處理和封裝。

看完 Netty 的源碼實現,那麼就可以進行實際的編碼解決問題了。

當服務端/客戶端發現包文過大時,會進行拆包。而為每個包定義一系列的定義。

例如:當接收一個 Text 消息時, Netty 首先會實例化一個 TextWebSocketFrame 對象並傳遞給調用方,而通過  isFinalFragment 我們可以判斷出,這個幀對象是否已經傳輸完畢,如果傳輸完畢,那麼進行業務處理。如果沒有傳輸完畢,那麼繼續等待餘下信息,進行拼接處理。

以上為服務端接收拆包信息的處理方式,反之亦然,客戶端接收消息也可以進行相應操作。而根據 WebSocket 1.3 版本實現的組件,也都根據 RFC6455  規范進行相應實現,可以實現無縫對接。

參考: Netty做webSocket客戶端,服務端拆包發送客戶端接收處理

2. netty里的ByteBuf擴容源碼分析

  我們知道在實例化一個ByteBuf對象的時候,是可以設置一個capacity和一個maxCapacity,當writerIndex達到capacity的時候,再往裡面寫入內容,ByteBuf就會進行擴容。
  下面我們來看一些ByteBuf是怎樣進行擴容的:
  我們先看一下調用ByteBuf的writeByte(int value)寫入一個位元組的數據。因為ByteBuf的writeByte(int value)是一個抽象方法,它的具體實現是在AbstractByteBuf裡面

  我們能看到會先調用ensureWritable0()方法來檢查是否能往裡面寫入數據,傳入1

  ensureAccessible()方法是檢查refCnt是否為0,為0代表該ByteBuf對象以被釋放,會拋。
  接下來判斷minWritableBytes是否小於或等於writableBytes,如果滿足代表還有空間滿足寫入數據,則直接返回。如果不滿足,則判斷是否檢查邊界,checkBounds是個boolean值,可在啟動JVM的時候由io.netty.buffer.checkBounds參數指定,其默認值是true。如果checkBounds為true,判斷將要寫入的位元組數是否大於最大可寫入的位元組數(maxCapacity - writerIndex),如果大於直接拋異常,否則繼續執行。
  接下來會調用ByteBufAllocator的calculateNewCapacity計算新的capacity。方法的實現在AbstractByteBufAllocator裡面:

  我們能看到擴容是有一個閥值(CALCULATE_THRESHOLD)的,為4MB大小,當所需容量大小(minNewCapacity)小於閥值(threshold)的時候,新的容量(newCapacity)都是是以64位基數向坐移位位計算出來的,通過循環,每次移動移1位,直到newCapacity>=minNewCapacity為止,如果計算出來newCapacity大於maxCapacity,則返回maxCapacity,否則返回newCapacity。也就是說當minNewCapacity=300的時候,newCapacity=512。
  當minNewCapacity>=threshold的時候,則先計算minNewCapacity / threshold * threshold的大小,如果這個值在加上一個threshold(4MB)大於newCapacity的時候,則newCapacity的值取maxCapacity,否則newCapacity=minNewCapacity / threshold * threshold+threshold。
  再回到ensureWritable0()方法,我們能看到,拿到計算出來的newCapacity,原後調用capacity(newCapacity)方法將新的capacity設置進去。

3. 如何實現Netty框架中伺服器端的消息推送

netty框架是用在伺服器端,客戶端是嵌入式編程,通過自定義的tcp通信協議進行連接的,現在需求是這樣的,伺服器端只是用來和客戶端進行通信,現在有第三方如微信端進行支付成功後在資料庫里生成了一條數據,表示要往某個客戶端發送指令,以下兩種方式可供參考:
1、微信端生成通訊指令後調用TCP端的介面(負責通訊程序和資料庫交互的),在介面程序中通過定義Socket連到通訊程序伺服器端,根據通道編號去發送,但是這種會導致伺服器端的tcp客戶端連接變得更多。

2、直接在netty框架中定義了scheleAtF。
當然也可藉助第三方工具來完成推送。例如極光推送,極光推送具有以下功能:
1、多種消息類型
開發者可以輕松地通過極光發送各個移動平台的系統通知,還可以在控制台編輯多種富文本展示模板; 極光還提供自定義消息的透傳,客戶端接到消息內容後根據自己的邏輯自由處理。
2、用戶和推送統計
完整的消息生命周期查詢,並且可以形成「推送報表」與「用戶統計報表」呈現給開發者,用來觀察推送的效果和應用發展趨勢。
3、簡訊補充
通過極光後台推送APP通知消息,對於一些重要又不能遺漏的信息可以調用極光簡訊的後台對未收到的客戶端發送簡訊通知,保證消息的可靠性。
4、A/B 測試
合理的推送能夠激活用戶,提高用戶粘性,使用A/B分組測試的科學方法,根據測試反饋的結果,幫助開發者選擇最優化的推送方案。
5、極光推送安全包
為金融、新聞、政務及其他對推送安全要求極高的客戶提供安全嚴謹、穩定可靠的信息推送解決方案
6、可定製的私有雲
對於安全性要求更高,希望推送數據和系統存儲在自己伺服器的客戶,及個性化需求需要定製開發的,性能更高要求的,或者想擁有自己推送平台的甚至要求源碼授權二次開發的開發者,極光提供全功能的私有雲解決方案。
深圳市和訊華谷信息技術有限公司(極光 Aurora Mobile,納斯達克股票代碼:JG)成立於2011年,是中國領先的開發者服務提供商,專注於為開發者提供穩定高效的消息推送、一鍵認證以及流量變現等服務,助力開發者的運營、增長與變現。同時,極光的行業應用已經拓展至市場洞察、金融風控與商業地理服務,助力各行各業優化決策、提升效率。

4. Netty源碼篇-處理OP_ACCEPT

AbstractBootstrap#initAndRegister

MultithreadEventLoopGroup#register

MultithreadEventExecutorGroup#next

AbstractChannel#AbstractUnsafe#register

SingleThreadEventExecutor#execute

SingleThreadEventExecutor#startThread

SingleThreadEventExecutor#doStartThread

如果有客戶端連接,channel會利用unsafe去讀取數據

NioEventLoop#processSelectedKey

AbstractNioMessageChannel#NioMessageUnsafe#read

NioServerSocketChannel#doReadMessages

ServerBootstrap#ServerBootstrapAcceptor#channelRead

5. Netty源碼_UnpooledHeapByteBuf詳解

本篇文章我們講解緩存區 ByteBuf 八大主要類型中兩種,未池化堆緩沖區 UnpooledHeapByteBuf 和 未池化不完全堆緩沖區 UnpooledUnsafeHeapByteBuf

UnpooledHeapByteBuf 是 java 堆緩沖區的實現,而且它推薦使用 UnpooledByteBufAllocator.heapBuffer(int, int) , Unpooled.buffer(int) 和 Unpooled.wrappedBuffer(byte[]) 方式創建 UnpooledHeapByteBuf ,而不是直接調用它的構造方法 new 出來。

有三個成員屬性:

它有兩個構造方法,一個是創建的時候沒有內容,一個創建的時候就帶有內容數據。

分配新的位元組數組。

替換緩存區的位元組數組 array ,必須將 tmpNioBuf 設置為 null 。

你會發現最後都是調用 HeapByteBufUtil 對應方法,這個類 HeapByteBufUtil 我們後面再說。

根據目標緩存區 dst 類型不同,使用的方式也不同。

FileChannel 就是 GatheringByteChannel 的子類。

都是調用 HeapByteBufUtil 對應方法,這個類 HeapByteBufUtil 我們後面再說。

我們知道這個方法是在 類中定義的,當引用計數變成 0 的時候,就會調用這個 deallocate() 方法,釋放持有的資源。

仔細閱讀 UnpooledUnsafeHeapByteBuf 源碼,你會發現這個類很簡單,它是 UnpooledHeapByteBuf 的子類,與 UnpooledHeapByteBuf 區別就兩個方面。

UnpooledUnsafeHeapByteBuf 是通過 PlatformDependent.allocateUninitializedArray 創建數組,利用 Unsafe 來加快數據的訪問

UnpooledUnsafeHeapByteBuf 是通過 UnsafeByteBufUtil 工具類獲取基本數據類型的數據。

先明確一個概念,什麼是大端,什麼是小端。

例如獲取 short

就是通過右移位運算和 或 | 位運算,實現數的拼接。

利用左移位運算,將高位數據轉換成 byte 類型存儲;再使用 (byte) 類型強轉,只保留低八位的數據存儲。

6. Netty源碼_UnpooledDirectByteBuf詳解

本篇文章我們講解緩存區 ByteBuf 八大主要類型中兩種,未池化直接緩沖區 UnpooledDirectByteBuf 和 未池化不安全直接緩沖區 UnpooledUnsafeDirectByteBuf 。

UnpooledDirectByteBuf 一個基於 NIO ByteBuffer 的緩沖區。
建議使用 UnpooledByteBufAllocator.directBuffer(int, int) , Unpooled.directBuffer(int) 和 Unpooled.wrappedBuffer(ByteBuffer) ;而不是顯式調用構造函數。

有四個成員屬性:

通過 allocateDirect(initialCapacity) 方法創建一個新的 NIO 緩存區實例來初始化此緩存區對象。

利用現有的 NIO 緩存區創建此緩存區。

通過 NIO 緩存區 buffer 對應方法獲取基本數據類型數據。

根據目標緩存區 dst 類型不同,處理的方式也不同。

你會發現這些方法都是獲取此緩存區對應 NIO 緩存區 ByteBuffer 對象,調用 ByteBuffer 對象的方法,與 IO 流的交互,進行數據傳輸

和 get 系列方法一樣, set 系列的實現也是靠 NIO 緩存區 ByteBuffer 對應方法。

剩餘方法也幾乎都是和 NIO 緩存區 ByteBuffer 有關,而且也不難,就不做過多介紹了。

UnpooledDirectByteBuf 主要是通過 NIO 緩存區 buffer 來存儲數據。而它獲取和設置數據,也都是通過 NIO 緩存區對應方法實現的。

光看介紹,和 UnpooledDirectByteBuf 沒有任何區別。它也是 UnpooledDirectByteBuf 的子類。

那麼 UnpooledUnsafeDirectByteBuf 和 UnpooledDirectByteBuf 不同處在那裡呢?

通過復習 setByteBuffer 方法,獲取 NIO 緩存區 buffer 對應的直接內存地址。

通過 UnsafeByteBufUtil 對應方法,直接從內存地址獲取對應基本類型數據。

通過 UnsafeByteBufUtil 對應方法,直接向內存地址設置對應基本類型數據。

只有這個類型 hasMemoryAddress() 方法才會返回 true 。

UnpooledUnsafeDirectByteBuf 就是通過直接從內存地址中獲取和設置數據的方式,提高性能。

7. [Netty源碼分析]ByteBuf(一)

ByteBuf通過兩個指針協助讀寫操作,讀操作使用readerIndex,寫操作使用writerIndex.

readerIndex、writerIndex初始值是0,寫入數據時writerIndex增加,讀取數據時readerIndex增加,但是readerIndex不會超過writerIndex.

讀取之後,0-readerIndex之間的空間視為discard的,調用discardReadByte方法可以釋放這一部分空間,作用類似於ByteBuffer的compact方法.readerIndex-writerIndex之間的數據是可讀的,等價於ByteBuffer中position-limit之間的數據.

writerIndex-capacity之間的空間是可寫的,等價於ByteBuffer中limit-capacity之間的空間.

讀隻影響readerIndex、寫隻影響writerIndex,讀寫之間不需要調整指針位置,所以相較於NIO的ByteBuffer,可以極大的簡化讀寫操作

調用discardReadBytes會發生位元組數組的內存復制,所以頻繁調用會導致性能下降

ByteBuf對write操作進行了封裝,有ByteBuf的write操作負責進行剩餘咳喲好難過空間的校驗,如果可用緩沖區不足,ByteBuf會自動進行動態擴展。對於使用者而言不需要關心底層的校驗和擴展細節,只需要不超過capacity即可

對緩沖區進行讀操作時,有的時候我們需要對之前的操作進行回滾,讀操作並不會改變緩沖區的內容,回滾主要是重新設置索引信息

Mark:將當前的位置指針被分到mark變數中

Reset:恢復位置指針為mark中的變數值

ByteBuf有readerIndex、writerIndex,所以有四個相應的方法

markReaderIndex: 將當前readerIndex備份到markedReaderIndex中

resetReaderIndex: 將當前readerIndex設置為markedReaderIndex

markWriterIndex: 將當前readerIndex備份到markedWriterIndex中

resetWriterIndex: 將當前readerIndex設置為markedWriterIndex

3)slice:
返回當前ByteBuf的可讀子緩沖區,即從readerIndex到writerIndex的ByteBuf,返回的ByteBuf和原有緩沖區共享內容,但是維護獨立的索引.當修改其中一個ByteBuf的內容時,另一個也會改變,即雙方持有的是同一個對象的引用

常見類:

相比於PooledHeapByteBuf,UnpooledHeapByteBuf的實現更加簡單,也不容易出現內存管理的問題,所以才性能滿足的情況下,推薦使用UnpooledHeapByteBuf

在I/O通信線程的讀寫緩沖區中使用DirectByteBuf,後端業務消息的編碼使用HeapByteBuf,這樣的組合性能最優

8. Netty 源碼解析 ——— ChannelConfig 和 Attribute

嗯,本文與其說是ChannelConfig、Attribute源碼解析,不如說是對ChannelConfig以及Attribute結構層次的分析。因為這才是它們在Netty中使用到的重要之處。

在 Netty 源碼解析 ——— 服務端啟動流程 (下) 中說過,當我們在構建NioServerSocketChannel的時候同時會構建一個NioServerSocketChannelConfig對象賦值給NioServerSocketChannel的成員變數config。

而這一個NioServerSocketChannelConfig是當前NioServerSocketChannel配置屬性的集合。NioServerSocketChannelConfig主要用於對NioServerSocketChannel相關配置的設置(如,網路的相關參數配置),比如,配置Channel是否為非阻塞、配置連接超時時間等等。

NioServerSocketChannelConfig其實是一個ChannelConfig實例。ChannelConfig表示為一個Channel相關的配置屬性的集合。所以NioServerSocketChannelConfig就是針對於NioServerSocketChannel的配置屬性的集合。

ChannelConfig是Channel所需的公共配置屬性的集合,如,setAllocator(設置用於channel分配buffer的分配器)。而不同類型的網路傳輸對應的Channel有它們自己特有的配置,因此可以通過擴展ChannelConfig來補充特有的配置,如,ServerSocketChannelConfig是針對基於TCP連接的服務端ServerSocketChannel相關配置屬性的集合,它補充了針對TCP服務端所需的特有配置的設置setBacklog、setReuseAddress、setReceiveBufferSize。

DefaultChannelConfig作為ChannelConfig的默認實現,對ChannelConfig中的配置提供了默認值。

接下來,我們來看一個設置ChannelConfig的流程:
serverBootstrap.option(ChannelOption.SO_REUSEADDR, true);
我們可以在啟動服務端前通過ServerBootstrap來進行相關配置的設置,該選項配置會在Channel初始化時被獲取並設置到Channel中,最終會調用底層ServerSocket.setReuseAddress方法來完成配置的設置。
ServerBootstrap的init()方法:

首先對option和value進行校驗,其實就是進行非空校驗。
然後判斷對應的是哪個常量屬性,並進行相應屬性的設置。如果傳進來的ChannelOption不是已經設定好的常量屬性,則會列印一條警告級別的日誌,告知這是未知的channel option。
Netty提供ChannelOption的一個主要的功能就是讓特定的變數的值給類型化。因為從』ChannelOption<T> option』和』T value』可以看出,我們屬性的值類型T,是取決於ChannelOption的泛型的,也就屬性值類型是由屬性來決定的。

這里,我們可以看到有個ChannelOption類,它允許以類型安全的方式去配置一個ChannelConfig。支持哪一種ChannelOption取決於ChannelConfig的實際的實現並且也可能取決於它所屬的傳輸層的本質。

可見ChannelOption是一個Consant擴展類,Consant是Netty提供的一個單例類,它能安全去通過』==』來進行比較操作。通過ConstantPool進行管理和創建。
常量由一個id和name組成。id:表示分配給常量的唯一數字;name:表示常量的名字。

如上所說,Constant是由ConstantPool來進行管理和創建的,那麼ConstantPool又是個什麼樣的類了?

首先從constants中get這個name對應的常量,如果不存在則調用newConstant()來構建這個常量tempConstant,然後在調用constants.putIfAbsent方法來實現「如果該name沒有存在對應的常量,則插入,否則返回該name所對應的常量。(這整個的過程都是原子性的)」,因此我們是根據putIfAbsent方法的返回來判斷該name對應的常量是否已經存在於constants中的。如果返回為null,則說明當前創建的tempConstant就為name所對應的常量;否則,將putIfAbsent返回的name已經對應的常量值返回。(注意,因為ConcurrentHashMap不會允許value為null的情況,所以我們可以根據putIfAbsent返回為null則代表該name在此之前並未有對應的常量值)

正如我們前面所說的,這個ConstantPool<ChannelOption<Object>> pool(即,ChannelOption常量池)是ChannelOption的一個私有靜態成員屬性,用於管理和創建ChannelOption。

這些定義好的ChannelOption常量都已經存儲數到ChannelOption的常量池(ConstantPool)中了。

注意,ChannelOption本身並不維護選項值的信息,它只是維護選項名字本身。比如,「public static final ChannelOption<Integer> SO_RCVBUF = valueOf("SO_RCVBUF");」👈這只是維護了「SO_RCVBUF」這個選項名字的信息,同時泛型表示選擇值類型,即「SO_RCVBUF」選項值為Integer。

好了,到目前為止,我們對Netty的ChannelOption的設置以及底層的實現已經分析完了,簡單的來說:Netty在初始化Channel時會構建一個ChannelConfig對象,而ChannelConfig是Channel配置屬性的集合。比如,Netty在初始化NioServerSocketChannel的時候同時會構建一個NioServerSocketChannelConfig對象,並將其賦值給NioServerSocketChannel的成員變數config,而這個config(NioServerSocketChannelConfig)維護了NioServerSocketChannel的所有配置屬性。比如,NioServerSocketChannelConfig提供了setConnectTimeoutMillis方法來設置NioServerSocketChannel連接超時的時間。
同時,程序可以通過ServerBootstrap或Boostrap的option(ChannelOption<T> option, T value)方法來實現配置的設置。這里,我們通過ChannelOption來實現配置的設置,ChannelOption中已經將常用的配置項預定義為了常量供我們直接使用,同時ChannelOption的一個主要的功能就是讓特定的變數的值給類型化。因為從』ChannelOption<T> option』和』T value』可以看出,我們屬性的值類型T,是取決於ChannelOption的泛型的,也就屬性值類型是由屬性來決定的。

一個attribute允許存儲一個值的引用。它可以被自動的更新並且是線程安全的。
其實Attribute就是一個屬性對象,這個屬性的名稱為AttributeKey<T> key,而屬性的值為T value。

我們可以通過程序ServerBootstrap或Boostrap的attr方法來設置一個Channel的屬性,如:
serverBootstrap.attr(AttributeKey.valueOf("userID"), UUID.randomUUID().toString());
當Netty底層初始化Channel的時候,就會將我們設置的attribute給設置到Channel中:

如上面所說,Attribute就是一個屬性對象,這個屬性的名稱為AttributeKey<T> key,而屬性的值為T value。
而AttributeKey也是Constant的一個擴展,因此也有一個ConstantPool來管理和創建,這和ChannelOption是類似的。

Channel類本身繼承了AttributeMap類,而AttributeMap它持有多個Attribute,這些Attribute可以通過AttributeKey來訪問的。所以,才可以通過channel.attr(key).set(value)的方式將屬性設置到channel中了(即,這里的attr方法實際上是AttributeMap介面中的方法)。

AttributeKey、Attribute、AttributeMap間的關系:
AttributeMap相對於一個map,AttributeKey相當於map的key,Attribute是一個持有key(AttributeKey)和value的對象。因此在map中我們可以通過AttributeKey key獲取Attribute,從而獲取Attribute中的value(即,屬性值)。

Q:ChannelHandlerContext和Channel都提供了attr方法,那麼它們設置的屬性作用域有什麼不同了?
A:在Netty 4.1版本之前,它們兩設置的屬性作用域確實存在著不同,但從Netty 4.1版本開始,它們兩設置的屬性的作用域已經完全相同了。

若文章有任何錯誤,望大家不吝指教:)

聖思園《精通並發與Netty》

9. 如何構建一個基於netty的後端伺服器

Netty服務端創建
當我們直接使用JDK NIO的類庫開發基於NIO的非同步服務端時,需要使用到多路復用器Selector、ServerSocketChannel、SocketChannel、ByteBuffer、SelectionKey等等,相比於傳統的BIO開發,NIO的開發要復雜很多,開發出穩定、高性能的非同步通信框架,一直是個難題。
Netty為了向使用者屏蔽NIO通信的底層細節,在和用戶交互的邊界做了封裝,目的就是為了減少用戶開發工作量,降低開發難度。ServerBootstrap是Socket服務端的啟動輔助類,用戶通過ServerBootstrap可以方便的創建Netty的服務端。

10. Netty源碼-內存泄漏檢測toLeakAwareBuffer

Netty在實現 ByteBuf 時採用了引用計數法進行 ByteBuf 的回收,使用引用計數法進行回收的 ByteBuf 都擴展了 類,在使用 時需要調用 .retain 方法遞增引用計數器,在使用完畢時則需要調用 .release 方法遞減引用計數器,當計數器為 0 時,會進行 ByteBuf 的回收工作:池化的 ByteBuf 不會進行實際的內存釋放,會將佔用的內存歸還給內存池,非池化的 ByteBuf 則會直接釋放內存(為了敘述簡單,後面釋放內存則指真正釋放內存或者將內存歸還給內存池)。

通過上面的描述可知, ByteBuf 的正確回收依賴 retain 和 release 方法的正確調用,內存提前釋放(即在使用 ByteBuf 時沒有調用 retain 方法,導致提前釋放)應用會報錯,用戶也能及時感知到;但是如果使用完 ByteBuf 忘了調用 release 則會導致內存不能及時得到回收,造成內存泄漏,且內存泄漏用戶無法及時感知,久而久之就會發生OOM。為了解決這種問題,Netty採用了內存泄漏檢測機制,發生內存泄漏時會通過日誌將內存泄漏信息列印出來,報告給用戶。

Netty的內存泄漏檢測使用了 WeakReference ,即弱引用,了解過Java四種引用類型(強、軟、弱、虛)和引用隊列( ReferenceQueue )的讀者知道,弱引用持有的對象會在虛擬機觸發GC時(不管回收之後內存是否夠用)被回收掉,如果使用具有引用隊列參數的構造函數實例化 WeakReference 時,弱引用持有的對象在GC被回收時,弱引用自身會被放入引用隊列。

為了後面能更好的理解Netty內存泄漏檢測的細節,下面先看幾個弱引用的例子,在下面的幾個例子中,我們使用的數據類和自定義的弱引用類子類如下:

好了,三個例子已經介紹完畢,後面在介紹Netty內存泄漏檢測時就使用了這里的例子結果,在具體介紹時會和這里的例子一一對應。

Netty中將普通 ByteBuf 轉為具有內存泄漏檢測功能的 ByteBuf 是通過 AbstractByteBufAllocator.toLeakAwareBuffer 方法實現的,我們直接在Eclipse中看該方法的調用層次即可知道Netty在哪裡對 ByteBuf 進行了轉換,該方法調用如下圖所示:

可見池化內存分配器在分配heap或者direct ByteBuf 時都進行了轉換,非池化內存分配器僅在分配direct ByteBuf 時進行了轉換。個人理解時採用池化內存需要特別關注內存釋放,否則為了實現池化內存預先分配的一大塊內存會因為沒有釋放被很快分配完,造成後面沒有內存進行分配。非池化分配的直接內存也需要特別注意釋放,放置內存泄漏;非池化分配的heap內存(其實就是一個 byte 數組)則可以在對象被回收時同時被回收掉,發生內存泄漏的可能性較小。

本節介紹Netty中內存泄漏檢測相關的類,僅做一個大致介紹,類中的重要方法我們放在後面介紹。

主要負責使用 track 方法對指定的 ByteBuf 進行內存檢測泄漏進行追蹤,並返回負責追蹤的 ResourceLeakTracker 類實例,同時在調用 track 方法時,也會根據指定的檢測級別匯報最近的內存泄漏檢測結果。該類由工廠類 ResourceLeakDetectorFactory 負責實例化,默認的實現為 ResourceLeakDetector ,在 ResourceLeakDetectorFactory 類的默認實現 中,也會根據用戶是否配置了 io.netty.customResourceLeakDetector 來決定採用默認實現 ResourceLeakDetector 還是使用用戶自定義的 ResourceLeakDetector ,用戶自定義的 ResourceLeakDetector 必須是其子類。

默認實現為 DefaultResourceLeak , DefaultResourceLeak 實現了 ResourceLeakTracker 和 ResourceLeak 介面,同時也繼承了類 WeakReference ,是一個弱引用實現。首先,同上面 例2 的結果一樣,如果在使用 ByteBuf 時忘了調用 .release 方法,那麼將不會調用 DefaultResourceLeak.clear 方法去手動清空該弱引用持有的實際對象,在發生GC時,會由垃圾收集器對弱引用持有的實際對象進行回收,即發生了內存泄漏,同時該弱引用自身也會被加入到引用隊列中,該引用隊列是 ResourceLeakDetector 的成員域,上面介紹 ResourceLeakDetector 類時說到該類會在用戶 track 指定 ByteBuf 是匯報檢測結果,該類的匯報數據來源就是引用隊列。 DefaultResourceLeak 同時還提供了 record 方法可以讓用戶在指定時機選擇調用,這個方法可以記錄用戶的調用軌跡(堆棧)。 Record 同時也是一種單鏈表,在 DefaultResourceLeak 中就使用單鏈表記錄用戶的調用軌跡。

DefaultResourceLeak 供用戶記錄程序調用軌跡的類,也就是 DefaultResourceLeak.record 方法返回的對象,繼承自 Throwable ,因此可以使用 Throwable.getStackTrace 方法獲得調用軌跡信息,列印在內存泄漏報告中可以讓用戶更好的排除內存泄漏問題。

在上面介紹 ResourceLeakTracker 時,說到其默認實現為 DefaultResourceLeak , DefaultResourceLeak 提供了 record 方法記錄用戶的調用軌跡,用戶可在調用 ByteBuf 方法時調用 record 方法記錄調用軌跡,調用的頻率越多,後面在匯報內存泄漏情況時就能列印出越詳細的信息,這樣也能更方便的排查問題。

Netty提供了兩個 ByteBuf 的封裝類供選擇,就對應不同的 record 調用頻率,每個封裝類都持有 ResourceLeakTracker 對象,Netty根據配置的內存檢測級別(下一節介紹相關配置參數)使用不同的 ByteBuf 封裝類。

Netty提供的兩個 ByteBuf 封裝類就是 和 , 是 的子類, 類僅僅持有 ResourceLeakTracker 對象,但是看其源碼,發現沒有調用過 record 方法,所以只能知道是否發生了內存泄漏時,無法列印出任何調用軌跡信息。 作為 的子類,在 ByteBuf 的多個方法中調用了 record 方法,所以在發生內存泄漏時,能夠列印出比較詳細的調用軌跡信息。

在 類中使用了配置參數 io.netty.leakDetection.acquireAndReleaseOnly 來控制是否只是在調用增加或減少引用計數器的方法時才調用 record 方法記錄調用軌跡,默認為false。 中 retain 和 release 方法因為改變了引用計數器就直接調用了 record 方法,而該類中的其他方法則根據 io.netty.leakDetection.acquireAndReleaseOnly 的配置決定是否調用 record 方法,這里為了節省篇幅就不列出 類中調用 record 的方法了,讀者可自行查看。

在介紹相關配置參數之前,我們先看下Netty提供的內存泄漏檢測級別:

Level.ADVANCED 和 Level.PARANOID 使用的 ByteBuf 包裝類都是 ,我們上面介紹 ResourceLeakDetector 類時提到該類使用 track 方法對指定的 ByteBuf 進行內存檢測泄漏進行追蹤,並返回負責追蹤的 ResourceLeakTracker 類實例,同時在調用 track 方法時,也會根據指定的檢測級別匯報最近的內存泄漏檢測結果。如果內存泄漏檢測級別為 Level.PARANOID 時則每次調用 track 方法都會進行內存泄漏報告;如果級別為 Level.ADVANCED 或者 Level.SIMPLE 則會以一定頻率進行內存泄漏報告,而不是每次 track 都進行報告。

是否關閉Netty內存泄漏檢測功能,默認為false。如果該參數配置為false,則默認的內存泄漏檢測級別根據此參數的配置為 Level.DISABLED ,否則默認的級別為 Level.SIMPLE 。

配置內存泄漏檢測級別的參數,用於老版本的配置參數。

新的內存泄漏檢測級別參數,如果沒有配置,則會採用老版本參數配置的級別作為最終配置。

在第4節介紹內存泄漏檢測相關類時,我們介紹過 DefaultResourceLeak 提供了 record 方法記錄用戶的調用軌跡,如果當前保存的調用軌跡記錄數 Record 大於參數 io.netty.leakDetection.targetRecords 配置的值,那麼會以一定的概率(1/2^n)刪除頭結點之後再加入新的記錄,當然也有可能不刪除頭結點直接新增新的記錄。

該參數的默認為4。

上面介紹過,在 類中使用了配置參數 io.netty.leakDetection.acquireAndReleaseOnly 來控制是否只是在調用增加或減少引用計數器的方法時才調用 record 方法記錄調用軌跡,默認為false。

在介紹 ResourceLeakDetector 類時提到過,默認的 ResourceLeakDetector 類就是 ResourceLeakDetector ,但是用戶可以使用參數 io.netty.customResourceLeakDetector 來決定採用默認實現 ResourceLeakDetector 還是使用用戶自定義的 ResourceLeakDetector 。

我們在第二節介紹了Netty中將普通 ByteBuf 轉為具有內存泄漏檢測功能的 ByteBuf 是通過 AbstractByteBufAllocator.toLeakAwareBuffer 方法實現的。

這里我們先看下該方法的源碼:

上面的源碼中是調用 AbstractByteBuf.leakDetector.track(buf) 返回 ResourceLeakTracker 類對象的,這里我們看下默認的 ResourceLeakDetector 中 track 方法實現:

我們看到 AbstractByteBufAllocator.toLeakAwareBuffer 對 ResourceLeakDetector.track 返回的 DefaultResourceLeak 和傳入的 ByteBuf 對象進行封裝,返回了具有內存泄漏檢測功能的 ByteBuf 封裝類 或其子類 。如果應用程序在使用 ByteBuf 正確調用了 retain 和 release 方法,則在引用計數器為0時,則會清除弱引用持有的實際對象,發生GC時, DefaultResourceLeak 也不會被放入引用隊列中(見前面第2節 例3 結果)。

但是如果應用程序在使用 ByteBuf 沒有正確調用 retain 和 release 方法,則不會清除弱引用持有的實際對象,此時如果實際上已經沒有強引用指向該 ByteBuf ,那麼在發生GC時,垃圾收集器會回收該 ByteBuf ,而弱引用 DefaultResourceLeak 會被放入引用隊列中(見前面第2節 例2 結果),加入到引用隊列中的就是識別到的發生內存泄漏的 ByteBuf 。在 ResourceLeakDetector.track 方法中調用的 reportLeak 輸出的就是引用隊列中的弱引用 DefaultResourceLeak :

到這里,已經基本上介紹完Netty內存檢測的實現原理,下面我們再看下 DefaultResourceLeak.record 是如何記錄調用軌跡的:

最後我們再看下 Record 是如何輸出調用軌跡的,前面我們說到 Record 繼承自類 Throwable ,因此可使用 getStackTrace 方法獲取實例化該對象時的調用軌跡,所以上面在輸出內存泄漏報告時就調用了 Record.toString 方法:

熱點內容
android項目手工編譯 發布:2023-02-06 19:51:46 瀏覽:898
手機怎麼看是什麼編譯器 發布:2023-02-06 19:48:33 瀏覽:612
oracle存儲過程continue 發布:2023-02-06 19:46:12 瀏覽:56
精通腳本黑客光碟 發布:2023-02-06 19:43:37 瀏覽:725
db2創建資料庫表 發布:2023-02-06 19:41:27 瀏覽:852
linux訪問samba 發布:2023-02-06 19:33:08 瀏覽:254
cf三頭龍腳本 發布:2023-02-06 19:29:07 瀏覽:756
linux改語言 發布:2023-02-06 19:21:29 瀏覽:620
韓服fifaol3資料庫 發布:2023-02-06 19:19:22 瀏覽:682
安卓系統小紅書如何設置連播 發布:2023-02-06 19:17:09 瀏覽:688