当前位置: 首页 > 科技观察

Java性能优化实战:七种技术手段让性能优化有规律可循

时间:2023-03-11 23:12:52 科技观察

今天主要讲解Java性能优化可以遵循的规律。在上一篇文章中,我们详细了解了性能的定义,这样我们在做性能优化的时候,就有了具体的优化目标和衡量方法,优化效果不会仅仅停留在直观感受上。了解了优化目标之后,接下来我们应该做什么呢?本文主要侧重于理论分析。下面我们来看一下Java性能优化整体上可以遵循的规律。性能优化的7种技术手段性能优化按优化类型分为业务优化和技术优化。业务优化的作用也很大,但属于产品和管理的范畴。作为程序员,在日常工作中,我们面临的优化方式主要是通过一系列的技术手段来完成既定的优化目标。我将这一系列的技术手段大致归纳为如图所示的七类:可以看到,优化方法侧重于计算资源和存储资源的规划。优化方法中有很多用空间换取时间的方法,但只考虑计算速度而不考虑复杂度和空间问题是不可取的。我们要做的是在兼顾性能的同时,达到资源利用的最佳状态。接下来我简单介绍一下这7种优化方式。如果你觉得枯燥,没关系,我们今天的目的就是让你心中有一个整体的概念,对理论基础有一个整体的认识。1.重用优化在写代码的时候,你会发现有很多重复的代码可以提取出来做成公共方法。这样下次用的时候就不用再费心去写了。这个想法就是重用。以上描述是对编码逻辑的优化,数据访问也存在同样的复用情况。无论是在生活中还是在编码中,重复的事情一直在发生。如果没有重用,工作和生活会更累。在软件系统中,提到数据复用,我们首先想到的就是缓冲和缓存。注意这两个词的区别,它们的意思完全不同,很多同学很容易混淆,所以这里简单介绍一下(后续06、07课会详细讲解)。缓冲区(Buffer),常用于数据的暂存,然后批量传输或写入。顺序方法通常用于缓解不同设备之间频繁且缓慢的随机写入。缓冲区主要用于写操作。缓存通常用于读取数据的多路复用。通过将它们缓存在一个相对高速的区域,缓存主要针对读操作。同样,对象池操作,如数据库连接池、线程池等,在Java中也被频繁使用。由于创建和销毁这些对象的成本都比较高,所以我们也会在使用后将这些对象暂存起来,这样下次使用的时候就不用再经过耗时的初始化操作了。2.计算优化(1)并行执行目前的CPU发展很快,大部分硬件都是多核的。如果你想加快一个任务的执行速度,最快最好的解决办法就是让它并行执行。并行执行有以下三种模式。第一种模式是多机,利用负载均衡将流量或大的计算拆分成多个部分同时处理。比如Hadoop通过MapReduce将任务打散,多台机器同时进行计算。第二种模式是使用多进程。例如Nginx就采用了NIO编程模型。Master统一管理Worker进程,然后Worker进程进行真正的请求代理,也可以很好的利用硬件的多CPU。第三种模式是使用多线程,这也是Java程序员接触最多的。比如Netty,它使用的是Reactor编程模型,也使用了NIO,但是它是基于线程的。Boss线程用于接收请求,然后分派给相应的Worker线程进行真正的业务计算。像Golang这样的语言有一个更轻量级的协程(Coroutine)。Coroutine是比线程更轻量级的存在,但是在Java中还不成熟,就不过多介绍了。但本质上也是针对多核的应用,让任务并行执行。(2)从同步到异步的变化计算的另一种优化是从同步到异步,这通常涉及到编程模型的变化。在同步模式下,请求将被阻塞,直到有成功或返回失败结果。其编程模型虽然简单,但在处理时隙偏斜的突发流量时问题尤为突出,请求容易失败。异步操作可以轻松支持横向扩展,也可以缓解瞬时压力,平滑请求。同步请求就像拳头打钢板;异步请求就像拳头打海绵。这个过程大家可以想象一下,后者肯定是有弹性的,体验更友好。(3)最后一类懒加载是利用一些常见的设计模式来优化业务,提升体验,比如单例模式,代理模式等。比如在绘制Swing窗口的时候,如果想展示更多的图片,可以先加载一个placeholder,然后通过后台线程慢慢加载需要的资源,这样可以避免窗口卡死。3.结果集优化接下来介绍一下结果集的优化。举个更直观的例子,我们都知道XML的表现形式很好,那为什么会有JSON呢?除了更容易编写之外,一个重要的原因是它的体积变小了,它的传输效率和分析效率变高了。和Google的Protobuf一样,它的体积更小。虽然可读性降低了,但是在一些高并发场景(比如RPC)下可以明显提升效率,是典型的结果集优化。这是因为我们现在的web服务都是C/S模型。数据从服务器传输到客户端时,需要分发多个副本。这些数据的数量正在迅速扩大。每减少一小部分存储,传输性能和成本就会有比较大的提升。和Nginx一样,一般都会启用GZIP压缩,以保持传输内容的紧凑性。客户端只需要少量的计算能力,轻松解压。由于此操作是分布式的,因此性能损失是固定的。了解了这个原理之后,我们就可以看出结果集优化的大概思路了,返回的数据尽量简单。一些客户端不需要的字段可以在代码中去掉或者直接在SQL查询中去掉。对于一些时效性要求不高但处理能力要求高的业务。应该借鉴缓冲区的经验,尽量减少网络连接的交互,采用批处理的方式来提高处理速度。结果集很可能会被使用两次,你可能会把它加到缓存中,但还是缺乏速度。这时候就需要对数据采集进行优化处理,使用索引或者Bitmap位图来加速数据访问。4.资源冲突优化在我们平时的开发中,会涉及到很多共享资源。其中一些共享资源是独立的,例如HashMap;有些是外部存储,例如数据库行;有些是单个资源,例如Redis中某个键的Setnx;有些是多个资源的协调,比如事务,分布式事务等,现实中有很多性能问题,和锁相关的问题也很多。我们大部分人会想到数据库中的行锁、表锁、Java中的各种锁等,在更底层,比如CPU命令级锁、JVM指令级锁、操作系统内部锁等,可以说无处不在。只有并发才会产生资源冲突。即同一时间只有一个处理请求可以获得共享资源。解决资源冲突的办法就是加锁。再比如事务,本质上是一种锁。根据锁级别,锁可以分为乐观锁和悲观锁。乐观锁肯定更高效;根据锁的类型,锁分为公平锁和非公平锁。调度任务有一些微妙之处。不同之处。对资源的争用会造成严重的性能问题,所以会有一些对无锁队列的研究,对性能有很大的提升。5.算法优化算法可以显着提升复杂业务的性能,但在实际业务中,往往是变体。随着存储越来越便宜,在一些CPU非常吃紧的业务中,往往会采用空间换时间的方式来加快处理速度。算法属于代码调优,而代码调优涉及到很多编码技巧,需要用户非常熟悉所使用语言的API。有时,算法和数据结构的灵活运用也是代码优化的重要一环。例如,常用的降低时间复杂度的方法有递归、二分切分、排序、动态规划等。一个好的实施比一个糟糕的实施对系统的影响要大得多。比如作为List的实现,LinkedList和ArrayList在随机访问性能上差了几个数量级;又如CopyOnWriteList采用了??copy-on-write的方式,在读多写少的场景下可以显着减少锁冲突。而什么时候使用同步,什么时候线程安全,对我们的编码能力也有很高的要求。6、高效实现在日常编程中,尽量使用一些设计理念良好、性能优越的组件。例如,使用Netty,无需选择较旧的Mina组件。在设计系统时,考虑性能因素,不要选择SOAP等耗时的协议。再比如,一个好的语法分析器(比如使用JavaCC)会比正则表达式效率高很多。简而言之,如果通过测试分析发现了系统的瓶颈,就必须将关键部件更换为更高效的部件。在这种情况下,适配器模式非常重要。这就是为什么许多公司喜欢在现有组件之上抽象出自己的一层;底层组件切换时,上层应用无感知。7、JVM优化由于Java运行在JVM虚拟机上,所以它的很多特性都受到了JVM的限制。优化JVM虚拟机也可以在一定程度上提高JAVA程序的性能。如果参数配置不当,甚至会造成OOM等严重后果。目前广泛使用的垃圾收集器是G1,它可以通过很少的参数配置来高效回收内存。CMS垃圾收集器在Java14中被移除。由于其GC时间不可控,因此应尽可能避免。JVM性能调优涉及方方面面的取舍,往往牵一发而动全身。需要充分考虑各方面的影响。所以了解JVM内部的一些运行原理是非常重要的。有利于我们加深对代码的理解,帮助我们写出更高效的代码。总结以上就是代码优化的七个大方向。通过简单的介绍,让大家对性能优化的内容有一个大概的了解。这7大方向是代码优化最重要的方向。当然,性能优化还包括数据库优化、操作系统优化、架构优化等其他内容。这些不是我们的重点。在后面的合集中,我们只会做一个简单的介绍介绍。在下一篇文章中,我们将学习一些性能评估工具,了解操作系统的一些资源限制,然后讨论这七个优化点。