java-performance-tuning

Java 性能调优不像是学一门编程语言,无法通过直线式的思维来掌握和应用,它对于工程师的技术广度和深度都有着较高的要求。
互联网时代,一个简单的系统就囊括了应用程序、数据库、容器、操作系统、网络等技术,线上一旦出现性能问题,就可能要你协调多方面组件去进行优化,这就是技术广度;而很多性能问题呢,又隐藏得很深,可能因为一个小小的代码,也可能因为线程池的类型选择错误…可归根结底考验的还是我们对这项技术的了解程度,这就是技术深度。

我们调优的对象不是单一的应用服务,而是错综复杂的系统。应用服务的性能可能与操作系统、网络、数据库等组件相关,所以我们需要储备计算机组成原理、操作系统、网络协议以及数据库等基础知识。具体的性能问题往往还与传输、计算、存储数据等相关,那我们还需要储备数据结构、算法以及数学等基础知识。

如果你们公司做的是 12306 网站,不做系统性能优化就上线,试试看会是什么情况。

一款线上产品如果没有经过性能测试,那它就好比是一颗定时炸弹,你不知道它什么时候会出现问题,你也不清楚它能承受的极限在哪儿。

好的系统性能调优不仅仅可以提高系统的性能,还能为公司节省资源

有哪些参考因素可以体现系统的性能?上面我们讲到了在项目研发的各个阶段性能调优是如何介入的,其中多次讲到了性能指标,那么性能指标到底有哪些呢?在我们了解性能指标之前,我们先来了解下哪些计算机资源会成为系统的性能瓶颈。CPU:有的应用需要大量计算,他们会长时间、不间断地占用 CPU 资源,导致其他资源无法争夺到 CPU 而响应缓慢,从而带来系统性能问题。例如,代码递归导致的无限循环,正则表达式引起的回溯,JVM 频繁的 FULL GC,以及多线程编程造成的大量上下文切换等,这些都有可能导致 CPU 资源繁忙。内存:Java 程序一般通过 JVM 对内存进行分配管理,主要是用 JVM 中的堆内存来存储 Java 创建的对象。系统堆内存的读写速度非常快,所以基本不存在读写性能瓶颈。但是由于内存成本要比磁盘高,相比磁盘,内存的存储空间又非常有限。所以当内存空间被占满,对象无法回收时,就会导致内存溢出、内存泄露等问题。磁盘 I/O:磁盘相比内存来说,存储空间要大很多,但磁盘 I/O 读写的速度要比内存慢,虽然目前引入的 SSD 固态硬盘已经有所优化,但仍然无法与内存的读写速度相提并论。网络:网络对于系统性能来说,也起着至关重要的作用。如果你购买过云服务,一定经历过,选择网络带宽大小这一环节。带宽过低的话,对于传输数据比较大,或者是并发量比较大的系统,网络就很容易成为性能瓶颈。异常:Java 应用中,抛出异常需要构建异常栈,对异常进行捕获和处理,这个过程非常消耗系统性能。如果在高并发的情况下引发异常,持续地进行异常处理,那么系统的性能就会明显地受到影响。数据库:大部分系统都会用到数据库,而数据库的操作往往是涉及到磁盘 I/O 的读写。大量的数据库读写操作,会导致磁盘 I/O 性能瓶颈,进而导致数据库操作的延迟性。对于有大量数据库读写操作的系统来说,数据库的性能优化是整个系统的核心。锁竞争:在并发编程中,我们经常会需要多个线程,共享读写操作同一个资源,这个时候为了保持数据的原子性(即保证这个共享资源在一个线程写的时候,不被另一个线程修改),我们就会用到锁。锁的使用可能会带来上下文切换,从而给系统带来性能开销。JDK1.6 之后,Java 为了降低锁竞争带来的上下文切换,对 JVM 内部锁已经做了多次优化,例如,新增了偏向锁、自旋锁、轻量级锁、锁粗化、锁消除等。而如何合理地使用锁资源

数据库响应时间:数据库操作所消耗的时间,往往是整个请求链中最耗时的;
服务端响应时间:服务端包括 Nginx 分发的请求所消耗的时间以及服务端程序执行所消耗的时间;
网络响应时间:这是网络传输时,网络硬件需要对传输的请求进行解析等操作所消耗的时间;
客户端响应时间:对于普通的 Web、App 客户端来说,消耗时间是可以忽略不计的,但如果你的客户端嵌入了大量的逻辑处理,消耗的时间就有可能变长,从而成为系统的瓶颈。

