日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

當(dāng)JAVA虛擬機(jī)出現(xiàn)故障和性能問題時(shí),我們通常會(huì)借助一些業(yè)界知名的工具來輔助排查問題。為了能更好的利用這些工具,我們通常需要對這些工具的實(shí)現(xiàn)原理有所了解,現(xiàn)有資料在介紹一些性能排查和故障診斷工具時(shí),通常只會(huì)圍繞這個(gè)工具的實(shí)現(xiàn)原理展開,例如Eclipse的MAT插件,主要是解讀虛擬機(jī)的Dump文件。這篇文章將從虛擬機(jī)的角度展開,看看虛擬機(jī)到底能提供什么樣的靜態(tài)或運(yùn)行時(shí)數(shù)據(jù)。對于HotSpot這款虛擬機(jī)來說,能提供的主要數(shù)據(jù)如下圖所示。

 

下面就來簡單介紹一下上圖中9個(gè)部分的數(shù)據(jù)以及圍繞這9部分?jǐn)?shù)據(jù)做出來的調(diào)優(yōu)工具。

1、虛擬機(jī)參數(shù)、系統(tǒng)變量等

如果要查看虛擬機(jī)參數(shù)或系統(tǒng)變量,可通過如下命令:

// 查看系統(tǒng)配置選項(xiàng)
jcmd 5617 VM.flags
// 查看虛擬機(jī)啟動(dòng)參數(shù)
jcmd 5617 VM.command_line
// 查看系統(tǒng)配置信息
jcmd 5617 VM.system_properties

許多的虛擬機(jī)故障和調(diào)優(yōu)都可通過調(diào)整虛擬機(jī)參數(shù)來達(dá)到目地,不過對于一般的Java開發(fā)人員來說,這并不是一項(xiàng)簡單的工作,需要對虛擬機(jī)相關(guān)的運(yùn)行原理有所了解。

針對虛擬機(jī)參數(shù)、系統(tǒng)變量等的調(diào)優(yōu)工具有:

(1)VM Options Explorer https://chriswhocodes.com/

(2)HeapDump社區(qū)的XXFox
https://opts.console.heapdump.cn/

2、堆轉(zhuǎn)儲(chǔ)文件

堆轉(zhuǎn)儲(chǔ)文件可用來檢索整個(gè)堆的快照,能夠從這個(gè)文件中獲取到活躍集合、對象的類型和數(shù)量,以及對象圖的形狀和結(jié)構(gòu)等等,堆導(dǎo)出常用的2種方式如下:

(1)通過命令,在發(fā)生OOM時(shí)導(dǎo)出,可配置參數(shù)-XX:+
HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=堆轉(zhuǎn)儲(chǔ)文件名

(2)Attach到目標(biāo)進(jìn)程后發(fā)送dump命令,jmap工具就是這樣做的,通過命令jmap -dump:format=b,file=堆轉(zhuǎn)儲(chǔ)文件名 pid導(dǎo)出文件。如文件較大時(shí),可通過添加live參數(shù)來有效縮小大小,如果要將堆轉(zhuǎn)儲(chǔ)文件轉(zhuǎn)移到其它地方,最好壓縮一下,堆轉(zhuǎn)儲(chǔ)文件的壓縮比例相對較高。

分析堆轉(zhuǎn)儲(chǔ)文件的工具通常都能夠給出類實(shí)例的數(shù)量、類實(shí)例的大小等,方便進(jìn)行堆溢出、頻繁GC等問題的排查。尤其是更多要關(guān)注類實(shí)例的數(shù)量和大小。假設(shè)GC頻繁,那么需要重點(diǎn)關(guān)注那些類實(shí)例占用內(nèi)存相對較大的;假設(shè)GC時(shí)間長,需要重點(diǎn)關(guān)注實(shí)例數(shù)量較多的,因?yàn)榭赡芑钴S的實(shí)例較多,標(biāo)注的時(shí)間就會(huì)長一些。

分析堆轉(zhuǎn)儲(chǔ)文件的工具有:

(1)Eclipse MAT https://www.eclipse.org/mat/

(2)HeapDump社區(qū)的XElephant
https://thread.console.heapdump.cn/

(3)HeapHero https://heaphero.io/

3、線程調(diào)用棧

通過JDK自帶的工具jstack可以導(dǎo)出HotSpot VM的線程棧,這些線程棧對于排查問題非常有幫助,不過導(dǎo)出的線程棧比較原始,拿到這些線程棧后可以做許多的事情,如:

從一次的堆棧信息中,我們可以直接獲取以下信息:

  • 是否有很多線程都在等待同一個(gè)鎖,說明這個(gè)系統(tǒng)存在性能瓶頸,導(dǎo)致了鎖競爭;
  • 當(dāng)前線程的總數(shù)量,如果線程的總數(shù)量有幾千上萬個(gè),那么大概率是線程泄漏;
  • 每一個(gè)線程的調(diào)用關(guān)系,當(dāng)前線程在調(diào)用哪些函數(shù),從而可看出一些性能比較影響大的一些方法;
  • 每個(gè)線程的當(dāng)前狀態(tài),持有哪些鎖,在等待哪些鎖,是否產(chǎn)生了死鎖,當(dāng)某個(gè)鎖的等待線程數(shù)很多時(shí),很明顯這就是系統(tǒng)瓶頸;
  • 大多數(shù)線程在干什么,在執(zhí)行什么代碼?

如果指定采樣,則從多次采樣的堆棧信息中,可以得到以下信息:

  • 線程數(shù)不斷上漲,可能是線程泄漏;
  • 是否總是存在同一個(gè)鎖總是有等待的線程,如果有,說明鎖是一個(gè)性能瓶頸;
  • 一個(gè)線程是否長期執(zhí)行,如果每次打印堆棧某個(gè)線程一直處于同樣的調(diào)用上下文中,那么說明這個(gè)線程一直執(zhí)行這段代碼,此時(shí)要根據(jù)代碼邏輯檢查,是否合理;
  • 通過多次堆棧信息,結(jié)合上火焰圖能更容易定位出慢方法;

網(wǎng)絡(luò)上最常見的通過線程調(diào)用棧分析的問題就是CPU使用率高的問題,通過top命令找到占用CPU最高的線程id,然后通過jstack來查看對應(yīng)線程id的調(diào)用棧,不過有些工具可一步到位,例如usefulscripts的show-busy-java-threads腳本,還有Arthas的thread命令等。

分析線程調(diào)用棧的工具有:

(1)HeapDump社區(qū)的XSheepdog
https://memory.console.heapdump.cn/

(2)fastThread https://fastthread.io/

(3)生成火焰圖的async-profiler
https://Github.com/jvm-profiling-tools/async-profiler

async-profiler直接抓取的是C/C++棧的調(diào)用棧,如果要生成Java調(diào)用棧的火焰圖,可通過jstack工具導(dǎo)出Java調(diào)用棧,然后整理成collapsed格式,利用async-profiler生成火焰圖即可。

4、日志

日志是出了系統(tǒng)問題第一手的排查資料,尤其是開發(fā)人員自己記錄的應(yīng)用日志。

(1)業(yè)務(wù)日志

一般是應(yīng)用的開發(fā)者根據(jù)不同的業(yè)務(wù)需求落日志,最終會(huì)通過大數(shù)據(jù)采集后進(jìn)行存儲(chǔ),方便以報(bào)表的方式展現(xiàn),也能輔助運(yùn)營人員對業(yè)務(wù)做出優(yōu)化。這一類數(shù)據(jù)對系統(tǒng)問題排查的幫助不大,可直接忽略。

(2)應(yīng)用日志

應(yīng)用可能會(huì)采集起來進(jìn)行人工排查或監(jiān)控。大多數(shù)開發(fā)人員在查找系統(tǒng)問題時(shí),也應(yīng)該重點(diǎn)關(guān)注這些系統(tǒng)日志,因?yàn)樗瑧?yīng)用程序編寫的各種錯(cuò)誤消息,警告或其他事件。這些消息可以提供與特定用例相關(guān)的詳細(xì)信息。如

  • 用例中發(fā)生的異常的堆棧跟蹤。有關(guān)外部系統(tǒng)響應(yīng)時(shí)間較慢的警告消息。用例被觸發(fā)或完成的信息。

(3)系統(tǒng)日志

GC日志、Crash文件等屬于JVM相關(guān)的系統(tǒng)日志。這里我們只介紹一下GC日志,因?yàn)樗容^重要,記錄的信息比較全面。需要配置GC參數(shù)來開啟,如下:

Java 8版本:-XX:+PrintGC(或-verbose:gc) -XX:+PrintGCDetAIls -XX:+PrintGCDateStamps -Xloggc:日志名稱

Java 9及9+版本:-Xlog:gc*:file=日志名稱

GC日志能夠給出回收前后堆中各個(gè)代的大小、總堆的大小、GC發(fā)生的原因及GC所花費(fèi)的時(shí)間等,連續(xù)監(jiān)控GC日志能夠得到GC發(fā)生的頻次及內(nèi)存分配率等。好多人有的問題就是,想要知道GC觸發(fā)的原因,目前只能通過GC日志來查看,所以建議配置GC日志。

由于GC日志含有的數(shù)據(jù)指標(biāo)多,而且日志沒有一個(gè)標(biāo)準(zhǔn)的格式,所以要借助一些專業(yè)的日志解析工具查看,典型的分析工具如下:

(1)開源GCViewer
https://github.com/chewiebug/GCViewer

(2)GCeasy https://gceasy.io

(3)商用工具Censum
https://www.jclarity.com/pricing/censum-as-a-service-enterprise/

5、PerfData

對于HotSpot VM來說,會(huì)將一些統(tǒng)計(jì)信息寫到一個(gè)叫PerfData的共享文件中,默認(rèn)路徑為/tmp/hsperfdata_<user>/。在我的本機(jī)上看一下/tmp/hsperfdata_<user>/目錄下的內(nèi)容:

 

其中的名稱文件是pid, 我們可以從/tmp/hsperfdata_<user>/<pid>這個(gè)特定的文件中獲取相關(guān)數(shù)據(jù)。

JDK自帶的工具jps就是直接讀取這個(gè)目錄下的文件名來列出所有的Java進(jìn)程號(hào)。正常情況下當(dāng)JVM進(jìn)程退出的時(shí)候會(huì)自動(dòng)刪除,但是當(dāng)執(zhí)行kill -9命令時(shí),由于JVM不能捕獲這種信號(hào),雖然JVM進(jìn)程不存在了,但是這個(gè)文件還是存在的。這個(gè)文件不是一直存在的,當(dāng)再次有JVM進(jìn)程啟動(dòng)時(shí)會(huì)自動(dòng)刪除這些無用的文件。jps在讀取/tmp/hsperfdata_<user>/路徑下的文件名稱時(shí),也會(huì)通過attach的方式判斷這個(gè)進(jìn)程是否存活,這樣就能保證讀取出的是存活的進(jìn)程。

JDK自帶的工具jstat也是通過讀取PerfData中特定的文件內(nèi)容來實(shí)現(xiàn)的。由于PerfData文件是通過mmap的方式映射到了內(nèi)存里,而jstat是直接通過DirectByteBuffer的方式從PerfData里讀取的,所以只要內(nèi)存里的值變了,那我們從jstat看到的值就會(huì)發(fā)生變化,內(nèi)存里的值什么時(shí)候變,取決于
-XX:PerfDataSamplingInterval這個(gè)參數(shù),默認(rèn)是50ms,也就是說50ms更新一次值,基本上可以認(rèn)為是實(shí)時(shí)的了。基于PerfData實(shí)現(xiàn)的jstat,因?yàn)槔厥掌鲿?huì)主動(dòng)將jstat所需要的摘要數(shù)據(jù)保存至固定位置之中,所以只需直接讀取即可。

jstat在讀取相關(guān)內(nèi)容時(shí),需要知道鍵值對,查看鍵值對的方式如下:

 

jstat -J-Djstat.showUnsupported=true -snap 4726

 

或者直接查看
jdk/src/share/classes/sun/tools/jstat/resources/jstat_options文件,其中給出了timestamp、class、compiler、gc、gccapacity、gccause、gcnew、gcnewcapacity、gcold、gcoldcapacity、gcmetacapacity、gcutil、printcompilation這幾個(gè)大類中的相關(guān)信息。

相關(guān)工具有:

(1)JDK自帶的jps、jstat

(2)vjtools中的vjtop
https://github.com/vipshop/vjtools/tree/master/vjtop

6、JMX

JVM是一個(gè)成熟的執(zhí)行平臺(tái),它為運(yùn)行中的應(yīng)用程序注入、監(jiān)控和可觀測性提供了很多技術(shù)選擇,而JMX(Java Management Extensions)就是一種,通過JMX可以實(shí)現(xiàn)對類加載監(jiān)控、內(nèi)存監(jiān)控、線程監(jiān)控,以及獲取Java應(yīng)用本地JVM內(nèi)存、GC、線程、Class、堆棧、系統(tǒng)數(shù)據(jù)等。另外,還可以用作日志級(jí)別的動(dòng)態(tài)修改,比如 log4j 就支持 JMX 方式動(dòng)態(tài)修改線上服務(wù)的日志級(jí)別。最主要的還是被用來做各種監(jiān)控工具,比如Spring Boot Actuator、JConsole、VisualVM 等。

JMX通過各種 MBean(Managed Bean) 來傳遞消息。外界可以獲取被管理的資源的狀態(tài)和操縱MBean的行為。常見的MBean如下表所示。

 

ClassLoadingMXBean獲取類裝載信息,已裝載、已卸載量

CompilationMXBean獲取編譯器信息

GarbageCollectionMXBean獲取GC信息,但他僅僅提供了GC的次數(shù)和GC花費(fèi)總時(shí)間

MemoryManagerMXBean提供了內(nèi)存管理和內(nèi)存池的名字信息

MemoryMXBean提供整個(gè)虛擬機(jī)中內(nèi)存的使用情況

MemoryPoolMXBean提供獲取各個(gè)內(nèi)存池的使用信息

OperatingSystemMXBean提供操作系統(tǒng)的簡單信息

RuntimeMXBean提供運(yùn)行時(shí)當(dāng)前JVM的詳細(xì)信息

ThreadMXBean提供對線程使用的狀態(tài)信息

名稱解釋

 

下面舉一個(gè)小例子,讓大家有直觀的認(rèn)識(shí),如下:

class JMXUtil {
    private static final long MB = 1024*1024L;
 
    public static void main(String[] args) {
        printMemoryInfo();
    }
 
    static void printMemoryInfo() {
        MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
        MemoryUsage headMemory = memory.getHeapMemoryUsage();
        String info = String.format("ninit: %st max: %st used: %st committed: %st use rate: %sn",
                headMemory.getInit() / MB + "MB",
                headMemory.getMax() / MB + "MB", headMemory.getUsed() / MB + "MB",
                headMemory.getCommitted() / MB + "MB",
                headMemory.getUsed() * 100 / headMemory.getCommitted() + "%"
        );
 
        System.out.print(info);
 
        MemoryUsage nonheadMemory = memory.getNonHeapMemoryUsage();
        info = String.format("init: %st max: %st used: %st committed: %st use rate: %sn",
                nonheadMemory.getInit() / MB + "MB",
                nonheadMemory.getMax() / MB + "MB", nonheadMemory.getUsed() / MB + "MB",
                nonheadMemory.getCommitted() / MB + "MB",
                nonheadMemory.getUsed() * 100 / nonheadMemory.getCommitted() + "%"
 
        );
        System.out.println(info);
    }
}

運(yùn)行后的輸出如下:

init: 124MB max: 1751MB used: 2MB committed: 119MB use rate: 2%
init: 2MB   max: 0MB    used: 5MB   committed: 7MB  use rate: 66%

一般監(jiān)控系統(tǒng)用的比較多,也就是和JavaAgent方式結(jié)合以后,就能在指定了監(jiān)控的目標(biāo)Java進(jìn)程后,打印目標(biāo)Java進(jìn)程的一些系統(tǒng)信息,如堆和非堆的參數(shù)。Arthas中dashboard命令中顯示的堆外內(nèi)存大小就是通過JavaAgent加上JMX來實(shí)現(xiàn)的。  

相關(guān)的工具有:

(1)JDK自帶的jconsole或VisualVM

(2)監(jiān)控系統(tǒng)Spring Boot Actuator
https://www.baeldung.com/spring-boot-actuators

(3)vjtools中的vjmxcli
https://github.com/DarLiner/vjtools

7、JVMTI

JVMTI 本質(zhì)上是對JVM內(nèi)部的許多事件進(jìn)行了埋點(diǎn)。通過這些埋點(diǎn)可以給外部提供當(dāng)前上下文的一些信息。甚至可以接受外部的命令來改變下一步的動(dòng)作。外部程序一般利用C/C++實(shí)現(xiàn)一個(gè)JVMTIAgent,在JVMTIAgent里面注冊一些JVM事件的回調(diào)。當(dāng)事件發(fā)生時(shí)JVMTI調(diào)用這些回調(diào)方法。JVMTIAgent可以在回調(diào)方法里面實(shí)現(xiàn)自己的邏輯。通過JVMTI,可以實(shí)現(xiàn)對JVM的多種操作,它通過接口注冊各種事件勾子,在JVM事件觸發(fā)時(shí),同時(shí)觸發(fā)預(yù)定義的勾子,以實(shí)現(xiàn)對各個(gè)JVM事件的響應(yīng),事件包括類文件加載、異常產(chǎn)生與捕獲、線程啟動(dòng)和結(jié)束、進(jìn)入和退出臨界區(qū)、成員變量修改、GC開始和結(jié)束、方法調(diào)用進(jìn)入和退出、臨界區(qū)競爭與等待、VM啟動(dòng)與退出等等。

另外還有一種是JavaAgent,其底層的實(shí)現(xiàn)就是利用了JVMTI,不過可以使用Java語言來實(shí)現(xiàn),但是功能沒有JVMTIAgent強(qiáng)大。

現(xiàn)在假設(shè)有一個(gè)需求,監(jiān)控應(yīng)用拋出的異常,如果出現(xiàn)異常,就在監(jiān)控系統(tǒng)中提醒,這時(shí)候就需要JVMTI來實(shí)現(xiàn)了。

使用C++編寫JVMTIAgent,分別實(shí)現(xiàn)Agent_OnLoad()和Agent_OnUnload()函數(shù),另外注冊異常回調(diào)函數(shù),在發(fā)生異常時(shí),打印異常的詳細(xì)信息,實(shí)現(xiàn)如下:

#include <IOStream>
#include <cstring>
#include "jvmti.h"
 
using namespace std;
 
//異常回調(diào)函數(shù)
static void JNICALL callbackException(
 jvmtiEnv *jvmti_env,
 JNIEnv  *env,
 jthread thr,
 jmethodID methodId,
 jlocation location,
 jobject exception,
 jmethodID catch_method,
 jlocation catch_location
) {
 
    // 得到方法對應(yīng)的類
    jclass clazz;
    jvmti_env->GetMethodDeclaringClass(methodId, &clazz);
 
    // 得到類的簽名
    char *class_signature;
    jvmti_env->GetClassSignature(clazz, &class_signature, nullptr);
 
    //異常類名稱
    char *exception_class_name;
    jclass exception_class = env->GetObjectClass(exception);
    jvmti_env->GetClassSignature(exception_class, &exception_class_name, nullptr);
 
    // 得到方法名稱
    char *method_name_ptr, *method_signature_ptr;
    jvmti_env->GetMethodName(methodId, &method_name_ptr, &method_signature_ptr, nullptr);
 
    //獲取目標(biāo)方法的起止地址和結(jié)束地址
    jlocation start_location_ptr;    //方法的起始位置
    jlocation end_location_ptr;      //用于方法的結(jié)束位置
    jvmti_env->GetMethodLocation(methodId, &start_location_ptr, &end_location_ptr);
 
    //輸出測試結(jié)果
    cout << "測試結(jié)果-定位類的簽名:" << class_signature << endl;
    cout << "測試結(jié)果-定位方法信息:" << method_name_ptr << " -> " << method_signature_ptr << endl;
    cout << "測試結(jié)果-定位方法位置:" << start_location_ptr << " -> " << end_location_ptr + 1 << endl;
    cout << "測試結(jié)果-異常類的名稱:" << exception_class_name << endl;
 
    cout << "測試結(jié)果-輸出異常信息(能夠分析行號(hào)):" << endl;
    jclass throwable_class = (*env).FindClass("java/lang/Throwable");
    jmethodID print_method = (*env).GetMethodID(throwable_class, "printStackTrace", "()V");
    (*env).CallVoidMethod(exception, print_method);
}
 
// Agent_OnLoad函數(shù),如果agent是在啟動(dòng)的時(shí)候加載的,也就是在vm參數(shù)里通過-agentlib來指定,那在啟動(dòng)過程中就會(huì)去執(zhí)行這個(gè)agent里的Agent_OnLoad函數(shù)
JNIEXPORT jint JNICALL Agent_OnLoad(
JavaVM *vm,
char *options,
void *reserved
) {
    cout << "Agent_OnLoad(" << vm << ")" << endl;
 
    jvmtiEnv *gb_jvmti = nullptr;
    //初始化
    vm->GetEnv(reinterpret_cast<void **>(&gb_jvmti), JVMTI_VERSION_1_0);
    // 建立一個(gè)新的環(huán)境
    jvmtiCapabilities caps;
    memset(&caps, 0, sizeof(caps));
    caps.can_signal_thread = 1;
    caps.can_get_owned_monitor_info = 1;
    caps.can_generate_method_entry_events = 1;
    caps.can_generate_exception_events = 1;
    caps.can_generate_vm_object_alloc_events = 1;
    caps.can_tag_objects = 1;
    // 設(shè)置當(dāng)前環(huán)境
    gb_jvmti->AddCapabilities(&caps);
    // 建立一個(gè)新的回調(diào)函數(shù)
    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(callbacks));
    //異常回調(diào)
    callbacks.Exception = &callbackException;
    // 設(shè)置回調(diào)函數(shù)
    gb_jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
    // 開啟事件監(jiān)聽(JVMTI_EVENT_EXCEPTION)
    gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, nullptr);
    return JNI_OK;
}
// Agent_OnUnload函數(shù),在agent做卸載的時(shí)候調(diào)用,不過貌似基本上很少實(shí)現(xiàn)它
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) { }

通過相關(guān)命令將如上代碼編寫為動(dòng)態(tài)鏈接庫,如下:

g++ -std=c++11 -Wall -fPIC -c TestException.cpp -I ./  -I /home/mazhi/workspace/jdk1.8.0_192/include/linux/ -I /home/mazhi/workspace/jdk1.8.0_192/include/
g++ -Wall -rdynamic -shared -o libdiaoyong.so TestException.o

相關(guān)命令就不再過多解釋,有興趣可自行查閱相關(guān)資料。 

現(xiàn)在編寫一個(gè)拋出異常的Java應(yīng)用,如下:

public class CatchAllException {
    public static void main(String[] args) throws Exception {
        try {
            throw new NullPointerException("空指針異常");
        } catch (Exception e) {
            // e.printStackTrace();
        }
    }
}

在啟動(dòng)Java應(yīng)用時(shí),為虛擬機(jī)配置參數(shù)
-agentpath:/home/mazhi/workspace/projectcplusplus/TestException/src/libdiaoyong.so,打印的異常信息如下:

測試結(jié)果-定位類的簽名:LCatchAllException;
測試結(jié)果-定位方法信息:main -> ([Ljava/lang/String;)V
測試結(jié)果-定位方法位置:0 -> 12
測試結(jié)果-異常類的名稱:Ljava/lang/NullPointerException;
測試結(jié)果-輸出異常信息(能夠分析行號(hào)):
java.lang.NullPointerException: 空指針異常
    at CatchAllException.main(CatchAllException.java:9)

如上功能的實(shí)現(xiàn)要依賴于JVMTI這套接口,有了這套接口能夠做出許多重要的功能。

JVMTI接口的中文文檔:
https://blog.caoxudong.info/blog/2017/12/07/jvmti_reference

另外還有JavaAgent,他是JVMTIAgent的一個(gè)特例,可做的操作有限,但好處就是可用Java語言來實(shí)現(xiàn)。下面舉一個(gè)JavaAgent的例子。創(chuàng)建一個(gè)Maven工程,編寫JavaAgent,實(shí)現(xiàn)如下:

package lesson5.example1;
// ...
public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) throws Throwable {
        System.out.println("loading static agent...");
        MyTransformer monitor = new MyTransformer();
        inst.addTransformer(monitor);
    }
}

編寫Transformer,如下:

package lesson5.example1;
// ...
public class MyTransformer implements ClassFileTransformer {
    public byte[] transform(
            ClassLoader loader, 
            String className, 
            Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, 
            byte[] classfileBuffer ) throws IllegalClassFormatException {
 
        ClassReader cr = new ClassReader(classfileBuffer);
        ClassNode classNode = new ClassNode(Opcodes.ASM6);
        cr.accept(classNode, ClassReader.SKIP_FRAMES);
 
        for (MethodNode methodNode : classNode.methods) {
            if ("main".equals(methodNode.name)) {
                InsnList instrumentation = new InsnList();
                instrumentation
                        .add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
                instrumentation.add(new LdcInsnNode("Hello, Instrumentation!"));
                instrumentation.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println",
                        "(Ljava/lang/String;)V", false));
 
                methodNode.instructions.insert(instrumentation);
                break;
            }
        }
 
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        classNode.accept(cw);
        return cw.toByteArray();
    }
}

然后在Maven中配置:

<plugin>
    <groupId>org.Apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>single</goal>
            </goals>
            <phase>package</phase>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                    <manifestEntries>
                        <Premain-Class>lesson5.example1.MyAgent</Premain-Class> 
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
        </execution>
    </executions>
</plugin>

在運(yùn)行Maven Install后,會(huì)在target目錄下生成一個(gè)jar包,這就是JavaAgent,我們可以在啟動(dòng)任何一個(gè)Java應(yīng)用啟動(dòng)時(shí),通過-javaagent參數(shù)來指定JavaAgent,如下:

public class Test {
    public static void main(String args[]){
        System.out.println("execute main method ...");
    }
}

運(yùn)行結(jié)果如下:

Hello, Instrumentation!

execute main method ...

可以看到,通過JavaAgent對原有的Test類生成的字節(jié)碼程序進(jìn)行了增強(qiáng),這就讓我們的想像空間變的非常大,因?yàn)槟憧梢愿娜魏畏椒w中的字節(jié)碼,甚至替換整個(gè)類。例如,可以在字節(jié)碼前后打印時(shí)間,這樣就能輸出調(diào)用方法的耗時(shí);可以給整個(gè)方法體增加異常捕獲的try-catch,在不修改、不重新部署應(yīng)用程序的情況下修復(fù)某些Bug等等。  

有些資料總結(jié)了Agent可以實(shí)現(xiàn)的功能,如下:

1、使用JVMTI對Class文件加密

有時(shí)一些涉及到關(guān)鍵技術(shù)的Class文件或者jar包不希望對外暴露,所以需要加密。使用一些常規(guī)的手段(例如使用混淆器或者自定義類加載器)來對Class文件進(jìn)行加密很容易被反編譯。反編譯后的代碼雖然增加了閱讀的難度,但花費(fèi)一些功夫也是可以讀懂的。使用JVMTI可以將解密的代碼封裝成.dll或.so 文件。這些文件想要反編譯就很麻煩了。不過個(gè)人認(rèn)為,這樣并不能完全避免代碼泄漏,不要忘記Agent中提供的一些API,這些API能夠?qū)⒓虞d到虛擬機(jī)中的Class文件的內(nèi)容Dump出來。

2、使用JVMTI實(shí)現(xiàn)應(yīng)用性能監(jiān)控(APM)

在微服務(wù)大行其道的環(huán)境下,分布式系統(tǒng)的邏輯結(jié)構(gòu)變得越來越復(fù)雜。這給系統(tǒng)性能分析和問題定位帶來了非常大的挑戰(zhàn)。基于JVMTI的APM能夠解決分布式架構(gòu)和微服務(wù)帶來的監(jiān)控和運(yùn)維上的挑戰(zhàn)。APM通過匯聚業(yè)務(wù)系統(tǒng)各處理環(huán)節(jié)的實(shí)時(shí)數(shù)據(jù),分析業(yè)務(wù)系統(tǒng)各事務(wù)處理的交易路徑和處理時(shí)間,實(shí)現(xiàn)對應(yīng)用的全鏈路性能監(jiān)測。

