我们知道,性能问题无非就这么几种:CPU、内存、磁盘IO、网络。那我们来逐一介绍以下相关的现象和一些可能出现的问题。
一、CPU过高。
查看CPU最简单的我们使用任务管理器查看,如下图所示,windows下使用任务管理器查看,Linux下使用top查看。
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/ed1ee5a10e911ba21d204f1e709cd74f.jpeg)
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/1d1493e283c90715b2997c871182fd2e.jpeg)
一般我们的服务器都采用Linux,因此我们重点关注一下Linux(注:windows模式下相信大家已经很熟悉了,并且前面我们已经提到,使用资源监视器可以很清楚的看到系统的各项参数,在这里我就不多做介绍了)
在top视图下,对于多核的CPU,显示的CPU资源有可能超过100%,因为这里显示的是所有CPU占用百分百的总和,如果你需要看单个CPU的占用情况,直接按键1就可以看到。如下图所示,我的一台测试机为8核16GB内存。
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/55c3cb0bbc66cb39072f96a61300dafb.jpeg)
在
top 视图下,按键 shift+h 后,会显示各个线程的 CPU 资源消耗情况,如下图所示:![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/d35d5f93abef15d666b7486cfa9bf177.jpeg)
我们也可以通过
sysstat 工具集的 pidstat 来查看注:sysstat下载地址:http://sebastien.godard.pagesperso-orange.fr/download.html
安装方法:
1、chmod +x configure
2、./configure
3、make
4、make install
如输入pidstat 1 2就会隔一秒在控制台输出一次当然CPU的情况,共输出2次
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/85757e7893d6390265815e81a5f1eec2.jpeg)
除了
top 、 pidstat 以外, vmstat 也可以进行采样分析![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/9bd70e73f76ced39140492cc6f2cba72.jpeg)
相关
top 、 pidstat 、 mstat 的用法大家可以去网上查找。下面我们主要来介绍以下当出现CPU过高的时候,或者CPU不正常的时候,我们该如何去处理?
CPU消耗过高主要分为用户进程占用CPU过高和内核进程占用CPU过高(在Linux下top视图下us指的是用户进程,而sy是指内核进程),我们来看一个案例:
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/84c1ccbf420ba188800d6e9e0f69e766.jpeg)
程序运行前,系统运行平稳,其中蓝色的线表示总的
CPU 利用率,而红色的线条表示内核使用率。部署 war 测试程序,运行如下图所示:![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/cd59d3f929f13c3dce0ba92d6f18802e.jpeg)
对于一个
web 程序,还没有任何请求就占用这么多 CPU 资源,显然是不正常的。并且我们看到,不是系统内核占用的大量 CPU ,而是系统进程,那是哪一个进程的呢?我们来看一下。![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/1e288ee9b759c1b8c8ae8c3fbf0dc1ec.jpeg)
很明显是我们的
java 进程,那是那个地方导致的呢?这就需要用到我们之前提到的性能监控工具。在此我们使用可视化监控工具 VisualVM 。![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/ec45ddf2da3b315604883aef027f04cb.jpeg)
首先我们排除了是
GC 过于频繁而导致大 CPU 过高,因为很明显监控视图上没有 GC 的活动。然后我们打开 profilter 去查看以下,是那个线程导致了 CPU 的过高?![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/917a3456c41237ce24abc6cf17885c7b.jpeg)
前面一些线程都是容器使用的,而下面一个线程也一直在执行,那是什么地方调用的呢?查找代码中使用
ThredPoolExecutor 的地方。终于发现以下代码。private BlockingQueue queue;
private Executor executor;
//……
publicvoid run() {
while(true){
try {
SendMsg sendMsg = queue.poll();//从队列中取出
if(null != sendMsg) {
sendForQueue(sendMsg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
问题很显然了,我们看一下对应BlockingQueue的poll方法的API文档。
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/d4fd5c64dfc3829164efb3a2561ef936.jpeg)
不难理解了,虽然使用了阻塞的队列,但是使用了非阻塞的取法,当数据为空时直接返回
null ,那这个语句就等价于下面的语句。@Override
publicvoid run() {
while(true){
}
}
相当于死循环么,很显然是非常耗费CPU资源的,并且我们还可以发现这样的死循环是耗费的单颗CPU资源,因此可以解释上图为啥有一颗CPU占用特别高。我们来看一下部署在Linux下的top视图。
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/b0538062d09554feabd17cb29289a7e6.jpeg)
猛一看,不是很高么?我们按键
1 来看每个单独 CPU 的情况!![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/0a247a53b158193fc253a94224a47424.jpeg)
这下看的很清楚了吧!明显一颗
CPU 被跑满了。(因为一个单独的死循环只能用到一颗 CPU ,都是单线程运行的)。问题找到,马上修复代码为阻塞时存取,如下所示:
@Override
publicvoid run() {
while(true){
try {
SendMsg sendMsg = queue.take();//从队列中取出
sendForQueue(sendMsg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/c5d4f8a77cff8fed0ae8de12a82b8dcc.jpeg)
再来监控
CPU 的变换,我们可以看到,基本上不消耗 CPU 资源(是我没做任何的访问哦,有用户建立线程就会消耗)。![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/845aba432c0ee9456e8098314aa948c1.jpeg)
再来看
java 进程的消耗,基本上不消耗 CPU 资源![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/72795a7105dfb3580644457efdba921d.jpeg)
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/eadd3b52dc5918ddf882a81ff40dbdef.jpeg)
再来看VisualVM的监控,我们就可以看到基本上都是容器的一些线程了
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/cfc35f4306581bd0874b03645133676e.jpeg)
以上示例展示了
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
*/
publicclass SY_Hign_TestCase {
privatefinalstaticintLOCK_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();
}
abstractclass Task implements Runnable{
protected Object lock;
public Task(int index) {
this.lock= locks[index];
}
@Override
publicvoid run() {
while(true){ //循环执行自己要做的事情
doSth();
}
}
//做类自己要做的事情
publicabstractvoid doSth();
}
//任务A 休眠自己的锁
class TaskA extends Task{
public TaskA(int index) {
super(index);
}
@Override
publicvoid doSth() {
synchronized (lock) {
try {
lock.wait(random.nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//任务B 唤醒所有锁
class TaskB extends Task{
public TaskB(int index) {
super(index);
}
@Override
publicvoid doSth() {
try {
synchronized (lock) {
lock.notifyAll();
Thread.sleep(random.nextInt(10));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//启动函数
publicvoid start(){
ExecutorService service = Executors.newCachedThreadPool();
for(int i=0;i<LOCK_COUNT;++i){
service.execute(new TaskA(i));
service.execute(new TaskB(i));
}
}
//主函数入口
publicstaticvoid main(String[] args) {
new SY_Hign_TestCase().start();
}
}
代码很简单,就是创建了2000个线程,让一定的线程去等待,另外一个线程去释放这些资源,这样就会有大量的线程切换,我们来看下效果。
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/ca81debe84c5074d61b212a63d8244bc.jpeg)
很明显,
CPU 的内核占用率很高,我们拿具体的资源监视器看一下:![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/dc332fe716e9da3a558bd3863e069694.jpeg)
很明显可以看出有很多线程切换占用了大量的
CPU 资源。同样的程序部署在Linux下,top视图如下图所示:
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/7305b3c94573075c7bb60314d62376ea.jpeg)
展开对应的
CPU 资源,我们可以清晰的看到如下情形:![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/940aa12c4e1004242375aafba01b9dcc.jpeg)
大家可以看到有大量的
sy 内核占用,但是也有不少的 us , us 是因为我们启用了大量的循环,而 sy 是因为大量线程切换导致的。我们也可以使用vmstat来查看,如下图所示:
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/06de4e984b28c40e4ca0dcec13c88e76.jpeg)
二、文件
IO 消耗过大,磁盘队列高。在windows环境下,我们可以使用资源监视器查看对应的IO消耗,如下图所示:
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/5b630fc13656504420dc303684c8c558.jpeg)
这里不但可以看到当前磁盘的负载信息,队列详情,还能看到每个单独的进程的资源消耗情况。
Linux下主要使用pidstat、iostat等进行分析。如下图所示
Pidstat –d –t –p [pid] {time} {count}
如:pidstat -d -t -p 18720 1 1
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/ca3a39efe67d3d30ed57110ae6a1c64f.jpeg)
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/12ff28c74164c4abd48eea8083b3aa51.jpeg)
Iostat
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/33e24d3e5aaa90e3d8c660423ac3b294.jpeg)
Iostat –x xvda 1 10做定时采样
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/aa3a1a3c337dc939fa348c52705f74de.jpeg)
废话不多说,直接来示例,上干货!
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;
/**
* @Described:IO测试用例
* @author YHJ create at 2012-3-29 上午09:56:06
* @FileNmae com.yhj.jvm.monitor.io.IO_TestCase.java
*/
publicclass IO_TestCase {
private String fileNmae = "monitor.log";
private String context ;
// 和CPU处理器个数相同,既充分利用CPU资源,又导致线程频繁切换
privatefinalstaticintTHRED_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.context= new String(sb);
}
}
//写文件任务
class Task implements Runnable{
@Override
publicvoid 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();
}
}
}
}
}
//启动函数
publicvoid start(){
ExecutorService service = Executors.newCachedThreadPool();
for(int i=0;i<THRED_COUNT;++i)
service.execute(new Task());
}
//主函数入口
publicstaticvoid main(String[] args) {
new IO_TestCase().start();
}
}
这段示例很简单,通过创建一个和CPU个数相同的线程池,然后开启这么多线程一起读写同一个文件,这样就会因IO资源的竞争而导致IO的队列很高,如下图所示:
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/1d230782ce916755d042c8a858ac668c.jpeg)
关掉之后马上就下来了
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/e2b1d42309494e8afba0cd08f5d3fe79.jpeg)
我们把这个部署到
Linux 上观看。![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/fc526201d35da7355b1f9733d35a75f1.jpeg)
这里的
%idle 指的是系统没有完成写入的数量占用 IO 总量的百分百,为什么这么高我们的系统还能承受?因为我这台机器的内存为 16GB 的,我们来查看以下 top 视图就可以清晰的看到。![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/503dbc6218fb92cfdef01debd4995304.jpeg)
占用了大量的内存资源。
三、内存消耗
对于JVM的内存模型大家已经很清楚了,前面我们讲了JVM的性能监控工具。对于Java应用来说,出现问题主要消耗在于JVM的内存上,而JVM的内存,JDK已经给我们提供了很多的工具。在实际的生成环境,大部分应用会将-Xms和-Xmx设置为相同的,避免运行期间不断开辟内存。
对于内存消耗,还有一部分是直接物理内存的,不在堆空间,前面我们也写过对应的示例。之前一个系统就是因为有大量的NIO操作,而NIO是使用物理内存的,并且开辟的物理内存是在触发FULL GC的时候才进行回收的,但是当时的机器总内存为16GB 给堆的内存是14GB Edon为1.5GB,也就是实际剩下给物理呢哦村的只有0.5GB,最终导致总是发生内存溢出,但监控堆、栈的内存消耗都不大。在这里我就不多写了!
四、网络消耗过大
Windows下使用本地网络视图可以监控当前的网络流量大小
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/af11f2ab96bc2092c9823c05c9e7ef74.jpeg)
更详细的资料可以打开资源监视器,如下图所示
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/42e95ac4e64fd02541d4886cb8e96088.jpeg)
Linux
平台可以使用以下 sar 命令查看sar -n DEV 1 2
![深入理解JVM性能调优](https://i-blog.csdnimg.cn/blog_migrate/8432c839cf700acd3a4f924f1aaafee9.jpeg)
字段说明:
rxpck/s:每秒钟接收的数据包
txpck/s:每秒钟发送的数据包
rxbyt/s:每秒钟接收的字节数
txbyt/s:每秒钟发送的字节数
rxcmp/s:每秒钟接收的压缩数据包
txcmp/s:每秒钟发送的压缩数据包
rxmcst/s:每秒钟接收的多播数据包
Java程序一般不会出现网络IO导致问题,因此在这里也不过的的阐述。
五、程序执行缓慢
当CPU、内存、磁盘、网络都不高,程序还是执行缓慢的话,可能引发的原因大致有以下几种:
1程序锁竞争过于激烈,比如你只有2颗CPU,但是你启用了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、 使用缓存