选择了Java,那么在你享受它带来的便利的同时,也必须忍受它的缺点。少了C++里的指针,也不需要开发人员去管理内存的分配,JVM提供了很好内存管理机制,帮助分配与回收内存。在为你省心的同时又为你制造了麻烦,回收内存时,会导致系统不对外提供服务,而只专心做一件事——回收内存(GC)。 在一个正常服务中,这个时间很短,短到感知不到(没有full GC)。但是如果写了错误的代码或是系统到达瓶颈或是别的什么原因,导致内存不足,就会导致系统频繁的GC,甚至是full GC。这时系统会变慢,接口相应时间变长(超出调用方设置的最大时间),甚至不对外提供服务(专心做GC,但是完全清理不出内存来)。 当上述情况发生的时候,一般是先想办法服务降级(重启服务,限流,切流量),尽快恢复服务;再使用分析工具分析频繁GC的原因。这些都是后话,那么如何未雨绸缪,防患于未然呢?
每写一行代码,就想下有没有可能导致频繁GC对外提供的服务,是否做了限流控制,超出系统瓶颈时,有没有办法做服务降级对JVM的状况做监控,在异常发生的前夕就做好准备(GC次数和时间通常不是一蹴而就,有个增长的过程)。
下面将列举一下,可能导致频繁GC的代码
日志 日志有助于帮助我们分析系统的运行情况,排查问题,是平时写代码不可缺少的一部分,但是不恰当的使用,可能导致系统异常。
public class MessageListenerImpl implements MessageListener { private static final Logger LOGGER = LoggerFactory.getLogger(MessageListenerImpl.class); public void onMessage(Message message) { LOGGER.info("receive a message, message is {}", message); Stopwatch stopWatch = Stopwatch.createStarted(); try { //handle message } finally { Monitor.record("reveice_message", stopWatch.stop().elapsed(TimeUnit.MILLISECONDS)); } } }消息是我们经常遇到的,上面的例子展示了接受一个消息,然后打印出消息体,再处理消息,最后记个监控的过程。咋一看似乎没什么问题,三两行能有什么问题呢?如果这里的消息体很大,QPS很高呢? 我曾经写过这样的代码
@Controller @RequestMapping("/user") public class UserManagerCOntroller { private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class); @RequestMapping(value = "/login") public void login(String userName, String password) { Preconditions.checkArgument(StringUtils.isNotBlank(userName), "userName is need"); Preconditions.checkArgument(StringUtils.isNotBlank(password), "password is need"); //do something } }用户登录验证的接口, 传入用户名密码,首先检查是否为空,为空则交给统一异常handler去处理。
@Component public class ExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { LogUtil.ERROR_LOGGER.error("request {} parameter is {} error", request.getRequestURL(), JsonUtil.writeAsString(request.getParameterMap()), ex); exceptionHandler.handle(request, response, handler, ex); return null; } }这里将统一处理controller里抛出的所有异常,也很简单,打印出异常的内容,然后按照异常的分类做出不同的处理。问题同样很明显,当接口的参数检查不通过时就会打印出异常的内容以及异常栈,量越大问题越明显。 总之,在写代码时,要预估下接口的qps是多少。如果不能预估,那就做好监控。
频繁创建对象
GC消灭的主要目标就是大量的短暂存在的对象。因此如果减少此类对象的创建,能够复用的尽量复用,也能达到目的。 - 通常在编码过程中,对于方法的输出只依赖输入时,我们可以将之写为静态方法 - 如果类是线程安全的,那么在使用时,可将之写成单例 - 能够复用的资源,尽量复用(线程池,http连接池等) - 谨慎使用String.intern方法
警惕内存泄漏
所谓内存泄漏,是指长生命周期对象持有短生命周期对象的引用,导致短生命周期对象不能被GC,从而导致内存溢出。比如有一个静态Map,那么存放在Map中的对象将不能被GC,除非显示的移除。当然内存泄漏的情况还很多,这里不一一说明了。
