性能文章>使用XPocket插件vmstat查看上下文切换>

使用XPocket插件vmstat查看上下文切换原创

https://a.perfma.net/img/2382850
3年前
381703

vmstat(VirtualMeomoryStatistics,虚拟内存统计)是Linux中监控内存的常用工具,可对操作系统的虚拟内存、进程、CPU等的整体情况进行监视,虽然此工具提供的是对整个系统的一个统计数字,但是能够为更详细的诊断方法指明排查方向。现在XPocket已经集成了原生的vmstat,在XPocket中运行vmstat命令后如下图所示。
image.png
vmstat默认给出了一次的统计结果,其中各个字段的含义如下图所示。
image.png
我们在进行性能分析与故障排查时,通常会使用vmstat来查看CPU上下文切换次数,因为上下文切换会不可避免地浪费CPU资源。那么什么是上下文切换呢?上下文切换指的是操作系统调度器移除当前正在运行的线程或任务,并将其替换为等待中的线程。上下文切换的示意图如下图所示。
image.png
那么什么时候会造成上下文切换呢?下面进行了一下简单的列举:

  1. CPU 时间片结束,CPU 是划分为多个时间片给不同进程使用的,当这些进程使用完时间片后就被系统调度器移除出去;
  2. 进程运行需要的资源不够,如等待IO、锁争用等;
  3. 进程主动挂起,如调用系统函数sleep等;
  4. 有优先级更高的进程执行,如硬中断。

对于Java开发者来说,通常写的Java程序会造成上下文频繁切换的原因最可能是如上的第2点和第3点,我们可以分别写2个简单的Java实例模拟一下这2种情况,然后利用XPocket的vmstat插件工具观察上下文切换次数。

首先我们模拟一下由于等待IO造成的上下文切换。先创建一个SpringBoot项目,简单编写一个对外提供服务的Controller,代码如下:

@RestController
public class UserController implements Serializable {
    @RequestMapping("/list")
    public String list(){
        return "OK";
    }
}

然后我们用一个线程不断的发请求,代码如下:

public static void main(String[] args) {
    for (int i = 0; i < 10000000; ++i) {
        String url = "http://localhost:8080/list";
        sendRequest(url, "POST");
    }
}
 
public static String sendRequest(String urlParam, String requestType) {
    HttpURLConnection con = null;
    BufferedReader buffer = null;
    StringBuffer resultBuffer = null;
 
    try {
        URL url = new URL(urlParam);
        con = (HttpURLConnection) url.openConnection();
        con.setRequestMethod(requestType);
        con.setRequestProperty("Content-Type", "application/json;charset=GBK");
        con.setDoOutput(true);
        con.setDoInput(true);
        con.setUseCaches(false);
        int responseCode = con.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {
            InputStream inputStream = con.getInputStream();
            resultBuffer = new StringBuffer();
            String line;
            buffer = new BufferedReader(new InputStreamReader(inputStream, "GBK"));
            while ((line = buffer.readLine()) != null) {
                resultBuffer.append(line);
            }
            return resultBuffer.toString();
        }
 
    } catch (Exception e) {
        e.printStackTrace();
    }
    return "";
}

我们可以使用vmstat 1命令来查看上下文切换的次数,如下:
image.png
当我启动main()方法后只运行一个线程来发送请求,但是上下文的切换一下子多了几倍,这不可能是CPU 时间片结束、或者主动挂起或中断造成的,所以只能是IO等待,因为网络传输会涉及到IO交互,Java底层的实现会调用系统函数来处理。

我们再看一个进程频繁主动挂起的例子,如下:


import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
 
public final class ContextSwitchTest {
    static AtomicReference<Thread> turn = new AtomicReference<Thread>();
 
    static final class WorkerThread extends Thread {
        volatile Thread other;
        volatile int nparks;
 
        public void run() {
            final AtomicReference<Thread> t = turn;
            final Thread other = this.other;
            for (int i = 0; i < 100000000; ++i) {
                while (!t.compareAndSet(other, this)) {
                    LockSupport.park();
                }
                LockSupport.unpark(other);
            }
        }
    }
 
    public static void main(String[] args) throws Exception {
        WorkerThread a = new WorkerThread();
        WorkerThread b = new WorkerThread();
        a.other = b;
        b.other = a;
        turn.set(a);
        a.start();
        b.start();
        a.join();
        b.join();
    }
}

这次是线程频繁切换的例子,调用LockSupport.park()会主动让出CPU的使用权。再次运行vmstat 1后可以看到,系统的上下文切换增加了好几倍,如下:
image.png
在某个时间点,cs由104773一下增加到了373944,这就是我启动如上的Java程序造成的上下文切换次数突然增多。

虽然我们知道了哪些情况下会造成上下文切换,但是仅凭vmstat给出的cs上下文切换次数还不能够区分出到底是哪种情况,我们还需要结合XPocket中的其它集成工具综合查看,如线程锁争用时可通过JStack_X查看线程状态及锁的争用情况,使用线程剖析器查看阻塞代码。

点赞收藏
分类:标签:
鸠摩

著有《深入解析Java编译器:源码剖析与实例详解》、《深入剖析Java虚拟机:源码剖析与实例详解》等书籍。公众号“深入剖析Java虚拟机HotSpot”作者

请先登录,感受更多精彩内容
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
3
0