JVM堆內存泄露分析


 

一、背景

公司有一個中間的系統A可以對接多個后端業務系統B,一個業務系統以一個Namespace代表, Namespace中包含多個FrameChannel(用holder保存),表示A連接到業務系統B各服務實例的連接;A與B通過GRPC通信。
 

二、現象

測試使用一台服務實例A,對應后端的一個業務系統B,該業務系統有兩台服務實例,正常情況NameSpace中包含兩個FrameChannel
 
當后端業務系統升級上線重啟時,會重新創建FrameChannel,但舊的FrameChannel在GC(自己創建大量client,發送埋點消息,並使用jstat觀察gc數量,過程不詳述了)時卻沒有被釋放,正常情況下,FrameChannel數量為2,當B的兩台服務器重啟后,FrameChannel的數量變成4,並在gc時,沒有被釋放。
 
正常情況Framechannel有2個,即兩條線,當重啟B時,會變成4條線,查看堆內存FrameChannel對象,也是4個
既然仍能監控舊的FrameChannel,於是想到將舊的FrameChannel注銷監控
 
再重新將A部署測試,發現當重啟pp時,另外兩個FrameChannel確實沒有數據了,但堆內存中卻仍然有4個FrameChannel對象(原因分析見下面的分析部分)
 
 
最后分析堆內存后,發現注銷指標時少注銷了一部分,重新開發,編譯,打包,部署,並測試
 
發現FrameChannel對象仍然為4個,再分析堆內存,發現被Session引用,於是關閉所有client,再觀察一會,FrameChannel數量終於變成了2個
 

三、分析

dump內存對象,並使用MAT分析, 查看哪些對象在使用FrameChannel
 
可以看見,一共4個FrameChannel對象,經過查看引用,發現3、4對象被Namespace中Holder引用,說明3、4是正常的連接;1、2沒有被Hoder引用,是已經關閉的連接。選擇第1個對象,查看誰引用它
 
共有3個對象引用它,
  1. 第一個this$0是FrameChannel的內部類DownstreamObserver,此內部類對象被grpc使用,經過代碼分析,入口是FrameChannelStub,而此類只被Framechannel本身使用。
  2. 第二個arg$1是一個Lambda表達式生成的對象,此對象又被3個對象引用
 
查看這3個對象,再結果FrameChannel中設置指標監控的代碼,可以知道是監控channelRoom所使用的Lambda表達式
進入guage方法
gauges即是上面第2個引用Lambda表達式的對象
 
再查看registry.register方法
metrics即是上面第1個引用Lambda表達式的對象
 
進入OnMetricAdded, 往下點幾層,可看見
可見將gauge包裝成JmxGuage,通過JMX暴露出來.
 
歸納一下,這三個引用對象所在的類分別是
  • 公司自己封裝的Metrics指標類
  • com.codahale.metrics.MetricRegistry
  • com.codahale.metrics.jmx.JmxReporter
 
看一下,這三個類實例是什么時候被創建的
  • Metrics 是在最開始就會被創建
  • com.codahale.metrics.MetricRegistry和 com.codahale.metrics.jmx.JmxReporter 在 MetricsFactory 類被加載的時候就會被創建
 
MetricsFactory是一個監控指標的工具類,可以說是全局的,不會被JVM卸載,導致其引用的對象不會被釋放。
 
  1. 引用FrameChannel的第三個對象是Session中的channels
 
channels是一個Map類型,其作用是存儲namespace對應的frameChannel,在session第一次向后端業務系統發起事件時,會從Namespace中的Holder選擇一個FrameChannel,放入自身channel的Map中緩存起來,下一次使用時直接從channels map中查詢,不用從namespace holder中獲取。
 
一個 session 對象代表一個客戶端到長連接網關的連接,其是在客戶端連接長連接網關時被創建的。
而session被3個對象引用,下面標的是4個,因為SessionRoom同時會被Namespace中的rooms和FrameChannel中的channelRooms引用
 
我們先看下SessionRoom,它會不會不被釋放?
不會,因為NamespaceManager會定時(每30s)檢查Namespace和FrameChannel中的SessionRoom是否為空,如果為空,則將其從rooms和channelRooms Map中刪除,JVM就可以回收SessionRoom。
 
再看下SessionPool, 它會不會不釋放Session?
不會,因為SessionPool也會定時檢查已經關閉的Session,並將其刪除
 
再看下ClientHead, 它會不會不被釋放?
不會,ClientHead是Netty-SocketIO框架創建的對象,當客戶端連接長連接網關時,會創建ClientHead對象,放入到ClientBox中,當連接關閉時,會將其中ClientBox中刪除,具體請見類:com.corundumstudio.socketio.handler.ClientsBox
 
經過以上分析,發現使用 MetricsFactory 創建出的Metrics,在使用gauga等包含Lambda表達式的方法時,會使被引用的對象無法被GC回收,從而造成內存泄露。
 

四、總結

使用全局的對象時,最好不要直接引用生命周期變化的對象,如果非要引用其它對象,則保證被引用的對象也是全局的,不會被銷毀重建,如果被引用對象會被銷毀重建,則在銷毀時,從全局對象中刪除對其的引用,以免造成內存泄露。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM