这里写目录标题

调优的流程

调优前首先要做的是 衡量系统现状 ,这包括目前系统的请求次数、响应时间、资源消耗等信息,例如A系统目前95%的请求响应时间为1秒。

在有了系统现状后可 设定调优目标 ,通常调优目标是根据用户所能接受的响应速度或系统所拥有的机器以及所支撑的用户量制定出来的,因此通常会设定出调优目标:95%的请求要在500ms 内返回。

在设定了调优的目标后,需要做的是 寻找出性能瓶颈 ,这一步最重要的是找出造成目前系统性能不足的最大瓶颈点。找出后,可结合一些工具来找出造成瓶颈点的代码,到此才完成了这个步骤。

在寻找到了造成瓶颈点的代码后,通常需要 分析其需求场景 ,然后结合一些优化的技巧 制定优化的策略 。优化策略或简或繁,选择其中收益比(优化后的预期效果/优化需要付出的代价)最高的优化方案,进行优化。

优化部署后, 继续衡量系统的状况 ,如已达到目标,则可结束此次调优;如仍未达到目标,则要看是否产生了新的性能瓶颈。或可以考虑继续尝试上一步中制定的其他优化方案,直到达成调优目标或论证在目前的体系结构上无法达成调优目标为止。

CPU类问题

概念

  1. 上下文切换

每个CPU(或多核CPU中的每核CPU)在同一时间只能执行一个线程 ,Linux采用的是抢占式调度。即为每个线程分配一定的执行时间,当到达执行时间、线程中有IO阻塞或高优先级线程要执行时,Linux将 切换执行的线程 , 在切换时要存储目前线程的执行状态,并恢复要执行的线程的状态,这个过程就称为上下文切换 。对于Java应用,典型的是在进行文件IO操作、网络IO操作、锁等待或线程Sleep时,当前线程会进入阻塞或休眠状态,从而触发上下文切换,上下文切换过多会造成内核占据较多的CPU使用,使得应用的响应速度下降。

  1. 运行队列

每个CPU核都维护了一个可运行的线程队列,例如一个4核的CPU,Java应用中启动了8个线程,且这8个线程都处于可运行状态,那么在分配平均的情况下每个CPU中的运行队列里就会有两个线程。通常而言,系统的load主要由CPU的运行队列来决定,假设以上状况维持了1分钟,那么这1分钟内系统的load就会是2,但由于load是个复杂的值3,因此也不是绝对的, 运行队列值越大,就意味着线程会要消耗越长的时间才能执行完 。Linux System and NetWork Performance Monitoring *中建议控制在每个CPU核上的运行队列为13个。

  1. 利用率

**CPU利用率为CPU在用户进程、内核、中断处理、IO等待以及空闲五个部分使用百分比,这五个值是用来分析CPU消耗情况的关键指标。**Linux System and NetWork Performance Monitoring 中建议用户进程的CPU 消耗/内核的CPU消耗的比率在65%~70%/30%~35%左右。

工具和指令

  1. top

在此需要关注的是第三行的信息,其中 4.0% us 表示 为用户进程处理所占的百分比 ;8.9% sy 表示为 内核线程处理所占的百分比 ;0.0% ni表示被nice 命令改变优先级的任务所占的百分比;87.0%id表示CPU的空闲时间所占的百分比;0.0% wa表示为在执行的过程中等待IO所占的百分比;0.2% hi表示为硬件中断所占的百分比;0.0% si表示为软件中断所占的百分比。

进入top后按1会按cpu核来显示消耗情况:

在TOP视图中按shift+h会按线程来显示消耗状况:

  1. pidstat

pidstat是SYSSTAT中的工具,如须使用pidstat,请先安装SYSSTAT5。

输入pidstat 1 2,在console上将会每隔1秒输出目前活动进程的CPU消耗状况,共输出2次,结果如图所示:

其中CPU表示的为当前进程所使用到的CPU个数。

如须查看某进程中线程的CPU消耗状况,可输入 pidstat -p [PID]-t 15 这样的方式来查看,执行后的输出如图所示:

图中的TID即为线程ID,较之top命令方式而言,pidstat 的好处为 可查看每个线程的具体CPU利用率的状况(例如%system)。

对于JAVA应用而言,最重要的消耗体现在us,sy这两个值上。

当us值过高时,表示运行的应用消耗了大部分的CPU。在这种情况下,对于Java应用而言,最重要的为找到具体消耗 CPU的线程所执行的代码,可采用如下方法做到。

首先通过linux提供的命令 找到消耗CPU严重的线程及其ID ,将此线程ID转化为十六进制的值。之后 通过kill -3 [javapid]或jstack 的方式 dump出应用的java线程信息,通过之前转化出的十六进制的值找到对应的nid值的线程 。该线程即为消耗CPU的线程,在采样时须多执行几次上述的过程,以确保找到真实的消耗CPU的线程。

发生原因:

1.代码循环,计算量大等

Java应用造成us高的原因主要是 线程一直处于可运行(Runnable)状态 ,通常是这些 线程在执行无阻塞、循环、正则或纯粹的计算 等动作造成

2.GC频繁

另外一个可能也会造成us高的原因是 频繁的GC 。如每次请求都需要分配较多内存,当访问量高的时候就将导致不断地进行GC,系统响应速度下降,进而造成堆积的请求更多,消耗的内存更严重,最严重的时候有可能会导致系统不断进行Full GC,对于频繁GC的状况要通过分析JVM内存的消耗来查找原因。

解决方案:

  1. 适当释放CPU

根据之前的分析,CPU us高的原因主要是执行线程无任何挂起动作,且一直执行,导致CPU没有机会去调度执行其他的线程,造成线程饿死的现象。对于这种情况,常见的一种优化方法是 对这种线程的动作增加Thread.sleep,以释放CPU的执行权,降低 CPU的消耗。

当然,这种修改方式是以 损失单次执行性能为代价的,但由于降低了CPU的消耗,对于多线程的应用而言,反而提高了总体的平均性能。

在实际的Java应用中会有很多类似的场景,例如多线程的任务执行管理器,它通常要通过扫描任务集合列表来执行任务。对于这些类似的场景,都可通过增加一定的 sleep时间来避免消耗过多的CPU。

  1. wait/notify机制

除了上面的场景外,还有一种经典的场景是状态的扫描,例如某线程要等其他线程改变了值后才可继续执行。对于这种场景,最佳的方式是改为采用wait/notify机制。

  1. JVM和内存调优

对于GC频繁造成的CPU us高的现象,则要通过JVM调优或程序调优,降低GC的执行次数。

发生原因:

当sy值高时,表示Linux花费了更多的时间在进行线程切换,Java应用造成这种现象的主要原因是 启动的线程比较多 ,且这些线程多数都处于不断的阻塞(例如锁等待、IO等待状态)和执行状态的变化过程中,这就导致了操作系统要不断地 切换执行的线程,产生大量的上下文切换 。在这种状况下,对Java应用而言,最重要的是找出线程要不断切换状态的原因,可采用的方法为通过 kill -3 javapid]或jstack -l [javapid]的方式 dump 出 Java应用程序的线程信息,查看线程的状态信息以及锁信息,找出等待状态或锁竞争过多的线程。

解决方法:

  1. 减少线程数,降低锁竞争。

CPU sy高的原因 主要是线程的运行状态要经常切换 ,对于这种情况, 最简单的优化方法是减少线程数。

减少线程数是能让sy值下降的,所以 不是线程数越多吞吐量就越高 ,线程数需要设置为合理的值,这要根据应用情况来具体决定,同时使用线程池避免要不断地创建线程。如应用要支撑大量的并发,在减少线程数的情况下最好是增加一个缓冲队列,避免因为线程数的减少造成系统出错率上升。

