Tomcat开启了APR模式,而APR模式会使用堆外内存,关于堆内存可从如下链接了解一下:http://blog.csdn.net/zhouhl_cn/article/details/6573213。
完整异常信息如下:
Exception in thread "http-apr-8080-Acceptor-0" java.lang.OutOfMemoryError: Direct buffer memory at java.nio.Bits.reserveMemory(Bits.java:694) at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) at org.apache.tomcat.util.net.SocketBufferHandler.<init>(SocketBufferHandler.java:38) at org.apache.tomcat.util.net.AprEndpoint$AprSocketWrapper.<init>(AprEndpoint.java:2341) at org.apache.tomcat.util.net.AprEndpoint.setSocketOptions(AprEndpoint.java:907) at org.apache.tomcat.util.net.AprEndpoint.setSocketOptions(AprEndpoint.java:78) at org.apache.tomcat.util.net.Acceptor.run(Acceptor.java:115) at java.lang.Thread.run(Thread.java:748)
1. baidu/google半天,没找到太明确的解决办法。
2.网上提到,如果性能已经足够了,没有必要使用堆外内存,那么是否可以想办法禁用Tomcat使用堆外内存?查阅Tomcat官方文档的配置说明,没有找到。
3.下载相应版本的Tomcat 源码,根据异常信息找一下代码执行的流程,结果发现,代码中是写死的,一定会使用堆外内存,具体代码如下:
// org.apache.tomcat.util.net.AprEndpoint.java:2341行 public AprSocketWrapper(Long socket, AprEndpoint endpoint) { super(socket, endpoint); // TODO Make the socketWriteBuffer size configurable and align the // SSL and app buffer size settings with NIO & NIO2. if (endpoint.isSSLEnabled()) { sslOutputBuffer = ByteBuffer.allocateDirect(SSL_OUTPUT_BUFFER_SIZE); sslOutputBuffer.position(SSL_OUTPUT_BUFFER_SIZE); } else { sslOutputBuffer = null; } socketBufferHandler = new SocketBufferHandler(6 * 1500, 6 * 1500, true); }
// org.apache.tomcat.util.net.SocketBufferHandler.java:38 public SocketBufferHandler(int readBufferSize, int writeBufferSize, boolean direct) { this.direct = direct; if (direct) {// 使用堆外内存 readBuffer = ByteBuffer.allocateDirect(readBufferSize); writeBuffer = ByteBuffer.allocateDirect(writeBufferSize); } else { readBuffer = ByteBuffer.allocate(readBufferSize); writeBuffer = ByteBuffer.allocate(writeBufferSize); } }
4.既然没有办法禁用,是否可以调大?找到了如下参数,可以将此参数调大,此参数默认是64M。启动时命令行加上此参数即可。
-XX:MaxDirectMemorySize=512m
5.网上查询资料说,Full GC时,会将堆外内存回收,但是如果进程内存限制设的比较大,很长时间不会触发Full GC,就会导致这部分堆外内存持续不释放。所以,将进程内存调小一点,使用如下参数。
-Xms2000m -Xmx2000m
6.观察一下,是否还会出现此问题。
7.经观察后,发现问题还会出现,另外,又查了文档后发现-XX:MaxDirectMemorySize参数的默认值并不是64M,而是下面的规则:
- 如果有-XX:MaxDirectMemorySize参数显示指定了数值,则使用此参数指定的;
- 如果没有-XX:MaxDirectMemorySize,则看有没有-Xmx,如果有,则使用-Xmx的值;
- 如果-XX:MaxDirectMemorySize和-Xmx都没有,才会使用64M;
我们的程序中-Xmx设置的是2000M,所以理论上Direct Memory足够用了。
8.分析问题出现的时间点,每次必是00:00:00,排查代码,找到这个时间点进行的可疑的操作有两个,其中一个是定时任务(从业务上讲,这个定时任务已经没用了,但仍然在跑),另一个是log4j2的日志分割、压缩;
9.由于定时任务已经废弃,所以直接去掉此定时任务,再观察,发现问题出现的频率降低了,但并未完全消失,说明该定时任务仅是导致问题的部分原因;
10.修改log4j2.xml,取消对日志文件的压缩,并且,由原来的按天分割,改为按小时分割,并按天建立文件夹存储,文件如下:
<configuration debug="off" monitorInterval="1800"> <Properties> <Property name="log-path">/data/logs/</Property> </Properties> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36}.%M()/%L - %msg%xEx%n"/> </Console> <RollingFile name="DailyRollingFile" fileName="${log-path}/cell.log" filePattern="${log-path}/$${date:yyyy-MM-dd}/cell-%d{HH}.log"> <Policies> <TimeBasedTriggeringPolicy interval="1" modulate="true"/> </Policies> <DefaultRolloverStrategy> <Delete basePath="${log-path}" maxDepth="2"> <IfFileName glob="*/*.log" /> <IfLastModified age="30d" /> </Delete> </DefaultRolloverStrategy> <PatternLayout pattern="[%p] %t %d{HH:mm:ss,SSS} %c{1}:%L(%M) %m%n"/> </RollingFile> </Appenders> <Loggers> <root level="INFO"> <appender-ref ref="DailyRollingFile"/> </root> </Loggers> </configuration>
11.持续观察多天后,问题未再出现,至此问题解决。