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

简介: 系统平台运行一段时间后,平台出现无法访问的问题,重启对应的服务后平台恢复正常。查看日志发现在凌晨两点零四分之后没有对应的日志输出,直到重启服务后才有日志的正常输出。同时发现在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日志并进行多维度分析。
相关文章
|
29天前
|
监控 算法 Android开发
安卓应用开发中的内存优化策略
【2月更文挑战第30天】随着移动设备性能的不断提升,用户对应用程序的体验要求越来越高。在安卓应用开发中,内存管理是影响应用性能和用户体验的关键因素之一。本文将探讨针对安卓平台的内存优化技巧,包括避免内存泄漏、合理使用数据结构和算法、优化图片资源处理等策略,旨在帮助开发者提升应用性能和稳定性。
19 1
|
7月前
|
存储 Linux Apache
Apache IoTDB开发之内存工具
IoTDB中的内存分为三部分:写内存,读内存和保留内存。写内存用于数据写入分配。三者的比例可以在配置文件中设置。
127 0
|
3月前
|
架构师 Java 关系型数据库
一线架构师开发总结:剖析并发编程+JVM性能,深入Tomcat与MySQL
每一个程序员都有自己清晰的职业规划和终极目标,无论之后是继续钻研技术,还是转管理岗、产品岗,都是需要自己具备有一定的实力,换句话说技术要牛逼。架构师,是很多程序员的终极目标,而成为一名Java架构师,那就需要对自己自身有一定要求,不仅技术能力要过硬,还需要有组织能力和提出解决方案的能力。那么作为架构师,需要掌握哪些技术呢?
一线架构师开发总结:剖析并发编程+JVM性能,深入Tomcat与MySQL
|
3月前
|
应用服务中间件
Web开发:关于Tomcat出现The origin server did not find a current representation for the target resourc...的问题
Web开发:关于Tomcat出现The origin server did not find a current representation for the target resourc...的问题
|
4月前
|
存储 安全 数据安全/隐私保护
3.2 Windows驱动开发:内核CR3切换读写内存
CR3是一种控制寄存器,它是CPU中的一个专用寄存器,用于存储当前进程的页目录表的物理地址。在x86体系结构中,虚拟地址的翻译过程需要借助页表来完成。页表是由页目录表和页表组成的,页目录表存储了页表的物理地址,而页表存储了实际的物理页框地址。因此,页目录表的物理地址是虚拟地址翻译的关键之一。在操作系统中,每个进程都有自己的地址空间,地址空间中包含了进程的代码、数据和堆栈等信息。为了实现进程间的隔离和保护,操作系统会为每个进程分配独立的地址空间。在这个过程中,操作系统会将每个进程的页目录表的物理地址存储在它自己的CR3寄存器中。当进程切换时,操作系统会修改CR3寄存器的值,从而让CPU使用新的页
48 0
3.2 Windows驱动开发:内核CR3切换读写内存
|
5月前
|
存储 安全 API
3.5 Windows驱动开发:应用层与内核层内存映射
在上一篇博文`《内核通过PEB得到进程参数》`中我们通过使用`KeStackAttachProcess`附加进程的方式得到了该进程的PEB结构信息,本篇文章同样需要使用进程附加功能,但这次我们将实现一个更加有趣的功能,在某些情况下应用层与内核层需要共享一片内存区域通过这片区域可打通内核与应用层的隔离,此类功能的实现依附于MDL内存映射机制实现。
56 0
3.5 Windows驱动开发:应用层与内核层内存映射
|
5月前
|
安全 Windows
3.3 Windows驱动开发:内核MDL读写进程内存
MDL内存读写是一种通过创建MDL结构体来实现跨进程内存读写的方式。在Windows操作系统中,每个进程都有自己独立的虚拟地址空间,不同进程之间的内存空间是隔离的。因此,要在一个进程中读取或写入另一个进程的内存数据,需要先将目标进程的物理内存映射到当前进程的虚拟地址空间中,然后才能进行内存读写操作。
41 0
3.3 Windows驱动开发:内核MDL读写进程内存
|
5月前
|
jenkins 应用服务中间件 持续交付
gitlab、jenkins、tomcat内存限制
gitlab、jenkins、tomcat内存限制
54 0
|
5月前
|
Java 应用服务中间件 Linux
Linux下Tomcat指定JDK和设置内存大小
Linux下Tomcat指定JDK和设置内存大小
143 0
|
7月前
|
Linux
底层开发必知的三个内存结构概念
底层开发必知的三个内存结构概念
底层开发必知的三个内存结构概念