造成CPU sy高的原因除了启动的线程过多以外,还有一个 重要的原因是线程之间锁竞争激烈,造成了线程状态经常要切换 ,因此尽可能降低线程间的锁竞争也是常见的优化方法。锁竞争降低后,线程的状态切换的次数也就会下降,sy值会相应下降。但值得注意的是,如线程数过多,调优后有可能会造成us值过高,所以合理地设置线程数非常关键。锁竞争更有可能造成系统资源消耗不多,但系统性能不足的现象,因此关于降低线程之间锁竞争的调优技巧放入了后续的章节中进行讲述。

  1. 协程(Coroutine)

除了以上两种情况外,对于分布式Java应用而言, 还有一种典型现象是应用中有较多的网络IO操作或确实需要一些锁竞争机制(例如数据库连接池),但为了能够支撑高的并发量,在Java应用中又只能借助启动更多的线程来支撑 。在这样的情况下当并发量增长到一定程度后,可能会造成CPU sy高的现象,对于这种现象, 可采用协程(Coroutine)来支撑更高的并发量 ,避免并发量上涨后造成CPUJ sy消耗严重、系统load迅速上涨和系统性能下降。

采用协程后,能做到当线程等待数据库执行结果时,就立即释放此线程资源给其他请求,等到数据库执行结果返回后才继续执行。

在Java中目前主要可用于实现协程的框架为Kilim.。在使用Kilim执行一项任务时,并不创建Thread,而是改为创建Task,Task相对于Thread而言就轻量级多了。当此Task要做阻塞动作时,可通过Mailbox.get或Task.,pause来阻塞当前Task,Kilim会保存Task之后执行需要的对象信息,并释放Task执行所占用的线程资源:当Task的阻塞动作完成或被唤醒时,此时Kilim会重新载入Task所需的对象信息,恢复Task的执行,相当于Kilim来承担了线程的调度以及上下文切换动作。这种方式相对原生Thread方式更为轻量,且能够更好地利用CPU,因此可做到仅启动CPU核数的线程数,以及大量的Task来支撑高并发量,Kilim带来的是线程使用率的提升,但同时由于要在JVM堆中保存Task上下文信息,因此在采用Kilim的情况下要消耗更多的内存。

IO类问题

Linux 在操作文件时将数据放入文件缓存区,直到内存不够或系统要释放内存给用户进程使用,因此在查看Linux内存状况时经常会发现可用(free)的物理内存不多,但cached用了很多,这是Linux提升文件IO速度的一种做法。在这样的做法下,如物理空闲内存够用, 通常在 Linux 上只有写文件和第一次读取文件时会产生真正的文件IO 。

文件IO问题

工具和指令

  1. pidstat

第一次读取文件时会产生真正的文件IO。

在 Linux 中,要跟踪线程的文件IO的消耗,主要方法是通过pidstat来查找。

输入如 pidstat -d -t-p [pid] 1 100 类似的命令即可查看线程的IO消耗状况,必须在2.6.20 以上版本的内核中执行才有效,执行后的效果如图所示:

其中KB_rd/s表示每秒读取的KB 数,KB_wr/s表示每秒写入的KB数。

  1. iostat

在没有安装pidstat或内核版本为2.6.20以后的版本的情况下,可通过iostat 来查看,但iostat 只能查看整个系统的文件IO消耗情况,无法跟踪到进程的文件IO消耗状况。

直接输入iostat命令,可查看各个设备的IO历史状况,如图所示:

在上面的几项指标中,其中 Device表示设备卷标名或分区名; tps是每秒的IO请求数,这也是IO消耗情况中值得关注的数字;Blk_read/s是指每秒读的块数量,通常块的大小为512字节;Blk_wrtn/s是指每秒写的块数量;Blk_read是指总共读取的块数量;Blk_wrtn是指总共写入的块数量。

除了上面的方式外,还可通过输入 iostat –x xvda 3 5 这样的方式来定时采样查看IO的消耗状况,当使用上面的命令方式时,其输出信息会比直接输入iostat多一些:

