深入理解JVM—性能调优
在上文中我们分析了很多性能监控工具,介绍这些工具的目的只有一个,那就是找出对应的性能瓶颈。盲目的性能调优是没有效果的,只有充分知道了哪里出了问题,针对性的结果才是立竿见影的。解决了主要的性能问题,那些次要的性能问题也就不足为虑了!
我们知道,性能问题无非就这么几种:CPU、内存、磁盘IO、网络。那我们来逐一介绍以下相关的现象和一些可能出现的问题。
一、CPU过高。
查看CPU最简单的我们使用任务管理器查看,如下图所示,windows下使用任务管理器查看,Linux下使用top查看。
一般我们的服务器都采用Linux,因此我们重点关注一下Linux(注:windows模式下相信大家已经很熟悉了,并且前面我们已经提到,使用资源监视器可以很清楚的看到系统的各项参数,在这里我就不多做介绍了)
在top视图下,对于多核的CPU,显示的CPU资源有可能超过100%,因为这里显示的是所有CPU占用百分百的总和,如果你需要看单个CPU的占用情况,直接按键1就可以看到。如下图所示,我的一台测试机为8核16GB内存。
在top视图下,按键shift+h后,会显示各个线程的CPU资源消耗情况,如下图所示:
我们也可以通过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次
除了top、pidstat以外,vmstat也可以进行采样分析
相关
top、pidstat、mstat的用法大家可以去网上查找。
下面我们主要来介绍以下当出现CPU过高的时候,或者CPU不正常的时候,我们该如何去处理?
CPU消耗过高主要分为用户进程占用CPU过高和内核进程占用CPU过高(在Linux下top视图下us指的是用户进程,而sy是指内核进程),我们来看一个案例:
程序运行前,系统运行平稳,其中蓝色的线表示总的CPU利用率,而红色的线条表示内核使用率。部署war测试程序,运行如下图所示:
对于一个web程序,还没有任何请求就占用这么多CPU资源,显然是不正常的。并且我们看到,不是系统内核占用的大量CPU,而是系统进程,那是哪一个进程的呢?我们来看一下。
很明显是我们的java进程,那是那个地方导致的呢?这就需要用到我们之前提到的性能监控工具。在此我们使用可视化监控工具VisualVM。
首先我们排除了是GC过于频繁而导致大CPU过高,因为很明显监控视图上没有GC的活动。然后我们打开profilter去查看以下,是那个线程导致了CPU的过高?
前面一些线程都是容器使用的,而下面一个线程也一直在执行,那是什么地方调用的呢?查找代码中使用ThredPoolExecutor的地方。终于发现以下代码。
private BlockingQueue<SendMsg> queue;
private Executor executor;
//……
public void run() {
while(true){
try {
SendMsg sendMsg = queue.poll();//从队列中取出
if(null != sendMsg) {
sendForQueue(sendMsg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
问题很显然了,我们看一下对应BlockingQueue的poll方法的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.lock= locks[index];
}
@Override
public void run() {
while(true){ //循环执行自己要做的事情
doSth();
}
}
//做类自己要做的事情
public abstract void doSth();
}
//任务A 休眠自己的锁
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();
}
}
}
}
//任务B 唤醒所有锁
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下主要使用pidstat、iostat等进行分析。如下图所示
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;
/**
* @Described:IO测试用例
* @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.context= new 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 Edon为1.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程序锁竞争过于激烈,比如你只有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、 使用缓存