现象:
`生产环境运行一段时间后总是出现内存溢出,导致服务自动宕机。根据反馈,系统在执行某一任务时就会出现该现象,通过本地进行测试,很快也复现了该问题。
原因分析:
通过java自带工具jconsole监控服务,发现内存占用一直居高不下,老年代堆空间始终得不到回收。
通过jvisualvm工具导出dump日志,再使用jvisualvm进行分析。可以看到byte数组占据了93%的堆内存空间。
转到实例视图,查看具体的实例
看到持有这个byte数组对象的是一个 HeapByteBuffer对象,HeapByteBuffer是java NIO中的对象。程序中没有使用NIO,推测NIO应该在Tomcat中被使用。然后查看Tomcat相关配置,发现有一项配置是max-http-header-size=1024000(10M),这是设置请求头大小,默认大小为8KB。如果设置了max-http-header-size参数,会覆盖掉原有的默认值。
为什么会有那么大的数组对象
从Http11InputBuffer初始化过程可以看到,maxHeaderBufferSize会作为参数传递给headerBufferSize。
继续跟踪发现headerBufferSize会被用于分配缓冲大小。通过Http11InputBuffer可以看出,若是max-http-header-size配置为10M,Http11InputBuffer就会分配一个10M的缓冲区,同理,Http11OutputBuffer也会分配10M的缓冲区,那么一个请求线程就是20M。
流程分析
Tomcat 接收请求时
- NioEndpoint 将请求封装为 NioEndpoint$SocketProcessor 交给 Tomcat ThreadPoolExecutor 执行处理
- 执行调用 AbstractProtocol$ConnectionHandler 的 process 方法
- 内部使用 HTTP11NioProtocol 处理此请求,会实例化一个 Http11Processor ,最终调用其 process 方法处理
- 在 Http11Processor 实例化时会直接初始化一个 10M大小的 Http11OutputBuffer和 一个10M大小的Http11InputBuffer
- 平均每个线程持有 20M 大小的缓存数组,当并发请求时,线程池里的线程数达到一定数量时,超出了设置最大堆内存就会导致 OOM