开发踩坑记录之四:Tomcat内存溢出问题分析

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 系统平台运行一段时间后,平台出现无法访问的问题,重启对应的服务后平台恢复正常。查看日志发现在凌晨两点零四分之后没有对应的日志输出,直到重启服务后才有日志的正常输出。同时发现在Tomcat的目录下存在hprof文件,即java进程的内存镜像文件。初步猜测Tomcat发生了内存溢出导致服务出现假死现象,即在任务管理器中虽然为运行状态,但是实际已不能正常对外提供服务。  对于hprof文件的分析需要借助于内存分析工具Eclipse Memory Analyzer,通过它寻找到平台发生内存泄露的根源,再根据发生内存泄露的地方以及相关的日志信息定位什么样的业务场景下导致该异常情况的发生。

引言

  系统平台运行一段时间后,平台出现无法访问的问题,重启对应的服务后平台恢复正常。查看日志发现在凌晨两点零四分之后没有对应的日志输出,直到重启服务后才有日志的正常输出。同时发现在Tomcat的目录下存在hprof文件,即java进程的内存镜像文件。初步猜测Tomcat发生了内存溢出导致服务出现假死现象,即在任务管理器中虽然为运行状态,但是实际已不能正常对外提供服务。

 对于hprof文件的分析需要借助于内存分析工具Eclipse Memory Analyzer,通过它寻找到平台发生内存泄露的根源,再根据发生内存泄露的地方以及相关的日志信息定位什么样的业务场景下导致该异常情况的发生,同时采取相关措施防止类似情况再次发生。

  • 问题分析过程
  • 相关源码分析
  • 解决方法
  • 总结

一、问题分析过程

使用Eclipse Memory Analyzer分析工具导入java_pid4668.hprof文件进行分析,如下图所示:

image.png

颜色最深的区域代表最有可能发生内存泄露的地方,我们可以发现RequestInfo这个对象多达3190个,怀疑这个对象的异常增加同时没有被JVM回收,无法释放已经申请的内存空间,导致内存泄露,直至内存被消耗完,最终产生内存溢出问题,从而造成平台崩溃,使得系统无法正常对外提供服务,我们继续往下分析。

image.png

点击对应的RequestInfo项,如上图所示,我们可以发现这个对象所对应的接口地址。这个接口所对应的业务是服务端与客户端之间维持一个websocket的长连接,每隔三十秒客户端向服务端发送心跳,服务端响应心跳以维持客户端在线状态。如果连接建立不成功,客户端每隔三秒就会再次与服务端进行websocket的连接,如此循环往复。

 通过查看维护的代码可知,客户端在连接建立不成功时,会将连接对象销毁后再创建新的连接对象,而服务端在连接建立不成功时并未将连接关闭,又重新使用新的连接对象。随着时间的累积,最终导致内存溢出,影响平台的正常运行。


二、相关源码分析

  下图描述了一个请求进入Tomcat容器后的流转过程。

image.png

备注:该图片来源于网络

package org.apache.coyote;
import java.util.ArrayList;
/** This can be moved to top level ( eventually with a better name ).
 *  It is currently used only as a JMX artifact, to aggregate the data
 *  collected from each RequestProcessor thread.
 */
public class RequestGroupInfo {
  //此处源码中的RequestInfo对象通过一个list进行存放
    private final ArrayList<RequestInfo> processors = new ArrayList<>();
    private long deadMaxTime = 0;
    private long deadProcessingTime = 0;
    private int deadRequestCount = 0;
    private int deadErrorCount = 0;
    private long deadBytesReceived = 0;
    private long deadBytesSent = 0;
  //调用此方法进行请求对象的添加
    public synchronized void addRequestProcessor( RequestInfo rp ) {
        processors.add( rp );
    }
    public synchronized void removeRequestProcessor( RequestInfo rp ) {
        if( rp != null ) {
            if( deadMaxTime < rp.getMaxTime() )
                deadMaxTime = rp.getMaxTime();
            deadProcessingTime += rp.getProcessingTime();
            deadRequestCount += rp.getRequestCount();
            deadErrorCount += rp.getErrorCount();
            deadBytesReceived += rp.getBytesReceived();
            deadBytesSent += rp.getBytesSent();
            processors.remove( rp );
        }
    }
    public synchronized long getMaxTime() {
        long maxTime = deadMaxTime;
        for (RequestInfo rp : processors) {
            if (maxTime < rp.getMaxTime()) {
                maxTime=rp.getMaxTime();
            }
        }
        return maxTime;
    }
    // Used to reset the times
    public synchronized void setMaxTime(long maxTime) {
        deadMaxTime = maxTime;
        for (RequestInfo rp : processors) {
            rp.setMaxTime(maxTime);
        }
    }
    public synchronized long getProcessingTime() {
        long time = deadProcessingTime;
        for (RequestInfo rp : processors) {
            time += rp.getProcessingTime();
        }
        return time;
    }
    public synchronized void setProcessingTime(long totalTime) {
        deadProcessingTime = totalTime;
        for (RequestInfo rp : processors) {
            rp.setProcessingTime( totalTime );
        }
    }
    public synchronized int getRequestCount() {
        int requestCount = deadRequestCount;
        for (RequestInfo rp : processors) {
            requestCount += rp.getRequestCount();
        }
        return requestCount;
    }
    public synchronized void setRequestCount(int requestCount) {
        deadRequestCount = requestCount;
        for (RequestInfo rp : processors) {
            rp.setRequestCount( requestCount );
        }
    }
    public synchronized int getErrorCount() {
        int requestCount = deadErrorCount;
        for (RequestInfo rp : processors) {
            requestCount += rp.getErrorCount();
        }
        return requestCount;
    }
    public synchronized void setErrorCount(int errorCount) {
        deadErrorCount = errorCount;
        for (RequestInfo rp : processors) {
            rp.setErrorCount( errorCount);
        }
    }
    public synchronized long getBytesReceived() {
        long bytes = deadBytesReceived;
        for (RequestInfo rp : processors) {
            bytes += rp.getBytesReceived();
        }
        return bytes;
    }
    public synchronized void setBytesReceived(long bytesReceived) {
        deadBytesReceived = bytesReceived;
        for (RequestInfo rp : processors) {
            rp.setBytesReceived( bytesReceived );
        }
    }
    public synchronized long getBytesSent() {
        long bytes=deadBytesSent;
        for (RequestInfo rp : processors) {
            bytes += rp.getBytesSent();
        }
        return bytes;
    }
    public synchronized void setBytesSent(long bytesSent) {
        deadBytesSent = bytesSent;
        for (RequestInfo rp : processors) {
            rp.setBytesSent( bytesSent );
        }
    }
    public void resetCounters() {
        this.setBytesReceived(0);
        this.setBytesSent(0);
        this.setRequestCount(0);
        this.setProcessingTime(0);
        this.setMaxTime(0);
        this.setErrorCount(0);
    }
}

在AbstractProtocol类中ConnectionHandler类主要可以调用不同的Processor来处理socket请求,解析完成之后再调用Adapter的方法,将请求转发给容器进行处理。

 protected static class ConnectionHandler<S> implements AbstractEndpoint.Handler<S> {
        private final AbstractProtocol<S> proto;
        private final RequestGroupInfo global = new RequestGroupInfo();
        private final AtomicLong registerCount = new AtomicLong(0);
        private final Map<S,Processor> connections = new ConcurrentHashMap<>();
        private final RecycledProcessors recycledProcessors = new RecycledProcessors(this);
        public ConnectionHandler(AbstractProtocol<S> proto) {
            this.proto = proto;
        }
        protected AbstractProtocol<S> getProtocol() {
            return proto;
        }
        protected Log getLog() {  return getProtocol().getLog();
        }
        ...
   protected void register(Processor processor) {
            if (getProtocol().getDomain() != null) {
                synchronized (this) {
                    try {
                        long count = registerCount.incrementAndGet();
                        RequestInfo rp =
                            processor.getRequest().getRequestProcessor();
                         //
                        rp.setGlobalProcessor(global);
                        ObjectName rpName = new ObjectName(
                                getProtocol().getDomain() +
                                ":type=RequestProcessor,worker="
                                + getProtocol().getName() +
                                ",name=" + getProtocol().getProtocolName() +
                                "Request" + count);
                        if (getLog().isDebugEnabled()) {
                            getLog().debug("Register " + rpName);
                        }
                        Registry.getRegistry(null, null).registerComponent(rp,
                                rpName, null);
                        rp.setRpName(rpName);
                    } catch (Exception e) {
                        getLog().warn("Error registering request");
                    }
                }
            }
        }
        }