网络吞吐量不仅仅跟带宽有关系,还跟 CPU 的处理能力、网卡、防火墙、外部接口以及 I/O 等紧密关联。

当系统压力上升时,你可以观察,系统响应时间的上升曲线是否平缓。这项指标能直观地反馈给你,系统所能承受的负载压力极限。

QPS特指的一次查询请求,TPS是指每次处理事务请求,TPS包括了QPS,例如一个事务处理可能包括多个查询请求。

当我们做性能测试时,我们的系统会运行得越来越快,后面的访问速度要比我们第一次访问的速度快上几倍。这是怎么回事呢?
在 Java 编程语言和环境中,.java 文件编译成为 .class 文件后,机器还是无法直接运行 .class 文件中的字节码,需要通过解释器将字节码转换成本地机器码才能运行。
为了节约内存和执行效率,代码最初被执行时,解释器会率先解释执行这段代码。
随着代码被执行的次数增多,当虚拟机发现某个方法或代码块运行得特别频繁时,就会把这些代码认定为热点代码(Hot Spot Code)。
为了提高热点代码的执行效率,在运行时,虚拟机将会通过即时编译器(JIT compiler,just-in-time compiler)把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后存储在内存中,之后每次运行代码时,直接从内存中获取即可。

在完成性能测试之后,需要输出一份性能测试报告,帮我们分析系统性能测试的情况。
其中测试结果需要包含测试接口的平均、最大和最小吞吐量,响应时间,服务器的 CPU、内存、I/O、网络 IO 使用率,JVM 的 GC 频率等。

public class SharedLocation {

  private String city;
  private String region;
  private String countryCode;
}

public class Location {

  private SharedLocation sharedLocation;
  double longitude;
  double latitude;
}
SharedLocation sharedLocation = new SharedLocation();

sharedLocation.setCity(messageInfo.getCity().intern());    
sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());

Location location = new Location();
location.set(sharedLocation);
location.set(messageInfo.getLongitude());
location.set(messageInfo.getLatitude());

严格来说,是静态常量池和运行时常量池,静态常量池是存放字符串字面量、符号引用以及类和方法的信息,而运行时常量池存放的是运行时一些直接引用。

运行时常量池是在类加载完成之后,将静态常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。

这两个常量池在JDK1.7版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。

正则表达式

正则表达式引擎
DFA 自动机(Deterministic Final Automaton 确定有限状态自动机)
NFA 自动机(Non deterministic Finite Automaton 非确定有限状态自动机)

构造 DFA 自动机的代价远大于 NFA 自动机,但 DFA 自动机的执行效率高于 NFA 自动机

贪婪模式(Greedy) 顾名思义,就是在数量匹配中,如果单独使用 +、 ? 、* 或{min,max} 等量词,正则表达式会匹配尽可能多的内容。
text=“abbc” regex=“ab{1,3}c”
懒惰模式(Reluctant)在该模式下,正则表达式会尽可能少地重复匹配字符。如果匹配成功,它会继续匹配剩余的字符串。
text=“abbc” regex=“ab{1,3}?c”
独占模式(Possessive)同贪婪模式一样,独占模式一样会最大限度地匹配更多内容;不同的是,在独占模式下,匹配失败就会结束匹配,不会发生回溯问题。
text=“abbc” regex=“ab{1,3}+bc”

在很多情况下使用懒惰模式和独占模式可以减少回溯的发生。

  1. 少用贪婪模式,多用独占模式
  2. 减少分支选择 分支选择类型“(X|Y|Z)”的正则表达式会降低性能
  3. 减少捕获嵌套 一般一个 () 就是一个捕获组,捕获组可以进行嵌套。

for(:)循环[这里指的不是for(;;)]是一个语法糖,这里会被解释为迭代器,在使用迭代器遍历时,ArrayList内部创建了一个内部迭代器iterator,在使用next()方法来取下一个元素时,会使用ArrayList里保存的一个用来记录List修改次数的变量modCount,与iterator保存了一个expectedModCount来表示期望的修改次数进行比较,如果不相等则会抛出异常;

而在在foreach循环中调用list中的remove()方法,会走到fastRemove()方法,该方法不是iterator中的方法,而是ArrayList中的方法,在该方法只做了modCount++,而没有同步到expectedModCount。

当再次遍历时,会先调用内部类iteator中的hasNext(),再调用next(),在调用next()方法时,会对modCount和expectedModCount进行比较,此时两者不一致,就抛出了ConcurrentModificationException异常。

所以关键是用ArrayList的remove还是iterator中的remove。