深入理解JVM—性能调优

 在上文中我们分析了很多性能监控工具,介绍这些工具的目的只有一个,那就是找出对应的性能瓶颈。盲目的性能调优是没有效果的,只有充分知道了哪里出了问题,针对性的结果才是立竿见影的。解决了主要的性能问题,那些次要的性能问题也就不足为虑了!

我们知道,性能问题无非就这么几种:CPU、内存、磁盘IO、网络。那我们来逐一介绍以下相关的现象和一些可能出现的问题。

一、CPU过高。

查看CPU最简单的我们使用任务管理器查看,如下图所示,windows下使用任务管理器查看,Linux下使用top查看。

一般我们的服务器都采用Linux,因此我们重点关注一下Linux(注:windows模式下相信大家已经很熟悉了,并且前面我们已经提到,使用资源监视器可以很清楚的看到系统的各项参数,在这里我就不多做介绍了)

top视图下,对于多核的CPU,显示的CPU资源有可能超过100%,因为这里显示的是所有CPU占用百分百的总和,如果你需要看单个CPU的占用情况,直接按键1就可以看到。如下图所示,我的一台测试机为816GB内存。

 在

top 视图下,按键 shift+h 后,会显示各个线程的 CPU 资源消耗情况,如下图所示:

 我们也可以通过

sysstat 工具集的 pidstat 来查看

注:sysstat下载地址:http://sebastien.godard.pagesperso-orange.fr/download.html

安装方法:

1chmod +x configure

2./configure

3make

4make install

如输入pidstat 1 2就会隔一秒在控制台输出一次当然CPU的情况,共输出2

 除了

top pidstat 以外, vmstat 也可以进行采样分析

 相关

top pidstat mstat 的用法大家可以去网上查找。

下面我们主要来介绍以下当出现CPU过高的时候,或者CPU不正常的时候,我们该如何去处理?

CPU消耗过高主要分为用户进程占用CPU过高和内核进程占用CPU过高(在Linuxtop视图下us指的是用户进程,而sy是指内核进程),我们来看一个案例:

 程序运行前,系统运行平稳,其中蓝色的线表示总的

CPU 利用率,而红色的线条表示内核使用率。部署 war 测试程序,运行如下图所示:

 对于一个

web 程序,还没有任何请求就占用这么多 CPU 资源,显然是不正常的。并且我们看到,不是系统内核占用的大量 CPU ,而是系统进程,那是哪一个进程的呢?我们来看一下。

 很明显是我们的

java 进程,那是那个地方导致的呢?这就需要用到我们之前提到的性能监控工具。在此我们使用可视化监控工具 VisualVM

 首先我们排除了是

GC 过于频繁而导致大 CPU 过高,因为很明显监控视图上没有 GC 的活动。然后我们打开 profilter 去查看以下,是那个线程导致了 CPU 的过高?

 前面一些线程都是容器使用的,而下面一个线程也一直在执行,那是什么地方调用的呢?查找代码中使用

ThredPoolExecutor 的地方。终于发现以下代码。

    private BlockingQueue queue;

    private Executor executor;

//……

public void run() {

        while(true){

           try {

              SendMsg sendMsg = queue.poll();//从队列中取出

              if(null != sendMsg) {

                  sendForQueue(sendMsg);

              }

           } catch (Exception e) {

              e.printStackTrace();

           }

       }

    }

问题很显然了,我们看一下对应BlockingQueuepoll方法的API文档。

 不难理解了,虽然使用了阻塞的队列,但是使用了非阻塞的取法,当数据为空时直接返回

null ,那这个语句就等价于下面的语句。

@Override

    public void run() {

       while(true){

          

       }

    }

相当于死循环么,很显然是非常耗费CPU资源的,并且我们还可以发现这样的死循环是耗费的单颗CPU资源,因此可以解释上图为啥有一颗CPU占用特别高。我们来看一下部署在Linux下的top视图。

 猛一看,不是很高么?我们按键

1 来看每个单独 CPU 的情况!

 这下看的很清楚了吧!明显一颗

CPU 被跑满了。(因为一个单独的死循环只能用到一颗 CPU ,都是单线程运行的)。

问题找到,马上修复代码为阻塞时存取,如下所示:

@Override

    public void run() {

       while(true){

           try {

              SendMsg sendMsg = queue.take();//从队列中取出

              sendForQueue(sendMsg);

           } catch (Exception e) {

              e.printStackTrace();

           }

       }

    }

 再来监控

CPU 的变换,我们可以看到,基本上不消耗 CPU 资源(是我没做任何的访问哦,有用户建立线程就会消耗)。

 再来看

java 进程的消耗,基本上不消耗 CPU 资源

 

再来看VisualVM的监控,我们就可以看到基本上都是容器的一些线程了

 以上示例展示了

