文章目录
  1. 1. CPU 消耗分析
  2. 2. top
  3. 3. pidstat
  4. 4. us
  5. 5. sy
  6. 6. 对于 CPU 消耗严重的情况

随着系统数据量的不断增长, 访问量的不断提升, 系统的响应通常会越来越慢, 又或是编写的新的应用在性能上无法满足需求, 这个时候需要对系统的性能进行调优, 调优过程是构建高性能应用的必备过程, 也是一个相当复杂的过程, 而且涉及到了很多的方面, 硬件、操作系统、 运行环境软件以及应用本身, 要实现调优, 首先需要做的是找到性能低的根本原因, 然后才是针对性的进行调优, 本章节就来介绍下寻找性能瓶颈以及调优的一些技术上的方法。

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<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 并打开线程查看后看到的状况如下:




以上面最耗 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 值不会太高, 但锁竞争会造成系统性能的下降。

文章目录
  1. 1. CPU 消耗分析
  2. 2. top
  3. 3. pidstat
  4. 4. us
  5. 5. sy
  6. 6. 对于 CPU 消耗严重的情况