内存溢出对于我们做开发的人来说肯定是听说过的,但是对于java开发程序员想要遇到一次真正的内存溢出还挺不容易的。因为java自己会有内存回收机制,所以我们一般都是分配好内存后只管使用,不管回收,不用担心内存的问题。而这次居然让我碰上了一次。可得好好记录一下。

        首先问题的表象是这样的。项目中有一个服务是提供了前端报表页面的数据查询统计功能,而这个服务后来发现一直在启动后不久就会挂掉,然后就连接不上zookeeper注册中心。重启服务又可以连接上了。刚开始还以为是zookeeper集群的问题。后来通过xshell的top命令观察内存的时候发现在服务启动后,内存使用一直不停的在变多,一直飙升到5G。而联系运维同事得知,该服务的内存只分配了2G,难道是项目需要的内存不够?导致JVM没有内存?这时候开始仔细观察日志,发现了重要点。

11-21 00:00:51.058 [ERROR] [pool-4-thread-1] org.springframework.scheduling.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task.
java.lang.OutOfMemoryError: GC overhead limit exceeded
	at redis.clients.jedis.Protocol.processBulkReply(Protocol.java:181)
	at redis.clients.jedis.Protocol.process(Protocol.java:158)
	at redis.clients.jedis.Protocol.processMultiBulkReply(Protocol.java:209)
	at redis.clients.jedis.Protocol.process(Protocol.java:160)
	at redis.clients.jedis.Protocol.read(Protocol.java:218)
	at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:341)
	at redis.clients.jedis.Connection.getBinaryMultiBulkReply(Connection.java:277)
	at redis.clients.jedis.BinaryJedis.hgetAll(BinaryJedis.java:1137)
	at redis.clients.jedis.BinaryJedisCluster$48.execute(BinaryJedisCluster.java:555)
	at redis.clients.jedis.BinaryJedisCluster$48.execute(BinaryJedisCluster.java:552)
	at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:114)
	at redis.clients.jedis.JedisClusterCommand.runBinary(JedisClusterCommand.java:57)
	at redis.clients.jedis.BinaryJedisCluster.hgetAll(BinaryJedisCluster.java:557)
	at com.*.framework.cache.JedisClusterImpl.getMapValues(JedisClusterImpl.java:257)
	at com.*.scheduleTask.EmailAlertTask.*(EmailAlertTask.java:431)

     这个时候发现这个JVM的GC因为内存不够所以已经GC失败了。所以我们可以知道应该是内存被应用消耗完了。这时候我们去看JVM的dump文件发现里面的某一个对象所占的空间特别大。有一百多万个,而这个对象是刚好对应着数据库的一张表。这张表的属性很多,每个属性值的内容也不少。

     检查代码才发现,原来这里有一个方法涉及到了统计的功能,在这一个方法中需要把这种表中所有的对象查出来,大概1万2千个,而根据业务查了3次,导致每个方法会产生3万6个大对象。前端也在一直轮询查询,30秒一次。这样下来经过换算,14分钟就会达到上百万个对象。这时候可能会有疑惑,为什么这里的内存没有回收呢?我猜想是因为这里的方法响应慢,来不及触发JVM的GC,GC的垃圾回收算法都是以空间或时间作为纬度来触发,这里先不深究。那我们怎么解决呢?

     从上层解决。该方法调用的是dubbo的接口方法。而dubbo的provider有线程池的配置,我们可以通过修改dubbo的线程池配置,从程序的上游来解决内存中对象多的问题,相当于控制源头,不让生产出那么多对象。一般dubbo的线程池配置是这样的

<dubbo:provider retries="5" timeout="60000" loadbalance="leastactive" executes="2000" threads="2000" actives="2000"/>

其中executes设置为2000。说明可以创建2000个线程。这样来一个请求就会创建一个线程,线程就会生成新的对象。所以我们把这里的值设置为50或者100。这个看项目大小情况。这样就发现内存降下来了。很意外吧。没想到居然通过dubbo调优的的方式解决了这个问题。以下是总结:

       在发生内存溢出的事件后,我们可以先加大内存(如果有条件的话),虽然通常是没有用的,哈哈。但是也可以增加我们的观察时间。当还没有找到问题的时候,可以对服务进行监控(做一个心跳接口)。看一下大概服务挂掉的时间点。看看有没有规律。这些都有助于我们排查问题。也可以在服务准备挂掉的时候重启,争取不影响线上的服务(前提是服务有做负载均衡)。这些都是紧急的一些处理方法。

       分析原因的时候,我们需要从自己的代码中分析哪里会导致内存被消耗,这里可以通过应用的日志,方法的调用频率等其他角度来分析,而分析JVM的日志文件也是一个排查问题的办法。因为内存溢出原理说白了就是内存不够用了。然后再根据自己应用的实际情况去选择处理的方式。JVM底层的垃圾回收机制虽然也是一个解决的方向,但就普通的应用而言,还远远没到需要修改这里的程度。所以只需要查看GC的回收算法是否合理就可以了。最终还是要回归到自己写的代码中去。看看是不是自己生成的对象太多了,避免一些没有必要的内存浪费。然后如果是微服务架构的项目,可以从使用的RPC框架去看看有没有调优的地方。

        时刻准备着,说不定下一个内存溢出就会找上你哦~哈哈