其中值得关注的主要有: rls表示每秒读的请求数; w/s 表示每秒写的请求数; await 表示平均每次IO操作的等待时间,单位为毫秒;avgqu-sz表示等待请求的队列的平均长度;svctm表示平均每次设备执行IO操作的时间;util表示一秒之中有百分之几用于IO操作。

在使用 iostat查看IO的消耗情况时, 首先要关注的是CPU 中的 iowait%所占的百分比 ,当iowait占据了主要的百分比时,就表示要关注IO方面的消耗状况了,这时可以再通过iostat -x这样的方式来详细地查看具体状况。

当文件IO消耗过高时,对于 Java应用最重要的是找到造成文件IO消耗高的代码,寻找的最佳方法为通过pidstat直接找到文件IO操作多的线程。之后结合jstack 找到对应的Java代码,如没有pidstat,也可直接根据 jstack得到的线程信息来分析其中文件IO操作较多的线程。

发生原因:

  1. 并发读写多

Java应用造成文件IO消耗严重主要是多个线程需要进行大量内容写入(例如频繁的日志写入)的动作。

  1. 文件大

磁盘设备本身的处理速度慢,或文件系统慢;或操作的文件本身已经很大也会造成文件IO问题。

解决方法:

  1. 异步写文件

将写文件的 同步动作改为异步动作 ,避免应用由于写文件慢而性能下降太多,例如写日志,可以使用log4j提供的AsyncAppender。

  1. 批量读写

频繁的读写操作对IO消耗会很严重,批量操作将大幅度提升IO操作的性能。

  1. 限流

频繁读写的另外一个调优方式是限流,从而将文件IO消耗控制到一个能接受的范围,例如通常在记录日志时会采用如下方式:

如以上方式不做任何处理,在大量出现异常时,会出现所有的线程都在执行 log.error(…),此时可采取的一个简单策略为统计一段时间内log.error的执行频率。当超过这个频率时,一段时间内不再写log,或塞入一个队列后缓慢地写。

  1. 限制文件大小

操作太大的文件也是造成文件IO效率低的一个原因,因此对于每个输出的文件,都应做大小的限制,在超出最大值后可生成一个新的文件,类似 log4j 中RollingFileAppender的 maxFileSize属性的作用。

网络IO问题

工具和指令

sar

在Linux中可采用sar来分析网络IO的消耗状况,输入 sar -n FULL12,执行后以1秒为频率,总共输出两次网络IO的消耗情况,示例如下。

上面的信息中输出的主要有三部分,第一部分为网卡上成功接包和发包的信息,其报告的信息中主要有rxpck/s、txpck/s、rxbyt/s、txbyts、rxmcst/s;第二部分为网卡上失败的接包和发包的信息,其报告的信息中主要有rxerr/s、txerrls、rxdrop/s、txdrop/s:第三部分为sockets上的统计信息,其报告的信息中主要有tolsck、tcpsck、udpsck、rawsck。关于这些数值的具体含义可通过man sar 来进行了解,对于Java应用而言,使用的主要为tcpsck和l udpsck。

发生原因:读写操作太频繁

由于没办法分析具体每个线程所消耗的网络IO,因此当网络IO消耗高时,对于Java应用而言只能对线程进行dump,查找产生了大量网络IO操作的线程。这些线程的特征是读取或写入网络流,在用Java实现网络通信时,通常要将对象序列化为字节流,进行发送,或读取字节流,并反序列化为对象。这个过程要消耗JVM堆内存,JVMJVM堆的内存大小通常是有限的, 因此Java应用一般不会造成网络IO消耗严重。

解决方法:限流

从程序角度而言,造成网络IO消耗严重的原因主要是同时需要发送或接收的包太多。对于这类情况,常用的调优方法为进行限流,限流通常是限制发送packet 的频率,从而在网络IO消耗可接受的情况下来发送packet。

JVM内存问题

JVM内存消耗过多会导致GC执行频繁,CPU消耗增加,应用线程的执行速度严重下降,甚至造成OutOfMemoryError,最终导致Java进程退出。