三、解决办法

  在重写WebSocketHandler接口中的afterConnectionClosed以及handleTransportError方法,即在这两个方法中将对应的session进行关闭。

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
if (session.isOpen()) {
      try {
        session.close();
        logger.info("Server close this session!");
      } catch (Exception e) {
        logger.warn("Server close this session has a exception", e);
      }
    }
  }
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    if (session.isOpen()) {
      try {
        session.close();
        logger.info("Server close this session!");
      } catch (Exception e) {
        logger.warn("Server close this session has a exception", e);
      }
    }
  }

四、总结

  在实际项目中,类似websocket这种资源占用连接,在连接关闭以及连接异常的情况下需要将资源进行释放,避免出现JVM回收不掉创建的对象,最终引起内存溢出,导致平台无法正常运行。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2月前
|
存储 运维
.NET开发必备技巧:使用Visual Studio分析.NET Dump,快速查找程序内存泄漏问题!
.NET开发必备技巧:使用Visual Studio分析.NET Dump,快速查找程序内存泄漏问题!
|
3月前
|
Swift iOS开发
iOS开发-属性的内存管理
【8月更文挑战第12天】在iOS开发中,属性的内存管理至关重要,直接影响应用性能与稳定性。主要策略包括:`strong`(强引用),不维持对象生命期,可用于解除循环引用;`assign`(赋值),适用于基本数据类型及非指针对象属性;`copy`,复制对象而非引用,确保对象不变性。iOS采用引用计数管理内存,ARC(自动引用计数)自动处理引用增减,简化开发。为避免循环引用,可利用弱引用或Swift中的`[weak self]`。最佳实践包括:选择恰当的内存管理策略、减少不必要的强引用、及时释放不再使用的对象、注意block内存管理,并使用Xcode工具进行内存分析。
|
4月前
|
Java 运维
开发与运维内存问题之文件句柄泄漏如何解决
开发与运维内存问题之文件句柄泄漏如何解决
68 3
|
4月前
|
缓存 Java Linux
开发与运维内存问题之线上遇到故障,使用jstat命令发现Old区持续增长如何解决
开发与运维内存问题之线上遇到故障,使用jstat命令发现Old区持续增长如何解决
44 2
|
4月前
|
NoSQL Redis C++
c++开发redis module问题之在复杂的Redis模块中,特别是使用第三方库或C++开发时,接管内存统计有哪些困难
c++开发redis module问题之在复杂的Redis模块中,特别是使用第三方库或C++开发时,接管内存统计有哪些困难
|
4月前
|
Java 运维
开发与运维内存问题之在堆内存中新创建的对象通常首先分配如何解决
开发与运维内存问题之在堆内存中新创建的对象通常首先分配如何解决
23 1
|
4月前
|
Java Windows
Java演进问题之JVM在内存返还策略上会左右为难如何解决
Java演进问题之JVM在内存返还策略上会左右为难如何解决
|
3月前
|
jenkins 持续交付 开发工具
自动化开发之旅:Docker携手Jenkins,与Git和Tomcat共舞持续集成
【8月更文挑战第13天】在软件开发中,持续集成(CI)通过自动化构建、测试与部署提升效率与稳定性。Docker、Jenkins、Git和Tomcat构成CI的黄金组合:`git push`触发Jenkins作业,利用Docker确保环境一致性,最终将应用部署至Tomcat。首先配置Git Webhooks以触发Jenkins;接着在Jenkins中创建作业并使用Docker插件模拟真实环境;通过Maven构建项目;最后部署至Tomcat。这套流程减少人为错误,提高开发效率,展示了技术的力量与流程的革新。
87 0
|
3月前
|
Java 开发工具 Android开发
Android经典面试题之开发中常见的内存泄漏,以及如何避免和防范
本文介绍Android开发中内存泄漏的概念及其危害,并列举了四种常见泄漏原因:静态变量持有Context、非静态内部类、资源未释放及监听器未注销。提供了具体代码示例和防范措施,如使用ApplicationContext、弱引用、适时释放资源及利用工具检测泄漏。通过遵循这些建议,开发者可以有效提高应用稳定性和性能。
52 0
|
4月前
|
缓存 运维 监控
开发与运维内存问题之dmesg,它在故障排查中的作用如何解决
开发与运维内存问题之dmesg,它在故障排查中的作用如何解决
49 0