相關(guān)的工具有:

(1)開源的Pinpoint、ZipKin、Hawkular

(2)商業(yè)的AppDynamics、OneAPM、google Dapper等都是個(gè)中好手。

3、產(chǎn)品運(yùn)行時(shí)錯(cuò)誤監(jiān)測及調(diào)試

想要看生產(chǎn)環(huán)境的異常,最原始的方式是登錄到生產(chǎn)環(huán)境的機(jī)器查看日志。稍微高級(jí)一點(diǎn)的方式是通過日志監(jiān)控或者APM等工具將異常采集上來。但是這些手段都有許多明顯的缺點(diǎn)。首先,不是所有的異常都會(huì)被打印到日志中,有些異常可能被代碼吃掉了;其次,打印異常的時(shí)候通常只有異常堆棧信息,異常發(fā)生時(shí)上下文的變量值很難獲取到(除非有經(jīng)驗(yàn)的程序員將其打印出來了),而這些信息對定位異常的原因至關(guān)重要。基于JVMTI可以開發(fā)出一款工具來時(shí)事監(jiān)控生產(chǎn)環(huán)境的異常。其基本的原理和如上實(shí)例的原理相同。

相關(guān)的工具:商業(yè)軟件OverOps

4、JAVA程序的調(diào)試(debug)。

一般JAVA的IDE都自帶了調(diào)試工具。例如Eclipse的調(diào)試器相信大部分人都使用過。它的調(diào)試器org.eclipse.jdt.debug插件底層就是調(diào)用的JVMTI來實(shí)現(xiàn)的。經(jīng)常使用eclipse等工具對java代碼做調(diào)試,其實(shí)就利用了jre自帶的jdwp agent來實(shí)現(xiàn)的,只是由于eclipse等工具在沒讓你察覺的情況下將相關(guān)參數(shù)(類似-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:61349)給自動(dòng)加到程序啟動(dòng)參數(shù)列表里了,其中agentlib參數(shù)就是用來跟要加載的agent的名字,比如這里的jdwp(不過這不是動(dòng)態(tài)庫的名字,而JVM是會(huì)做一些名稱上的擴(kuò)展,比如在linux下會(huì)去找libjdwp.so的動(dòng)態(tài)庫進(jìn)行加載,也就是在名字的基礎(chǔ)上加前綴lib,再加后綴.so),接下來會(huì)跟一堆相關(guān)的參數(shù),會(huì)將這些參數(shù)傳給Agent_OnLoad或者Agent_OnAttach函數(shù)里對應(yīng)的options參數(shù)。

隨著服務(wù)云化的發(fā)展,google甚至推出了云端調(diào)試工具cloud debugger。它時(shí)一個(gè)web應(yīng)用,可以直接對生產(chǎn)環(huán)境進(jìn)行遠(yuǎn)程調(diào)試,不需要重啟或者中斷服務(wù)。阿里也有類似的工具Zdebugger。

5、JAVA程序的診斷(profile)。

當(dāng)出現(xiàn)CPU使用率過高、線程死鎖等問題時(shí),需要使用一些JAVA性能剖析或者診斷工具來分析具體的原因。例如Alibaba開源的Java診斷工具Arthas,深受開發(fā)者喜愛。Arthas的功能十分強(qiáng)大,它可以查看或者動(dòng)態(tài)修改某個(gè)變量的值、統(tǒng)計(jì)某個(gè)方法調(diào)用鏈上的耗時(shí)、攔截方法前后,打印參數(shù)值和返回值,以及異常信息等。

6、熱加載

熱加載指的是在不重啟虛擬機(jī)的情況下重新加載一些class。熱加載可以在本地調(diào)試代碼或線上修改代碼時(shí)不用頻繁重啟。如spring-loaded,還有商業(yè)產(chǎn)品JRebel等。

相關(guān)工具:

(1)spring-loaded https://github.com/spring-projects/spring-loaded

(2)JRebel https://www.jrebel.com/

8、Serviceability Agent

SA是JDK提供的一個(gè)強(qiáng)大的調(diào)試工具集,可以用來調(diào)試運(yùn)行著的Java進(jìn)程、core文件和虛擬機(jī)crash以后的dump文件。所以我們在遇到CPU飆高、內(nèi)存泄漏、應(yīng)用奔潰等問題時(shí),可以借助SA技術(shù)實(shí)現(xiàn)的工具來查找問題。在JDK自帶的工具中, jmap、jstack、jinfo、HSDB等工具都在使用著SA。SA 機(jī)制不需要與進(jìn)程互動(dòng),通過直接分析目標(biāo)進(jìn)程的內(nèi)存布局獲取目標(biāo) JVM 進(jìn)程的運(yùn)行時(shí)數(shù)據(jù),如呈現(xiàn)出類對象、能夠識(shí)別出Java堆、堆邊界、堆內(nèi)對象、載入的類描述、棧內(nèi)存、線程狀態(tài)等信息,是不是感覺黑科技?其實(shí)原理也并沒那么難,我們平時(shí)所說的Java堆棧等內(nèi)存模型都是虛擬機(jī)層面概念,虛擬機(jī)最終還是跑在操作系統(tǒng)上的,所以可以使用SA直接讀取目標(biāo)進(jìn)程的操作系統(tǒng)層面的內(nèi)存數(shù)據(jù)。

