Abstract
在這篇文章中我們會綜合性的介紹如何監控JVM cpu, thread 級別cpu, 以及如何通過JFR技術來分析JVM的CPU 問題.
如何獲取CPU
這裏我們會先介紹如何在進程內部獲取JVM的CPU. 這裏我們主要採用JVM 自帶的JMX來實現對自己的監控.
獲取整個系統的JVM cpu
可以通過調用mbean中的getProcessCpuTime方法來得到中的cputime.
簡單點來說就是:
(cputime2 - cputime1)/1000000/elapseTimeInMs/processorCount
完整代碼如下:
package cpu;
import com.sun.management.OperatingSystemMXBean;
import java.lang.management.ManagementFactory;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class CpuTest {
public static void main(String[] args) {
final AtomicInteger seq = new AtomicInteger(0);
ScheduledExecutorService es = Executors.newScheduledThreadPool(20, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread th = new Thread(r);
th.setName("consumingthreads-" + seq.incrementAndGet());
return th;
}
});
for (int i = 0; i < 200; i++) {
es.scheduleAtFixedRate(new ConsumingCpuTask(), 0, 10, TimeUnit.MILLISECONDS);
}
// not terminate the es
// another thread to print host cpu
ScheduledExecutorService printer = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread th = new Thread(r);
th.setName("printer");
return th;
}
});
// print every 10 seconds
printer.scheduleAtFixedRate(new PrintCurrentProcessCpuTask(), 0, 10, TimeUnit.SECONDS);
}
static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors();
// notice here is com.sun.management.OperatingSystemMXBean and it's not java.lang.management.OperatingSystemMXBean
static final OperatingSystemMXBean bean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
/**
* get process cpu in nanoseconds
*/
static double getProcessCpuTime() {
return bean.getProcessCpuTime();
}
/**
* A task to simulate consuming cpu
*/
static class ConsumingCpuTask implements Runnable {
@Override
public void run() {
AtomicInteger integer = new AtomicInteger(0);
for (int i = 0; i < 10000; i++) {
integer.incrementAndGet();
}
}
}
static class PrintCurrentProcessCpuTask implements Runnable {
double cpuTime = 0;
long collectTime = 0;
@Override
public void run() {
if (cpuTime == 0) {
cpuTime = getProcessCpuTime();
collectTime = System.currentTimeMillis();
}
else {
double newCpuTime = getProcessCpuTime();
long newCollectTime = System.currentTimeMillis();
double cpu = (newCpuTime - cpuTime) / (newCollectTime - collectTime) / 1000_000 / PROCESSOR_COUNT;
cpuTime = newCpuTime;
collectTime = newCollectTime;
System.out.println(String.format("Process cpu is: %.2f %%", cpu * 100));
}
}
}
}
當你運行這個代碼就可以看到定時打出的cpu指標,比如在我的機器上是17%左右(本身有16核):
而這個值跟系統顯示也是一致的(mac端的Activity Monitor/top命令):
或者用Java自帶的Jconsole 可以看(是JDK自帶的工具, 在bin目錄),運行jconsole
選擇關注的進程:
獲取系統中各個線程的CPU
這裏我們會展示如何獲取系統中各個線程的CPU 這個也很好統計.
獲取線程的cpu主要通過ThreadMXBean獲取:
// it's com.sun.management.ThreadMXBean
static ThreadMXBean threadMXBean = (ThreadMXBean) ManagementFactory.getThreadMXBean();
static class PrintThreadCpuTask implements Runnable {
// not consider thread safe here
Map<Long, Long> threadId2CpuTime = null;
Map<Long, String> threadId2Name = null;
private long collectTime = 0;
@Override
public void run() {
if (threadId2CpuTime == null) {
threadId2CpuTime = new HashMap<>();
threadId2Name = new HashMap<>();
long threads[] = threadMXBean.getAllThreadIds();
long cpuTimes[] = threadMXBean.getThreadCpuTime(threads);
for (int i = 0; i < threads.length; i++) {
threadId2CpuTime.put(threads[i], cpuTimes[i]);
// get the thread name
// maybe null, if not exists any more
ThreadInfo info = threadMXBean.getThreadInfo(threads[i]);
if (info != null) {
threadId2Name.put(threads[i], info.getThreadName());
}
}
collectTime = System.currentTimeMillis();
}
else {
long threads[] = threadMXBean.getAllThreadIds();
long cpuTimes[] = threadMXBean.getThreadCpuTime(threads);
Map<Long, Long> newthreadId2CpuTime = new HashMap<>();
for (int i = 0; i < threads.length; i++) {
newthreadId2CpuTime.put(threads[i], cpuTimes[i]);
}
long newCollectTime = System.currentTimeMillis();
threadId2CpuTime.entrySet().forEach(en -> {
long threadId = en.getKey();
Long time = en.getValue();
Long newTime = newthreadId2CpuTime.get(threadId);
if (newTime != null) {
double cpu = (newTime - time) * 1.0d / (newCollectTime - collectTime) / 1000000L / PROCESSOR_COUNT;
System.out.println(String.format("\t\tThread %s cpu is: %.2f %%", threadId2Name.get(threadId), cpu * 100));
}
threadId2CpuTime.put(threadId, newTime);
});
}
}
}
輸出如下:
如何分析
什麼JFR
JFR(Java flying recorder), 是java內置的一個性能數據採集器. 可以詳細的獲取JVM內部的狀態和事件.
JFR分析示例
通過如下的命令激活JFR:
java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder cpu.CpuTest
運行後通過內置的JCMD就可以進行採集等操作(JCMD 還有其他命令比如JFR.check 等.)
這個文件可以通過JMC(Java mission control)打開, 直接運行jmc即可 (也在jdk/bin下面):
在這裏面就可以看到熱點方法:
JFR本身還支持很多事件的採集包括GC/IO等.
Reference:https://docs.oracle.com/javacomponents/jmc-5-4/jfr-runtime-guide/about.htm
一個實際例子
在產品環境中發現的一個問題, 客戶會週期的執行一些snmp數據採集任務集合A, 然後每隔一段時間又會執行另外的任務集合B(此時A也在同步執行), 發現此時B執行時, CPU比較高, 這是正常的因爲任務B本身比較耗CPU, 但是發現任務B完成後, CPU依然很高, 並沒有下降的趨勢.
CPU圖形如下:
1. 分析了任務A的成功率並沒有變化.
2. 我們對比了前後的線程是否有增減, 找到了一個Windows selector 但是一查CPU 也沒有明顯的變化(~3%).
3. 最後我們用前面的腳本查詢了前後兩次的所有線程的CPU 發現了, snmp相關的線程cpu每個都從1.5% 增加到了 7%.
59 lm-collector-snmp-transport--4-1=1.61
60 lm-collector-snmp-transport--4-2=1.48
61 lm-collector-snmp-transport--4-3=1.56
62 lm-collector-snmp-transport--4-4=1.80
64 lm-collector-snmp-transport--4-5=1.64
65 snmp-selector=1.22
66 lm-collector-snmp-transport--4-6=1.41
67 lm-collector-snmp-transport--4-7=1.46
68 lm-collector-snmp-transport--4-10=1.77
69 lm-collector-snmp-transport--4-9=1.51
70 lm-collector-snmp-transport--4-8=1.88
4. 然後突然想起來了 好像我們底層是共享的snmp 發送線程, 然後又因爲有流控. 所以總共10組線程會每隔10ms 做一個host的發送任務. 但是在任務B中我們新加進來了很多任務(每個host新加了一個任務,然後在這10個線程中),相當於以前有900個host, 我們的線程組就會: 每10ms 執行900 次任務了, 然後B任務執行時, 又加了900個任務進來就是每10ms執行1800個任務.
這個對ScheduleExecutorService來說可能是個不小的性能問題.
5. 驗證猜想. 我們dump 2次JFR也發現了:
B任務執行前:
B任務執行後:
注意這個Context Switch count 被Double了, 還有ScheduledThreadPool中的siftDown方法也被調用更加頻繁了.
這個我本來最開始發現這個每個snmp線程的samplecount變多了, 但是感覺沒有多很多, 就沒去管.
實際上發現有還是多了很多的.
解決辦法嘛, 還在思考之中~~~~
博客代碼:https://github.com/gaoxingliang/goodutils/blob/master/src/cpu/CpuTest.java