随着系统数据量的不断增长, 访问量的不断提升, 系统的响应通常会越来越慢, 又或是编写的新的应用在性能上无法满足需求, 这个时候需要对系统的性能进行调优, 调优过程是构建高性能应用的必备过程, 也是一个相当复杂的过程, 而且涉及到了很多的方面, 硬件、操作系统、 运行环境软件以及应用本身, 要实现调优, 首先需要做的是找到性能低的根本原因, 然后才是针对性的进行调优, 本章节就来介绍下寻找性能瓶颈以及调优的一些技术上的方法。
当 CPU 消耗过高时, 对于多线程的 Java 应用而言, 最明显的性能影响是线程执行业务处理的速度大幅度下降。
在分析 Java 应用中什么动作造成了 CPU 的消耗时, 首先需要找到的为消耗了 较多 CPU资源的线程, 然后根据所消耗的 CPU 的类型并结合线程 dump 来找到造成 CPU 资源消耗高的具体原因。
在 linux 中, 可通过 top 或 pidstat 方式来查看进程中线程的 CPU 的消耗状况。
输入 top 命令后即可查看 CPU 的消耗情况, CPU 的信息在 TOP 视图的上面几行中, 图示如下:
pidstat 是 SYSSTAT 中的工具, 如需使用 pidstat, 请先安装 SYSSTAT。
输入 pidstat 1 2, 在 console 上将会每隔 1 秒输出目前活动进程的 CPU 消耗状况, 共输出 2 次, 图示如下:
当 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<String> list = new ArrayList<String>();
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 并打开线程查看后看到的状况如下:
"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 值高时, 表示系统调用耗费了较多的 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 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 查看效果: