阿里妹导读:在《如何回答性能优化的问题,才能打动阿里面试官?》中,主要是介绍了应用常见性能瓶颈点的分布,及如何初判若干指标是否出现了异常。
今天,齐光将会基于之前列举的众多指标,给出一些常见的调优分析思路,即:如何在众多异常性能指标中,找出最核心的那一个,进而定位性能瓶颈点,最后进行性能调优。整篇文章会按照代码、CPU、内存、网络、磁盘等方向进行组织,针对对某一各优化点,会有系统的「套路」总结,便于思路的迁移实践。
1.代码相关
遇到性能问题,首先应该做的是检查否与业务代码相关——不是通过阅读代码解决问题,而是通过日志或代码,排除掉一些与业务代码相关的低级错误。性能优化的最佳位置,是应用内部。
譬如,查看业务日志,检查日志内容里是否有大量的报错产生,应用层、框架层的一些性能问题,大多数都能从日志里找到端倪(日志级别设置不合理,导致线上疯狂打日志);再者,检查代码的主要逻辑,如for循环的不合理使用、NPE、正则表达式、数学计算等常见的一些问题,都可以通过简单地修改代码修复问题。
别动辄就把性能优化和缓存、异步化、JVM调优等名词挂钩,复杂问题可能会有简单解,「二八原则」在性能优化的领域里里依然有效。当然了,了解一些基本的「代码常用踩坑点」,可以加速我们问题分析思路的过程,从CPU、内存、JVM等分析到的一些瓶颈点优化思路,也有可能在代码这里体现出来。
下面是一些高频的,容易造成性能问题的编码要点。
1)正则表达式非常消耗CPU(如贪婪模式可能会引起回溯),慎用字符串的split()、replaceAll()等方法;正则表达式表达式一定预编译。
2)String.intern()在低版本(Java1.6以及之前)的JDK上使用,可能会造成方法区(永久代)内存溢出。在高版本JDK中,如果stringpool设置太小而缓存的字符串过多,也会造成较大的性能开销。
3)输出异常日志的时候,如果堆栈信息是明确的,可以取消输出详细堆栈,异常堆栈的构造是有成本的。注意:同一位置抛出大量重复的堆栈信息,JIT会将其优化后成,直接抛出一个事先编译好的、类型匹配的异常,异常堆栈信息就看不到了。
4)避免引用类型和基础类型之间无谓的拆装箱操作,请尽量保持一致,自动装箱发生太频繁,会非常严重消耗性能。
5)StreamAPI的选择。复杂和并行操作,推荐使用StreamAPI,可以简化代码,同时发挥来发挥出CPU多核的优势,如果是简单操作或者CPU是单核,推荐使用显式迭代。
6)根据业务场景,通过ThreadPoolExecutor手动创建线程池,结合任务的不同,指定线程数量和队列大小,规避资源耗尽的风险,统一命名后的线程也便于后续问题排查。
7)根据业务场景,合理选择并发容器。如选择Map类型的容器时,如果对数据要求有强一致性,可使用Hashtable或者「Map+锁」;读远大于写,使用CopyOnWriteArrayList;存取数据量小、对数据没有强一致性的要求、变更不频繁的,使用ConcurrentHashMap;存取数据量大、读写频繁、对数据没有强一致性的要求,使用ConcurrentSkipListMap。
8)锁的优化思路有:减少锁的粒度、循环中使用锁粗化、减少锁的持有时间(读写锁的选择)等。同时,也考虑使用一些JDK优化后的并发类,如对一致性要求不高的统计场景中,使用LongAdder替代AtomicLong进行计数,使用ThreadLocalRandom替代Random类等。
代码层的优化除了上面这些,还有很多就不一一列出了。我们可以观察到,在这些要点里,有一些共性的优化思路,是可以抽取出来的,譬如:
空间换时间:使用内存或者磁盘,换取更宝贵的CPU或者网络,如缓存的使用;时间换空间:通过牺牲部分CPU,节省内存或者网络资源,如把一次大的网络传输变成多次;其他诸如并行化、异步化、池化技术等。
2.CPU相关
前面讲到过,我们更应该