CPU 消耗过高情况下用户线程占用特别高的情况。也就是 Linux top 视图中 us 比较高的情况。发生这种情况的原因主要有以下几种:程序不停的在执行无阻塞的循环、正则或者纯粹的数学运算、 GC 特别频繁。

CPU过高还有一种情况是内核占用CPU很高。我们来看另外一个示例。

package com.yhj.jvm.monitor.cpu.sy;

 

import java.util.Random;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

 

/**

 * @Described:系统内核占用CPU过高测试用例

 * @author YHJ create at 2012-3-28 下午05:27:33

 * @FileNmae com.yhj.jvm.monitor.cpu.sy.SY_Hign_TestCase.java

 */

public class SY_Hign_TestCase {

   

    private final static int LOCK_COUNT = 1000;

 

    //默认初始化LOCK_COUNT个锁对象

    private Object [] locks = new Object[LOCK_COUNT];

 

    private Random random = new Random();

 

    //构造时初始化对应的锁对象

    public SY_Hign_TestCase() {

       for(int i=0;i<LOCK_COUNT;++i)

           locks[i]=new Object();

    }

 

 

 

    abstract class Task implements Runnable{

 

       protected Object lock;

 

       public Task(int index) {

           this.locklocks[index];

       }

       @Override

       public void run() {

           while(true){  //循环执行自己要做的事情

              doSth();

           }

       }

       //做类自己要做的事情

       public abstract void doSth();

    }

 

    //任务休眠自己的锁

    class TaskA extends Task{

 

       public TaskA(int index) {

           super(index);

       }

 

       @Override

       public void doSth() {

           synchronized (lock) {

              try {

                  lock.wait(random.nextInt(10));

              } catch (InterruptedException e) {

                  e.printStackTrace();

              }

           }

       }

 

    }

 

    //任务唤醒所有锁

    class TaskB extends Task{

      

       public TaskB(int index) {

           super(index);

        }

 

       @Override

       public void doSth() {

           try {

              synchronized (lock) {

                  lock.notifyAll();

                  Thread.sleep(random.nextInt(10));

              }

           } catch (InterruptedException e) {

              e.printStackTrace();

           }

       }

 

    }

    //启动函数

    public void start(){

       ExecutorService service = Executors.newCachedThreadPool();

       for(int i=0;i<LOCK_COUNT;++i){

           service.execute(new TaskA(i));

           service.execute(new TaskB(i));

       }

    }

    //主函数入口

    public static void main(String[] args) {

       new SY_Hign_TestCase().start();

    }

 

}

代码很简单,就是创建了2000个线程,让一定的线程去等待,另外一个线程去释放这些资源,这样就会有大量的线程切换,我们来看下效果。

 很明显,

CPU 的内核占用率很高,我们拿具体的资源监视器看一下:

 很明显可以看出有很多线程切换占用了大量的

CPU 资源。

同样的程序部署在Linux下,top视图如下图所示:

 展开对应的

CPU 资源,我们可以清晰的看到如下情形:

 大家可以看到有大量的

sy 内核占用,但是也有不少的 us us 是因为我们启用了大量的循环,而 sy 是因为大量线程切换导致的。

我们也可以使用vmstat来查看,如下图所示:

 二、文件

IO 消耗过大,磁盘队列高。

windows环境下,我们可以使用资源监视器查看对应的IO消耗,如下图所示:

 这里不但可以看到当前磁盘的负载信息,队列详情,还能看到每个单独的进程的资源消耗情况。

Linux下主要使用pidstatiostat等进行分析。如下图所示

Pidstat –d –t –p [pid] {time} {count}

如:pidstat -d -t -p 18720 1 1

Iostat

 

Iostat –x xvda 1 10做定时采样

 废话不多说,直接来示例,上干货!

package com.yhj.jvm.monitor.io;

 

import java.io.BufferedWriter;

import java.io.FileWriter;

import java.io.IOException;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

 

/**

 * @DescribedIO测试用例

 * @author YHJ create at 2012-3-29 上午09:56:06

 * @FileNmae com.yhj.jvm.monitor.io.IO_TestCase.java

 */

public class IO_TestCase {

   

    private String fileNmae = "monitor.log";

   

    private String context ;

   

    // CPU处理器个数相同,既充分利用CPU资源,又导致线程频繁切换

    private final static int THRED_COUNT = Runtime.getRuntime().availableProcessors();

   

    public IO_TestCase() {//加长写文件的内容,拉长每次写入的时间

       StringBuilder sb = new StringBuilder();

       for(int i=0;i<1000;++i){

           sb.append("context index :")

           .append(i)

           .append("\n");

           this.contextnew String(sb);

       }

    }

    //写文件任务

    class Task implements Runnable{

 

       @Override

