随着系统数据量的不断增长, 访问量的不断提升, 系统的响应通常会越来越慢, 又或是编写的新的应用在性能上无法满足需求, 这个时候需要对系统的性能进行调优, 调优过程是构建高性能应用的必备过程, 也是一个相当复杂的过程, 而且涉及到了很多的方面, 硬件、操作系统、 运行环境软件以及应用本身, 要实现调优, 首先需要做的是找到性能低的根本原因, 然后才是针对性的进行调优, 本章节就来介绍下寻找性能瓶颈以及调优的一些技术上的方法。
CPU 消耗分析
当 CPU 消耗过高时, 对于多线程的 Java 应用而言, 最明显的性能影响是线程执行业务处理的速度大幅度下降。
在分析 Java 应用中什么动作造成了 CPU 的消耗时, 首先需要找到的为消耗了 较多 CPU资源的线程, 然后根据所消耗的 CPU 的类型并结合线程 dump 来找到造成 CPU 资源消耗高的具体原因。
在 linux 中, 可通过 top 或 pidstat 方式来查看进程中线程的 CPU 的消耗状况。
top
输入 top 命令后即可查看 CPU 的消耗情况, CPU 的信息在 TOP 视图的上面几行中, 图示如下:
在此需要关注的为 Cpu 那行的信息, 其中 4.0% us 表示的为用户占用了 4%的 CPU 时间,主要为所运行的应用程序对 CPU 的消耗; 8.9% sy 表示的为系统占用了 8.9%的 CPU 时间, 主要为系统切换所消耗的 CPU; 0.0% ni 表示被 nice 命令改变优先级的任务所占用的 CPU 时间的百分比; 87.0% id 表示 CPU 的空闲时间所占的百分比为 87%; 0.0% wa 表示的为在执行的过程中等待 IO 所占用的 CPU 的百分比为 0%; 0.2% hi 表示的为硬件中断所占用的 CPU 时间百分比为 0.2%; 0.0% si 表示的为软件中断所占用的 CPU 时间的百分比为 0.0%。对于多个或多核的 CPU, 上面的显示则会是多个 CPU 所占用的百分比的总和, 因此会
出现 160% us 这样的现象, 如需查看每个核的消耗情况, 可在进入 top 视图后按 1, 就会按核来显示消耗情况, 如下图所示:
当 CPU 消耗过多时, 体现为 us 或 sy 值变大。
默认情况下, TOP 视图中显示的为进程的 CPU 消耗状况, 在 TOP 视图中按 shift+h 后,可查看线程的 CPU 消耗状况, 图示如下:
此时的 PID 即为线程 ID, 其后的%CPU 表示该线程所消耗的 CPU 百分比。
pidstat
pidstat 是 SYSSTAT 中的工具, 如需使用 pidstat, 请先安装 SYSSTAT。
输入 pidstat 1 2, 在 console 上将会每隔 1 秒输出目前活动进程的 CPU 消耗状况, 共输出 2 次, 图示如下:
其中 CPU 表示的为当前进程所使用到的 CPU 个数, 如需查看某进程中线程的 CPU 消耗状况, 可输入 pidstat –p [PID] –t 1 5 这样的方式来查看, 执行后的图示如下:
图中的 TID 即为线程 ID。
通过上面的方式可查找到 Java 进程中哪个线程消耗了 CPU, CPU 的消耗主要又分为了us 和 sy 两种, Java 应用造成这两个值高的原因不太相同, 分别来看看。
us
当 us 值高时, 表示运行的应用程序消耗了大部分的 CPU。
首先通过 top 或 pidstat 的方式找到消耗 CPU 的线程 ID, 并将此线程 ID 转化为十六进制的值, 之后通过 kill -3 或 jstack 的方式 dump 出应用的 java 线程信息, 通过之前转化出的十六进制的值找到对应的 nid 的线程, 该线程即为消耗 CPU 的线程, 以上过程需要多操作几次,
以确保找到真实的消耗 CPU 的线程, 对于 Java 应用而言, 多数情况下是由于该线程中执行的动作不需要进入过多的 IO 等待、 锁等待或睡眠状态等现象, 以下为一个示例这种状况的代码。
public static void main(String[] args) throws Exception
{
Demo demo = new Demo();
demo.runTest();
}
private void runTest() throws Exception
{
int count = Runtime. getRuntime().availableProcessors();
for (int i = 0; i < count; i++)
{
new Thread(new ConsumeCPUTask()).start();
}
for (int i = 0; i < 200; i++)
{
new Thread(new NotConsumeCPUTask()).start();
}
}
class ConsumeCPUTask implements Runnable
{
public void run()
{
String
str = "fwljfdsklvnxcewewrewrew12wre5rewf1ew2few4few2few2few3few3few5fsd
1sdewu3249gdfkvdvx" +
"wefsdjfewvmdxlvdsfofewmvdmvfd;lvds;vds;vdsvdsxcnzgewgdfuvxmvx.;f
" +
"fsaffsdjlvcx.vcxgdfjkf;dsfdas#vdsjlfdsmv.xc.vcxjk;fewipvdmsvzlfs
jlf;afdjsl;fdsp[euiprenvs" +
"fsdovxc.vmxceworupg;";
float i = 0.002f;
float j = 232.13243f;
while (true)
{
j = i * j;
str.indexOf("#");
ArrayList list = new ArrayList();
for (int k = 0; k < 10000; k++)
{
list.add(str + String. valueOf(k));
}
list.contains("iii");
try
{
Thread. sleep(10);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
class NotConsumeCPUTask implements Runnable
{
public void run()
{
while (true)
{
try
{
Thread. sleep(10000000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
在linux 机器上运行上面的程序, top 并打开线程查看后看到的状况如下:
以上面最耗 CPU 的线程 26697 为例, 将 26697 换算成十六进制的值, 结合 java threaddump( jstack [pid] | grep ‘nid=0x6849’) 找到此线程为:
"Thread-1" prio=10 tid=0x706cc400 nid=0x6849 runnable [0x6fd8d000]
java.lang.Thread.State: RUNNABLE
at chapter6.Demo$ConsumeCPUTask.run(Demo.java:36)
at java.lang.Thread.run(Thread.java:619)
从上可以看到, 主要是 ConsumeCPUTask 的执行消耗了 CPU。
总结来说, 当 us 值高时, 主要是由于启动的 Java 线程一直在执行( 例如循环执行),并且线程中所执行的步骤不太需要等待 IO 或进入 sleep、 wait 等状态, 又或者是启动的线程很多, 当一个线程 sleep、 wait 后, 其他的又在运行。
sy
当 sy 值高时, 表示系统调用耗费了较多的 CPU, 对于 Java 应用程序而言, 造成这种现象的主要原因是启动的线程比较多, 并且这些线程多数都处于不断的等待(例如锁等待状态)和执行状态的变化过程中, 这就导致了操作系统要不断的调度这些线程, 切换执行, 以下为一个示例这种状况的代码。
private static int threadCount = 500;
/**
*@param args
*/
public static void main(String[] args) throws Exception
{
if(args. length == 1)
{
threadCount = Integer. parseInt(args[0]);
}
SyHighDemo demo = new SyHighDemo();
demo.runTest();
}
private Random random = new Random();
private Object[] locks;
private void runTest() throws Exception
{
locks = new Object[threadCount];
for (int i = 0; i < threadCount; i++)
{
locks[i] = new Object();
}
for (int i = 0; i < threadCount; i++)
{
new Thread(new ATask(i)).start();
new Thread(new BTask(i)).start();
}
}
class ATask implements Runnable
{
private Object lockObject = null;
public ATask(int i)
{
lockObject = locks[i];
}
public void run()
{
while (true)
{
try
{
synchronized (lockObject)
{
lockObject.wait(random.nextInt(10));
}
}
catch (Exception e)
{
;
}
}
}
}
class BTask implements Runnable
{
private Object lockObject = null;
public BTask(int i)
{
lockObject = locks[i];
}
public void run()
{
while (true)
{
synchronized (lockObject)
{
lockObject.notifyAll();
}
try
{
Thread. sleep(random.nextInt(5));
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
执行以上代码, 结合 sar 查看 CPU 的消耗状况, 可看到类似如下的状况:
从上面可以看出, CPU 更多的是消耗在了系统调用上。
当 CPU 更多的是消耗在系统调用时, 对于 Java 应用而言, 主要是由于线程数太多以及线程状态切换过于频繁造成的。
根据资源的消耗情况以及分析, 对程序的实现进行一定的调优, 下面就来看看常用的一些调优方法。
对于 CPU 消耗严重的情况
根据之前的分析, CPU us 高的原因主要是执行线程不需要任何挂起动作, 且一直执行,导致 CPU 没有机会去调度执行其他的线程, 对于这种情况, 常见的一种优化方法是对这种线程的动作增加 Thread.sleep, 以释放 CPU 的执行权, 降低 CPU 的消耗。
按照这样的思想, 对 CPU 消耗章节中的例子进行修改, 在往集合中增加元素的部分增加 sleep, 修改如下:
for (int k = 0; k < 10000; k++)
{
list.add(str + String. valueOf(k));
if(k % 50 == 0)
{
try
{
Thread.sleep(1);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
重新执行以上代码, 通过 top 查看效果:
从上结果可见, CPU 的消耗大幅度下降, 当然, 这种修改方式是以损失单次执行性能为代价的, 但由于其降低了 CPU 的消耗, 对于多线程的应用而言, 反而提高了总体的平均性
能。
在实际的 Java 应用中会有很多类似的场景, 例如多线程的任务执行管理器, 其通常需要通过扫描任务集合列表来执行任务, 对于这些类似的场景, 都可通过增加一定的 sleep 时间来避免消耗过多的 CPU。
除了上面的场景外, 还有一种经典的场景是状态的扫描, 例如某线程需要等待其他线程改变了值后才可继续执行, 对于这种场景, 最佳的方式是改为采用 wait/notify 机制。
CPU sy 高的原因主要是线程的运行状态要经常切换, 对于这种情况, 常见的一种优化方法是减少线程数。
按照这样的思想, 将 CPU 资源消耗中的例子重新执行, 将线程数降低, 传入参数 100,执行结果如下:
可见减少线程数是能让 sy 值下降的, 所以不是说线程数越多吞吐量就越高的, 线程数需要设置为合理的值, 这需要根据应用情况来具体决定, 同时使用线程池避免线程需要不断的创建。
除了减少线程数外, 尽可能的降低线程间的锁竞争也是常见的优化方法, 锁竞争降低后,线程的状态切换的次数也就会下降, sy 值也将下降, 但值得注意的是, 如果线程数太多的话, 调优后有可能会造成 us 值过高, 因此合理的设置线程数非常关键, 在线程数以及锁不是很多的情况下, sy 值不会太高, 但锁竞争会造成系统性能的下降。