对于JVM堆以外的内存方面的消耗,最为值得关注的是 swap的消耗以及物理内存的消耗 ,这两方面的消耗都可基于os提供的命令来查看。

工具和指令

在Linux中可通过 vmstat、sar、top、pidstat 等方式来查看swap和物理内存的消耗状况。

  1. vmstat

在命令行中输入 vmstat ,其中的信息和内存相关的主要是 memory下的 swpd、free、buff、cache以及 swap下的si和 so

其中 swpd是指虚拟内存已使用的部分,单位为kb; free表示空闲的物理内存, buff表示用于缓冲的内存,cache表示用于作为缓存的内存,swap下的si是指每秒从disk读至内存的数据量, so是指每秒从内存中写入disk的数据量。

swpd值过高通常是由于物理内存不够用了,os将物理内存中的一部分数据转为放入硬盘上进行存储,以腾出足够的空间给当前运行的程序使用。这个过程会产生swap IO,因此看swap的消耗情况主要要关注的是swap IO的状况,如 swap IO发生得较频繁,那么会严重影响系统的性能。

  1. sar

通过sar的-r参数可查看内存的消耗状况如图所示,例如sar -r 2 5:

其中和swap相关的信息主要是kbswpfree、kbswpused、%swpused,kbswpfree表示swap空闲的大小,kbswpused表示已使用的swap大小,%swpused表示使用的swap空间比率。

其中和物理内存相关的信息主要是 kbmemfree、kbmemused、%memused、kbbuffers、kbcached ,当物理内存有空闲时,linux会使用一些物理内存用于buffer 以及 cache,以提升系统的运行效率,因此可以认为 系统中可用的物理内存为: kbmemfree+kbbuffers+kbcached。

sar 相比 vmstat的 好处是可以查询历史状况 ,以更加准确地分析趋势状况,例如 sar -r -f/tmp/log/sa/sa12。

vmstat和 sar的 共同弱点是不能分析进程所占用的内存量

  1. top

通过top可查看进程所消耗的内存量,不过 top中看到的Java进程的消耗内存是包括了JVM已分配的内存加上Java应用所耗费的JVM以外的物理内存,这会导致top中看到Java进程所消耗的内存大小有可能超过-Xmx加上-XX:MaxPermSize设置的内存大小,并且java程序在启动后也只是占据了-Xms的地址空间,但并没有占据实际的内存,只有在相应的地址空间被使用过后才会被计入消耗的内存中。因此 纯粹根据 top 很难判断出Java进程消耗的内存中有多少是属于JVM的,有多少是属于消耗JVM外的内存 。一个小技巧是,对由于内存满而发生过Full GC的应用而言(不是主动调用System.gc的应用),多数情况下(例如由于产生的对象过大导致执行Full GC并抛出OutOfMemoryError 的现象就要除外)可以认为其Java进程中显示出来的内存消耗值即为JVM-Xmx的值+消耗的JVM外的内存值。

  1. pidstat

通过pidstat 也可查看进程所消耗的内存量,命令格式为: **pidstat -r-p [pid] [interval] [timcs],例如pidstat -r -p 2013 1 100,**执行此命令后可查看该进程所占用的物理内存和虚拟内存的大小,示例如图所示:

从以上的几个工具来看,最佳的内存消耗分析方法是 结合top或pidstat,以及JVM的内存分析工具来共同分析内存的消耗状况。

堆以外内存问题(物理内存)

基于Direct ByteBuffer可以很容易地实现对物理内存的直接操作,而无须耗费JVM heap区。

发生原因:

  1. 使用了ByteBuffer但未释放

direct bytebuffer消耗的是JVM heap外的物理内存。但它同样是基于GC方式来释放的,同时也可以看出JVM heap一旦使用后,即使进行了GC,进程中仍然会显示之前其所消耗的内存大小,因此JVM内存中具体的消耗状况必须通过jak提供的命令才能准确分析。

  1. 线程创建过多

除了示例中 Direct ByteBuffer方式对JVM外物理内存的消耗外,创建线程也会消耗一定大小的内存。这一方面取决于-Xss对应值的大小,另一方面也取决于线程stack 的深度,当线程退出时,其所占用的内存将自动释放。