       public void run() {

           while(true){

              BufferedWriter writer = null;

              try {

                  writer = new BufferedWriter(new FileWriter(fileNmae,true));//追加模式

                  writer.write(context);

              } catch (Exception e) {

                  e.printStackTrace();

              }finally{

                  try {

                     writer.close();

                  } catch (IOException e) {

                     e.printStackTrace();

                  }

              }

           }

          

       }

    }

    //启动函数

    public void start(){

       ExecutorService service = Executors.newCachedThreadPool();

       for(int i=0;i<THRED_COUNT;++i)

           service.execute(new Task());

    }

    //主函数入口

    public static void main(String[] args) {

       new IO_TestCase().start();

    }

 

}

这段示例很简单,通过创建一个和CPU个数相同的线程池,然后开启这么多线程一起读写同一个文件,这样就会因IO资源的竞争而导致IO的队列很高,如下图所示:

 关掉之后马上就下来了

 我们把这个部署到

Linux 上观看。

 这里的

%idle 指的是系统没有完成写入的数量占用 IO 总量的百分百,为什么这么高我们的系统还能承受?因为我这台机器的内存为 16GB 的,我们来查看以下 top 视图就可以清晰的看到。

 占用了大量的内存资源。

三、内存消耗

对于JVM的内存模型大家已经很清楚了,前面我们讲了JVM的性能监控工具。对于Java应用来说,出现问题主要消耗在于JVM的内存上,而JVM的内存,JDK已经给我们提供了很多的工具。在实际的生成环境,大部分应用会将-Xms-Xmx设置为相同的,避免运行期间不断开辟内存。

对于内存消耗,还有一部分是直接物理内存的,不在堆空间,前面我们也写过对应的示例。之前一个系统就是因为有大量的NIO操作,而NIO是使用物理内存的,并且开辟的物理内存是在触发FULL GC的时候才进行回收的,但是当时的机器总内存为16GB 给堆的内存是14GB Edon1.5GB,也就是实际剩下给物理呢哦村的只有0.5GB,最终导致总是发生内存溢出,但监控堆、栈的内存消耗都不大。在这里我就不多写了!

四、网络消耗过大

Windows下使用本地网络视图可以监控当前的网络流量大小

 更详细的资料可以打开资源监视器,如下图所示

 Linux

平台可以使用以下 sar 命令查看

sar -n DEV 1 2

 字段说明:

rxpck/s:每秒钟接收的数据包

txpck/s:每秒钟发送的数据包

rxbyt/s:每秒钟接收的字节数

txbyt/s:每秒钟发送的字节数

rxcmp/s:每秒钟接收的压缩数据包

txcmp/s:每秒钟发送的压缩数据包

rxmcst/s:每秒钟接收的多播数据包

Java程序一般不会出现网络IO导致问题,因此在这里也不过的的阐述。

五、程序执行缓慢

CPU、内存、磁盘、网络都不高,程序还是执行缓慢的话,可能引发的原因大致有以下几种:

1程序锁竞争过于激烈,比如你只有2CPU,但是你启用了200个线程,就会导致大量的线程等待和切换,而这不会导致CPU很高,但是很多线程等待意味着你的程序运行很慢。

2未充分利用硬件资源。比如你的机器是16个核心的,但是你的程序是单线程运行的,即使你的程序优化的很好,当需要处理的资源比较多的时候,程序还会很慢,因此现在都在提倡分布式,通过大量廉价的PC机来提升程序的执行速度!

3其他服务器反应缓慢,如数据库、缓存等。当大量做了分布式,程序CPU负载都很低,但是提交给数据库的sql无法很快执行,也会特别慢。

总结一下,当出现性能问题的时候我们该怎么做?

一、CPU过高

1、  us过高

使用监控工具快读定位哪里有死循环,大计算,对于死循环通过阻塞式队列解决,对于大计算,建议分配单独的机器做后台计算,尽量不要影响用户交互,如果一定要的话(如框计算、云计算),只能通过大量分布式来实现

2、  sy过高

最有效的方法就是减少进程,不是进程越多效率越高,一般来说线程数和CPU的核心数相同,这样既不会造成线程切换,又不会浪费CPU资源

二、内存消耗过高

1、  及时释放不必要的对象

2、  使用对象缓存池缓冲

3、  采用合理的缓存失效算法(还记得我们之前提到的弱引用、幽灵引用么?)

三、磁盘IO过高

1、  异步读写文件

2、  批量读写文件

3、  使用缓存技术

4、  采用合理的文件读写规则

四、网络

1、增加宽带流量

五、资源消耗不多但程序运行缓慢

1、使用并发包,减少锁竞争

2、对于必须单线程执行的使用队列处理

3、大量分布式处理

六、未充分利用硬件资源

1、  修改程序代码,使用多线程处理

2、  修正外部资源瓶颈,做业务拆分

3、  使用缓存

你可能感兴趣的:(Java虚拟机)