编码技巧(三)减少GC

选择了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,除非显示的移除。当然内存泄漏的情况还很多,这里不一一说明了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值