解决方法:

  1. 释放不必要的引用

内存消耗严重的情况中最典型的一种现象是代码中持有了不需要的对象引用,造成这些对象无法被GC,从而占据了JVM堆内存。这种情况最典型的一个例子是在复用线程的情况下使用ThreadLocal,由于线程复用,ThreadLocal中存放的对象如未做主动释放的话则不会被GC,代码例子如下:

  1. 使用对象缓存池

创建对象的实例要耗费一定的CPU 以及内存,使用对象缓存池一定程度上可降低JVM Heap内存的使用。

在内存消耗严重的情况下,采用对象缓存池可以大幅度提升性能,避免创建对象所耗费的时间以及频繁GC造成的消耗。

  1. 采用合理的缓存失效算法

上面说到了采用对象缓存池来降低内存的消耗,但如果放入太多的对象在缓存池中,反而会造成内存的严重消耗。同时由于缓存池一直对这些对象持有引用,从而会造成Full GC增多,对于这种状况要合理控制缓存池的大小。

控制缓存池大小的问题在于当到达缓存池的最大容量后,如果要加入新的对象该如何处理,有一些经典的缓存失效算法来清除缓存池中的对象,例如 FIFO、LRU、LFU等。采用这些算法可控制缓存池中的对象数目,避免缓存池中的对象数量无限上涨。

  1. 合理使用SoftReference和WeakReference

对于占据内存但又不是必须存在的对象,例如缓存对象,也可以基于SoftReference或WeakReference的方式来进行缓存。根据之前第3章中的描述,SoftReference 的对象会在内存不够用的时候回收,WeakReference的对象则会在Full GC的时候回收,采用这两种方式也能一定程度上减少JVM Heap区内存的消耗。

资源消耗不多仍然很慢

发生原因

  1. 锁竞争激烈

锁竞争激烈直接就会造成程序执行慢,例如一个典型的例子是数据库连接池,通常数据库连接池提供的连接数都是有限的。假设提供的是10个,那么就意味着同时能够进行数据库操作的就只有10个线程,而如果此时有50个线程要进行数据库操作,那就会造成另外的40个线程处于等待状态,这种情况下对于4核类型的机器而言,CPU的消耗并不会高,但程序的执行仍然会较慢。

  1. 未充分使用硬件资源

例如机器上有双核CPU,但程序中都是单线程串行的操作,并 没有充分发挥硬件资源的作用 ,此时就可进行一定的优化来充分使用硬件资源,提升程序的执行速度。

  1. 数据量增长

数据量增长通常也是造成程序执行慢的典型原因,例如当数据库中单表的数据从100万个上涨到1亿个后,数据库的读写速度将大幅度下降,相应的操作此表的程序的执行速度也就下降了。

对于以上这两种状况,要记录程序执行的整个过程的时间消耗或使用JProfiler等商业工具,从而找到执行耗时比率最大的代码,图为一个基于JProfiler 跟踪代码执行速度的截图:

解决方法

锁的解决方法

  1. 使用并发包中的类

并发包中的类多数都采用了lock-free、nonblocking 算法,减少了多线程情况下资源的锁竞争,因此对于线程间要共享操作的资源而言,应 尽量使用并发包中的类来实现 。如并发包中的类无法满足需求时,可参考学习一些nonblocking 算法来自行实现,nonblocking算法的机制,为基于CAS来做到无需lock就可实现资源一致性的保证,主要的实现nonblocking的算法有 Treiber算法和Michael-Scott非阻塞队列算法。

  1. 尽可能少用锁

尽可能让锁仅在需要的地方出现,通常没必要对整个方法加锁,而只对需要控制的资源做加锁操作。

在编写多线程程序时,要仔细考虑哪些地方是要加锁的,哪些地方是不要加锁的。尽可能让锁最小化,只对互斥及原子操作的地方加锁,加锁时尽可能以被保护资源的最小粒度为单位。例如一个操作中需要保护的资源只有 HashMap,那么在加锁时则可只synchronized(map),而没必要 synchronized(this),并且可以只在对HashMap操作时才加锁。

  1. 拆分锁