一般在使用jmap、jstack工具時(shí),使用的是attach方式(之前介紹的JavaAgent和JVMTIAgent同樣也使用了attach),這種方式就是與目標(biāo)進(jìn)程建立 socket 連接,目標(biāo)進(jìn)程處理后回傳客戶端,所以需要虛擬機(jī)本身代碼的支持,但是SA不需要在目標(biāo) VM 中運(yùn)行任何代碼,SA 使用操作系統(tǒng)提供的符號(hào)查找和進(jìn)程內(nèi)存讀取等原語實(shí)現(xiàn)。所以當(dāng)jmap、jstack等導(dǎo)不出堆棧數(shù)據(jù)時(shí),可以采用SA的方式獲取數(shù)據(jù),例如jstack加上-F選項(xiàng)來解決。有時(shí)候我們在運(yùn)行這些工具時(shí),會(huì)報(bào)如下錯(cuò)誤:

Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 6: Operation not permitted
 
// ...

SA的操作,最主要是通過系統(tǒng)調(diào)用ptrace實(shí)現(xiàn)。ptrace會(huì)使內(nèi)核暫停目標(biāo)進(jìn)程并將控制權(quán)交給跟蹤進(jìn)程,使跟蹤進(jìn)程得以察看目標(biāo)進(jìn)程的內(nèi)存。這是一個(gè)很危險(xiǎn)的操作,會(huì)造成機(jī)密數(shù)據(jù)泄漏,所以ptrace-scope為了防止用戶訪問當(dāng)前正在運(yùn)行的進(jìn)程的內(nèi)存和狀態(tài),默認(rèn)情況下不允許再訪問了,我們可以使用sudo賦于權(quán)限來解決這個(gè)問題。

常用工具:

(1)JDK自帶的 jmap、jstack、jinfo、HSDB等工具

(2)vjmap是分代版的jmap(新生代,存活區(qū),老生代),是排查內(nèi)存緩慢泄露,老生代增長過快原因的利器,也是利用了SA的原理,
https://github.com/vipshop/vjtools/tree/master/vjmap

注意,當(dāng)SA 開始分析時(shí),整個(gè)目標(biāo)JVM是停頓下來不工作的,讓SA可以從容讀取進(jìn)程內(nèi)存中的數(shù)據(jù),直到斷開后才會(huì)恢復(fù)。所以在生產(chǎn)環(huán)境上使用有SA技術(shù)的工具時(shí),必須先摘除流量。

9、Crash文件

造成嚴(yán)重錯(cuò)誤的原因有多種可能性。Java虛擬機(jī)自身的Bug是原因之一,但是這種可能不是很大。在絕大多數(shù)情況下,

是由于系統(tǒng)的庫文件、API或第三方的庫文件造成的;系統(tǒng)資源的短缺也有可能造成這種嚴(yán)重的錯(cuò)誤。

當(dāng)JVM發(fā)生致命錯(cuò)誤導(dǎo)致崩潰時(shí),會(huì)生成一個(gè)hs_err_pid_xxx.log這樣的文件,該文件包含了導(dǎo)致 JVM crash 的重要信息,我們可以通過分析該文件定位到導(dǎo)致 JVM Crash 的原因,從而修復(fù)保證系統(tǒng)穩(wěn)定。

默認(rèn)情況下,該文件是生成在工作目錄下的,當(dāng)然也可以通過 JVM 參數(shù)指定生成路徑:

java -XX:ErrorFile=/var/log/hs_err_pid<pid>.log

這個(gè)文件主要包含如下內(nèi)容:

  • 日志頭文件
  • 導(dǎo)致 crash 的線程信息
  • 所有線程信息
  • 安全點(diǎn)和鎖信息
  • 堆信息
  • 本地代碼緩存
  • 編譯事件
  • gc 相關(guān)記錄
  • jvm 內(nèi)存映射
  • jvm 啟動(dòng)參數(shù)
  • 服務(wù)器信息

內(nèi)容還是相對來說比較全的,但是顯示過于專業(yè),一般虛擬機(jī)開發(fā)人員可能參考的比較多一些。

 

作者:鳩摩

出處:【全網(wǎng)首發(fā)】揭密Java常用性能調(diào)優(yōu)工具的底層實(shí)現(xiàn)原理 | HeapDump性能社區(qū)

分享到:
標(biāo)簽:Java
用戶無頭像

網(wǎng)友整理

注冊時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評(píng)定2018-06-03

通用課目體育訓(xùn)練成績評(píng)定