測量CPU和內存的佔用率常常是檢查Java應用程序是否達到特定性能的一個重要環節.儘管Java提供了一些重要的方法用於測量其堆棧大小,但是使用標準的API是無法測量本機Java進程的大小和 CPU當前的使用率的.這種測量的結果對於開發人員來說非常重要,它會提供應用程序的實時性能和效率信息.不幸的是,這樣的信息只能從操作系統直接獲取,而這已經超出了Java標準的可移植能力.
一個主要的解決方案是使用操作系統自帶的本機系統調用,將數據通過JNI(Java Native Interface,Java本機介面)傳輸給Java.與調用各個平台專用的外部命令(比如ps)並分析輸出結果不同,這種方案始終是一種很可靠的方式.以前碰到這樣的問題時,我嘗試過使用Vladimir Roubtsov自己編寫的一個很小的庫,它只能在Win32系統下測量進程的CPU佔用率.但是,這個庫的能力十分有限,所以我需要某種方式能夠同時在Windows和Solaris平台上測量CPU和內存的佔用率.
我擴展了這個庫的能力,在Windows和Solaris 8平台上實現了所有功能.新的庫能夠測量純CPU使用時間、CPU使用的百分比、本機剩餘內存和已經使用的內存、Java進程的本機內存大小、系統信息(比如操作系統的名字、補丁程序、硬體信息等).它由三部分實現: Java通用的部分、Windows實現,以及Solaris實現.依靠操作系統的部分用純C語言實現.
編輯提示:本文可以下載,所有的源代碼都以單獨的文本方式列出來了.
庫
所以,我們將創建一個簡單的JNI庫,用於同C層里的操作系統進行溝通,並把生成的數據提供給Java應用程序.,我們要創建一個SystemInformation類(列表A),為測量和記錄CPU的使用率和其他與系統相關的信息提供一個簡單的API.這個類是抽象的,它公開的是一個完全靜態的API.
列表A
<PRE>package com.vladium.utils; public abstract class SystemInformation m_time = time; m_CPUTime = CPUTime; } } // end of nested class // Custom exception class for throwing /** /** /** /** /** * Returns maximum memory available in the system. */ public static native long getMaxMem (); /** /** /** /** /** /** /** // protected: ............................................................. // package: ............................................................... // private: ............................................................... } // end of class |
最重要的方法是getProcessCPUTime(),它會返回當前進程的CPU時間(內核和用戶)的毫秒數,或者是PID在初始化期間被傳送給本機庫的進程所消耗的CPU時間.返回的值應該根據系統的處理器的個數進行調整;下面我們來看看這是如何在本機代碼里做到的.我們用修改符native來聲明這個方法,這意味著它必須在JNI代碼里實現.getProcessCPUTime()方法用來給CPU的使用率數據進行快照,方式是將經過測量的CPU時間與當前的系統時間進行關聯.這樣的資料庫快照在makeCPUUsageSnapshot()方法里進行,並輸出一個CPUUsageSnapshot容器對象.這樣,測量CPU使用率的原理就很容易理解了:我們按照給定的時間間隔進行兩次CPU快照,按1.0的分數來計算兩個時間點之間的CPU使用率,方式是兩點所在的CPU時間差除以兩點所在系統時間差.下面就是getProcessCPUUsage()方法的工作原理:
public static double getProcessCPUUsage (final CPUUsageSnapshot start, final CPUUsageSnapshot end) { if (start == null) throw new IllegalArgumentException ("null input: start"); if (end == null) throw new IllegalArgumentException ("null input: end"); if (end.m_time < start.m_time MIN_ELAPSED_TIME) throw new IllegalArgumentException ("end time must be at least " MIN_ELAPSED_TIME " ms later than start time"); return ((double)(end.m_CPUTime - start.m_CPUTime)) / (end.m_time - start.m_time); } |
只要我們知道分母里的快照之間的時間間隔,以及分子里進程花在活動狀態上的CPU時間,我們就會得到所測量的時間間隔過程中進程的CPU使用率;1.0就代表所有處理器100%的使用率.
事實上這種方式可以用在所有版本的UNIX的ps工具和Windows的任務管理器上,這兩個都是用於監視特定進程的CPU使用率的程序.很顯然,時間間隔越長,我們得到的結果就越平均、越不準確.但是最小時差應該被強制輸入getProcessCPUUsage().這種限制的原因是某些系統上的System.currentTimeMillis()的解析度很低.Solaris 8操作系統提供了一個系統調用,用於從內核表裡直接獲得CPU使用率.出於這個目的,我們擁有的getProcessCPUPercentage()方法會以百分比的形式返回進程所使用的CPU時間.如果這個特性不被操作系統支持(比如在Windows下),那麼JNI庫就會根據我們應用程序的設計返回一個負值.
還有其他一些本機聲明要在本機部分實現:
getCPUs()用來返回機器上處理器的個數
getMaxMem()用來返回系統上可用的最大物理內存
getFreeMem()用來返回系統上當前可用內存
getSysInfo()用來返回系統信息,包括一些硬體和操作系統的詳細信息
getMemoryUsage()用來返回分配給進程的空間,以KB為單位(這些頁面文件可能在內存里,也有可能不在內存里)
getMemoryResident()用來返回當前進程駐留在內存里的空間,以KB為單位.
所有這些方法對於不同的Java開發人員來說常常都是非常有用的.為了確保本機JNI庫被調入內存並在調用任何本機方法之前被初始化,這個庫被載入到一個靜態初始值里:
static { try { System.loadLibrary (SILIB); } catch (UnsatisfiedLinkError e) { System.out.println ("native lib '" SILIB "' not found in 'Java.library.path': " System.getProperty ("Java.library.path")); throw e; // re-throw } } |
在初始化一個.dll(或者.so)庫之後,我們可以直接使用本機聲明的方法:
final SystemInformation.CPUUsageSnapshot m_prevSnapshot = SystemInformation.makeCPUUsageSnapshot (); Thread.sleep(1000); final SystemInformation.CPUUsageSnapshot event = SystemInformation.makeCPUUsageSnapshot (); final long memorySize = SystemInformation.getMemoryUsage(); final long residentSize = SystemInformation.getMemoryResident(); long freemem = SystemInformation.getFreeMem()/1024; long maxmem = SystemInformation.getMaxMem()/1024; double receivedCPUUsage = 100.0 * SystemInformation.getProcessCPUUsage (m_prevSnapshot, event); System.out.println("Current CPU usage is " receivedCPUUsage "%」); |
現在讓我們來分別看看針對Windows和Solaris的JNI本機實現.C頭文件silib.h(列表B)能夠用JDK里的Javah工具生成,或者手動編寫.
列表B
/* DO NOT EDIT THIS FILE - it is machine generated */ #ifndef _Included_com_vladium_utils_SystemInformation /* /* /* /* /* #ifdef __cplusplus #endif #endif |
Windows
我們來看看Windows的實現(列表C).
列表C
/* ------------------------------------------------------------------------- */ #include #include "com_vladium_utils_SystemInformation.h" static jint s_PID; #define INFO_BUFFER_SIZE 32768
/* _time.LowPart = time->dwLowDateTime; return _time.QuadPart; /* s_PID = _getpid (); GetSystemInfo (& systemInfo); return JNI_VERSION_1_2; /* ......................................................................... */ JNIEXPORT void JNICALL if (!alreadyDetached && s_currentProcess!=NULL) { } /* /*
// Test for the specific product. if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 1 ) if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 0 ) if ( osvi.dwMajorVersion <= 4 ) // Test for specific product on Windows NT 4.0 SP6 and later. // Test for the server type. else if( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 0 ) else // Windows NT 4.0 lRet = RegOpenKeyEx( HKEY_LOCAL_MACHINE, lRet = RegQueryValueEx( hKey, "ProductType", NULL, NULL, RegCloseKey( hKey ); if ( lstrcmpi( "WINNT", szProductType) == 0 ) sprintf(buf2, "%d.%d ", (int)osvi.dwMajorVersion, (int)osvi.dwMinorVersion ); // Display service pack (if any) and build number. if( osvi.dwMajorVersion == 4 && // Test for SP6 versus SP6a. RegCloseKey( hKey ); break; // Test for the Windows Me/98/95. if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 0) if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 10) if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 90) case VER_PLATFORM_WIN32s: strcat(buf,"Win32s "); next_label: strcat(buf," next_label_2: lRet = RegQueryValueEx( hKey, "ProcessorNameString", NULL, NULL, strcat(buf,")"); jstring retval = (*env)->NewStringUTF(env,buf); /* ......................................................................... */
/* printf("[CPUmon] Could not attach to native process. /* /* BOOL resultSuccessful = GetProcessTimes (s_currentProcess, & creationTime, & exitTime, & kernelTime, & userTime); printf("[CPUmon] An error occured while trying to get CPU time. fflush(stdout); return (jlong) ((fileTimeToInt64 (& kernelTime) fileTimeToInt64 (& userTime)) / /* ......................................................................... */ /* /*
/* BOOL resultSuccessful = GetProcessTimes (s_currentProcess, & creationTime, & exitTime, & kernelTime, & userTime); printf("[CPUmon] An error occured while trying to get CPU time. /* elapsedTime = fileTimeToInt64 (& nowTime) - fileTimeToInt64 (& creationTime); if (elapsedTime < MIN_ELAPSED_TIME) /* // Not implemented on Windows return (jdouble)(-1.0); } /* ......................................................................... */ /* if ( GetProcessMemoryInfo( s_currentProcess, &pmc, sizeof(pmc)) ) /* if ( GetProcessMemoryInfo( s_currentProcess, &pmc, sizeof(pmc)) )
/* ------------------------------------------------------------------------- */ |
JNI里有兩個特殊的函數——JNI_OnLoad和JNI_OnUnload,它們分別在載入和卸載庫的時候被調用.JNI_OnLoad在調用其他任何方法之前被執行,能夠很方便地用於初始化在這一進程的生命周期中沒有發生變化的變數,並用於協調JNI規範的版本.在默認情況下,庫會測量它自己的進程的參數,但是通過調用systemInformation.setPid()方法它可以從Java應用程序被重載.s_PID C變數用來保存PID,而s_currentProcess用來保存進程句柄(用於Windows的是HANDLE類型,而用於Solaris的是int類型).為了讀取的一些參數,應該打開進程句柄,我們需要在庫關閉使用的時候停止同一個進程句柄(通常它在JVM相同的原因而關閉的時候發生).這就是JNI_OnUnload()函數起作用的地方.但是,JVM的一些實現事實上沒有調用JNI_OnUnload(),還有發生句柄會永遠打開的危險.為了降低這種可能性,我們應該在Java應用程序里加入一個明確調用detachProcess() C函數的關閉掛鉤.下面就是我們加入關閉掛鉤的方法:
if (pid!=-1) { int result = SystemInformation.setPid(pid); if (result!=0) { return; } hasPid = true; // Create shutdown hook for proper process detach Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { SystemInformation.detachProcess(); } }); } |
通過調用WinAPI里的GetSystemInfo(),我們還可以獲得關於中央處理器的一些信息.只要它是CPU的佔用率根據這個值來調節,測量進程最重要的值就是處理器的個數(s_numberOfProcessors).SystemInformation.getSysInfo()的Win32實現相當麻煩,在各個版本的Windows里,關於操作系統的版本、補丁、服務包以及相關硬體等信息被以不同的方式保存.所以需要讀者來分析相關的源代碼和代碼中的註釋.下面就是Windows XP上代碼輸出的示例:
System.out.println("SysInfo: 」 SystemInformation.getSysInfo()): And the same code on Solaris will give: SysInfo: SunOS 5.8 sxav-dev Generic_108528-29 sun4u sparc |
為了獲得CPU上進程所佔用的總時間,我們使用了WinAPI的函數GetProcessTimes.其他的函數實現都非常簡單,直接調用WinAPI,所以沒有什麼必要討論它們.列表D里是用於Windows版本的GCC的make.bat文件,用來幫助讀者創建相關的.dll庫.
列表D
gcc -D_JNI_IMPLEMENTATION_ -Wl,——kill-at -IC:/jdk1.3.1_12/include -IC:/jdk1.3.1_12/include/win32 -shared C:/cpu_profile/src/native/com_vladium_utils_SystemInformation.c -o C:/cpu_profile/dll/silib.dll C:/MinGW/lib/libpsapi.a 這個庫的Solaris實現見列表E和列表F.這兩個都是C語言文件,應該被編譯到一個共享庫(.so)里.用於編譯共享庫的幫助器make.sh見列表G.所有基於Solaris系統的調用被移到列表F里,這是的列表E就是一個JNI的簡單包裝程序.Solaris實現要比Win32實現更加複雜,要求更多的臨時數據結構、內核和進程表.列出的代碼里有更多的註釋.
列表E
/* ------------------------------------------------------------------------- */ * For simplicity, this implementaion assumes JNI 1.2 and omits error handling. * * Port from Win32 by Peter V. Mikhalenko (C) 2004, Deutsche Bank [peter@mikhalenko.ru] * Original source (C) 2002, Vladimir Roubtsov [vlad@trilogy.com] */ /* ------------------------------------------------------------------------- */ #include #include "com_vladium_utils_SystemInformation.h" // Helper Solaris8-dependent external routines static jint s_PID; /* ------------------------------------------------------------------------- */ /* ......................................................................... */ /* /* use kstat to update all processor information */ return JNI_VERSION_1_2; /* /* char * buf = sol_getSysInfo(); jstring retval = (*env)->NewStringUTF(env,buf); free(buf); return retval; } /* ......................................................................... */
/* /* /* /* /*
/* return 0.0; } /* ......................................................................... */ /*
/*
#undef MIN_ELAPSED_TIME /* ------------------------------------------------------------------------- */ #include #define _STRUCTURED_PROC 1
static struct nlist nlst[] = }; static kstat_ctl_t *kc = NULL; /* pagetok function is really a pointer to an appropriate function */ int pagetok_none(int size) { int pagetok_left(int size) { int pagetok_right(int size) { #define UPDKCID(nk,ok) void initKVM() { /* perform the kvm_open - suppress error here */ /* calculate pageshift value */ /* calculate an amount to shift to K values */ /* now determine which pageshift function is appropriate for the #define SI_LEN 512 char * sol_getSysInfo() { for (ks = kc->kc_chain; ks; cpu_ks[ncpu] = ks; /* return the number of cpus found */ unsigned long sol_getMaxMem() { unsigned long sol_getFreeMem() { // Returns the number of milliseconds (not nanoseconds and seconds) elapsed on processor // Returns percentage CPU by pid // Returns current space allocated for the process, in bytes. Those pages may or may not be in memory. // Returns current process space being resident in memory. /* |
列表G
#!/bin/sh gcc -G -lkvm -lkstat com_vladium_utils_SystemInformation.c -o libsilib.so solaris-extern.c # com_vladium_utils_SystemInformation.c is in Listing-E # solaris-extern.c is in Listing-F |
在本文里,我已告訴你如何編程測量Java進程的內存和CPU佔用率.當然用戶可以通過查看Windows任務管理器或者ps的輸出來達到相同的目的,但是重要的一點是,用戶現在能夠進行任何長期運行和/或自動的軟體性能測試,這對於開發一個分散式或者可伸縮的,或者實時的性能關鍵的應用程序十分重要.加入獲取系統軟體和硬體信息的能力對於開發人員同樣十分有用.這個庫可以在Win32和Solaris平台上實現.但是,把它移植到其他UNIX平台上應該也不是問題,比如Linux.
[火星人 ] 測量Java應用程序的CPU和內存佔用率已經有884次圍觀