拆分锁即把 独占锁拆分为多把锁 ,常见的有读写锁拆分及类似ConcurrentHashMap 中默认拆分为16把锁的方法。拆分锁很大程度上能提高读写的速度,但需要注意的是在采用拆分锁后,全局性质的操作会变得比较复杂,例如ConcurrentHashMap 中 size操作。

是否拆分锁还得根据业务场景来决定。有些场景并不适合做锁的拆分,而且 锁拆分得太多也会造成其他副作用,例如CPU的消耗明显增加等 ,因此锁拆分 要在合理的业务场景以及CPU消耗下进行

  1. 去除读写操作的互斥锁

在修改时加锁,并复制对象进行修改,修改完毕后切换对象的引用,而读取时则不加锁,这种方式称为CopyOnWrite。CopyOnWriteArrayList是CopyOnWrite方法的典型实现,CopyOnWrite 的好处是可以明显提升读的性能,对于读多写少的应用非常适合,但由于写操作时每次都要复制一份对象,会造成更多的内存消耗。

  1. 未充分使用CPU解决方法并行情况下增加线程数

对于此类演变为多线程也无须加锁的场景而言, 启动多个线程后的性能会远高于单线程,并且只要启动的线程数合理,也不会给CPU造成过大的负担 。如从单线程演变为多线程要加锁,则要引入尽量减少锁竞争的方法,并进行性能测试以保证调优后的资源消耗以及性能满足要求。

此种类型的场景还有不少,例如单线程的计算,可以拆分为多线程来分别计算,最后将结果合并,这样的方式也可很大程度提升系统的性能,JDK 7中的fork-join框架可以给以上这类场景提供一个好的支撑方法。

**在CPU资源消耗可接受,且不会因为线程增加带来激烈锁竞争的场景下,应适当对处理过程进行分解,增加线程数从而能并行处理以提升系统的运行性能。**但在重构为并行时,要注意控制内存消耗,而且通过重构为并行能提升的性能也是有限的。著名的Amdahl定律中的简单计算公式为:1/(F+(1-F)/N),其中F为必须串行化的执行在整个执行过程中所占的比率,N为处理器个数,例如在整个执行过程中串行化的过程占50%,那么最多只能提升2倍的性能。

  1. 未充分使用内存/数据量大解决方法

未充分使用内存的场景非常多,如数据的缓存、耗时资源的缓存(例如数据库连接的创建、网络连接的创建等)、页面片段的缓存等,这样的场景比较容易理解,毕竟内存的读取肯定远快于硬盘、网络的读取。但也要避免内存资源的过度使用,在内存资源消耗可接受、GC 频率及系统结构(例如集群环境可能会带来缓存的同步等)可接受的情况下,应充分使用内存来缓存数据,提升系统的性能。

对于数据量大造成的性能不足,在第7章“构建可伸缩的系统”一章中提供了一些优化方案。好的调优策略应是收益比(调优后提升的效果/调优改动所付出的代价)最高的,通常来说功能简单的系统调优比较好做,否则有可能出现调优了当前功能影响到了其他功能,因此应尽量保持单机上应用功能的纯粹性,这是大型系统的基本架构原则。从纯粹的软件调优角度来讲,充分而不过分使用硬件资源,合理调整JVM以及合理使用JDK包是调优的三大有效原则,调优没有“银弹”,结合系统现状和多尝试不同的调优策略是找到合适的调优方法的唯一途径。

的场景比较容易理解,毕竟内存的读取肯定远快于硬盘、网络的读取。但也要避免内存资源的过度使用,在内存资源消耗可接受、GC 频率及系统结构(例如集群环境可能会带来缓存的同步等)可接受的情况下,应充分使用内存来缓存数据,提升系统的性能。

喜欢的话,记得点赞,评论+转发!!!