zoukankan      html  css  js  c++  java
  • 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回收,从而造成内存泄露。
     

    四、总结

    使用全局的对象时,最好不要直接引用生命周期变化的对象,如果非要引用其它对象,则保证被引用的对象也是全局的,不会被销毁重建,如果被引用对象会被销毁重建,则在销毁时,从全局对象中删除对其的引用,以免造成内存泄露。
    作者: 单行线的旋律单行线的旋律's Blog on 博客园
    出处:http://www.cnblogs.com/mycodingworld/
    作品单行线的旋律 创作,采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。 欢迎转载,但任何转载必须保留完整文章,在显要地方显示署名以及原文链接。如您有任何疑问或者授权方面的协商,请 给我留言
    如果喜欢我的文章,欢迎关注我的公众号;分享技术、生活故事,做一个有故事的技术人
  • 相关阅读:
    spring boot 启动原理
    log4j相关配置
    JAVA多线程之volatile 与 synchronized 的比较
    Mybatis 一对一、一对多、多对多
    缓存
    spring boot 总结
    学习网站
    Kafka(一)
    hbase(二)
    Zookeeper那些事
  • 原文地址:https://www.cnblogs.com/mycodingworld/p/framechannel_memory_leak.html
Copyright © 2011-2022 走看看