當Java虛拟機出現故障和性能問題時,我們通常會借助一些業界知名的工具來輔助排查問題。為了能更好的利用這些工具,我們通常需要對這些工具的實作原理有所了解,現有資料在介紹一些性能排查和故障診斷工具時,通常隻會圍繞這個工具的實作原理展開,例如Eclipse的MAT插件,主要是解讀虛拟機的Dump檔案。這篇文章将從虛拟機的角度展開,看看虛拟機到底能提供什麼樣的靜态或運作時資料。對于HotSpot這款虛拟機來說,能提供的主要資料如下圖所示。
下面就來簡單介紹一下上圖中9個部分的資料以及圍繞這9部分資料做出來的調優工具。
1、虛拟機參數、系統變量等
如果要檢視虛拟機參數或系統變量,可通過如下指令:
// 檢視系統配置選項
jcmd 5617 VM.flags
// 檢視虛拟機啟動參數
jcmd 5617 VM.command_line
// 檢視系統配置資訊
jcmd 5617 VM.system_properties
許多的虛拟機故障和調優都可通過調整虛拟機參數來達到目地,不過對于一般的Java開發人員來說,這并不是一項簡單的工作,需要對虛拟機相關的運作原理有所了解。
針對虛拟機參數、系統變量等的調優工具有:
(1)VM Options Explorer https://chriswhocodes.com/
(2)HeapDump社群的XXFox https://opts.console.heapdump.cn/
2、堆轉儲檔案
堆轉儲檔案可用來檢索整個堆的快照,能夠從這個檔案中擷取到活躍集合、對象的類型和數量,以及對象圖的形狀和結構等等,堆導出常用的2種方式如下:
(1)通過指令,在發生OOM時導出,可配置參數-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=堆轉儲檔案名
(2)Attach到目标程序後發送dump指令,jmap工具就是這樣做的,通過指令jmap -dump:format=b,file=堆轉儲檔案名 pid導出檔案。如檔案較大時,可通過添加live參數來有效縮小大小,如果要将堆轉儲檔案轉移到其它地方,最好壓縮一下,堆轉儲檔案的壓縮比例相對較高。
分析堆轉儲檔案的工具通常都能夠給出類執行個體的數量、類執行個體的大小等,友善進行堆溢出、頻繁GC等問題的排查。尤其是更多要關注類執行個體的數量和大小。假設GC頻繁,那麼需要重點關注那些類執行個體占用記憶體相對較大的;假設GC時間長,需要重點關注執行個體數量較多的,因為可能活躍的執行個體較多,标注的時間就會長一些。
分析堆轉儲檔案的工具有:
(1)Eclipse MAT https://www.eclipse.org/mat/
(2)HeapDump社群的XElephant https://thread.console.heapdump.cn/
(3)HeapHero https://heaphero.io/
3、線程調用棧
通過JDK自帶的工具jstack可以導出HotSpot VM的線程棧,這些線程棧對于排查問題非常有幫助,不過導出的線程棧比較原始,拿到這些線程棧後可以做許多的事情,如:
從一次的堆棧資訊中,我們可以直接擷取以下資訊:
- 是否有很多線程都在等待同一個鎖,說明這個系統存在性能瓶頸,導緻了鎖競争;
- 目前線程的總數量,如果線程的總數量有幾千上萬個,那麼大機率是線程洩漏;
- 每一個線程的調用關系,目前線程在調用哪些函數,進而可看出一些性能比較影響大的一些方法;
- 每個線程的目前狀态,持有哪些鎖,在等待哪些鎖,是否産生了死鎖,當某個鎖的等待線程數很多時,很明顯這就是系統瓶頸;
- 大多數線程在幹什麼,在執行什麼代碼?
如果指定采樣,則從多次采樣的堆棧資訊中,可以得到以下資訊:
- 線程數不斷上漲,可能是線程洩漏;
- 是否總是存在同一個鎖總是有等待的線程,如果有,說明鎖是一個性能瓶頸;
- 一個線程是否長期執行,如果每次列印堆棧某個線程一直處于同樣的調用上下文中,那麼說明這個線程一直執行這段代碼,此時要根據代碼邏輯檢查,是否合理;
- 通過多次堆棧資訊,結合上火焰圖能更容易定位出慢方法;
網絡上最常見的通過線程調用棧分析的問題就是CPU使用率高的問題,通過top指令找到占用CPU最高的線程id,然後通過jstack來檢視對應線程id的調用棧,不過有些工具可一步到位,例如usefulscripts的show-busy-java-threads腳本,還有Arthas的thread指令等。
分析線程調用棧的工具有:
(1)HeapDump社群的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++棧的調用棧,如果要生成Java調用棧的火焰圖,可通過jstack工具導出Java調用棧,然後整理成collapsed格式,利用async-profiler生成火焰圖即可。
4、日志
日志是出了系統問題第一手的排查資料,尤其是開發人員自己記錄的應用日志。
(1)業務日志
一般是應用的開發者根據不同的業務需求落日志,最終會通過大資料采集後進行存儲,友善以報表的方式展現,也能輔助營運人員對業務做出優化。這一類資料對系統問題排查的幫助不大,可直接忽略。
(2)應用日志
應用可能會采集起來進行人工排查或監控。大多數開發人員在查找系統問題時,也應該重點關注這些系統日志,因為它包含應用程式編寫的各種錯誤消息,警告或其他事件。這些消息可以提供與特定用例相關的詳細資訊。如
- 用例中發生的異常的堆棧跟蹤。有關外部系統響應時間較慢的警告消息。用例被觸發或完成的資訊。
(3)系統日志
GC日志、Crash檔案等屬于JVM相關的系統日志。這裡我們隻介紹一下GC日志,因為它比較重要,記錄的資訊比較全面。需要配置GC參數來開啟,如下:
Java 8版本:-XX:+PrintGC(或-verbose:gc) -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:日志名稱
Java 9及9+版本:-Xlog:gc*:file=日志名稱
GC日志能夠給出回收前後堆中各個代的大小、總堆的大小、GC發生的原因及GC所花費的時間等,連續監控GC日志能夠得到GC發生的頻次及記憶體配置設定率等。好多人有的問題就是,想要知道GC觸發的原因,目前隻能通過GC日志來檢視,是以建議配置GC日志。
由于GC日志含有的資料名額多,而且日志沒有一個标準的格式,是以要借助一些專業的日志解析工具檢視,典型的分析工具如下:
(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來說,會将一些統計資訊寫到一個叫PerfData的共享檔案中,預設路徑為/tmp/hsperfdata_<user>/。在我的本機上看一下/tmp/hsperfdata_<user>/目錄下的内容:
其中的名稱檔案是pid, 我們可以從/tmp/hsperfdata_<user>/<pid>這個特定的檔案中擷取相關資料。
JDK自帶的工具jps就是直接讀取這個目錄下的檔案名來列出所有的Java程序号。正常情況下當JVM程序退出的時候會自動删除,但是當執行kill -9指令時,由于JVM不能捕獲這種信号,雖然JVM程序不存在了,但是這個檔案還是存在的。這個檔案不是一直存在的,當再次有JVM程序啟動時會自動删除這些無用的檔案。jps在讀取/tmp/hsperfdata_<user>/路徑下的檔案名稱時,也會通過attach的方式判斷這個程序是否存活,這樣就能保證讀取出的是存活的程序。
JDK自帶的工具jstat也是通過讀取PerfData中特定的檔案内容來實作的。由于PerfData檔案是通過mmap的方式映射到了記憶體裡,而jstat是直接通過DirectByteBuffer的方式從PerfData裡讀取的,是以隻要記憶體裡的值變了,那我們從jstat看到的值就會發生變化,記憶體裡的值什麼時候變,取決于-XX:PerfDataSamplingInterval這個參數,預設是50ms,也就是說50ms更新一次值,基本上可以認為是實時的了。基于PerfData實作的jstat,因為垃圾回收器會主動将jstat所需要的摘要資料儲存至固定位置之中,是以隻需直接讀取即可。
jstat在讀取相關内容時,需要知道鍵值對,檢視鍵值對的方式如下:
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這幾個大類中的相關資訊。
相關工具有:
(1)JDK自帶的jps、jstat
(2)vjtools中的vjtop https://github.com/vipshop/vjtools/tree/master/vjtop
6、JMX
JVM是一個成熟的執行平台,它為運作中的應用程式注入、監控和可觀測性提供了很多技術選擇,而JMX(Java Management Extensions)就是一種,通過JMX可以實作對類加載監控、記憶體監控、線程監控,以及擷取Java應用本地JVM記憶體、GC、線程、Class、堆棧、系統資料等。另外,還可以用作日志級别的動态修改,比如 log4j 就支援 JMX 方式動态修改線上服務的日志級别。最主要的還是被用來做各種監控工具,比如Spring Boot Actuator、JConsole、VisualVM 等。
JMX通過各種 MBean(Managed Bean) 來傳遞消息。外界可以擷取被管理的資源的狀态和操縱MBean的行為。常見的MBean如下表所示。
ClassLoadingMXBean擷取類裝載資訊,已裝載、已解除安裝量
CompilationMXBean擷取編譯器資訊
GarbageCollectionMXBean擷取GC資訊,但他僅僅提供了GC的次數和GC花費總時間
MemoryManagerMXBean提供了記憶體管理和記憶體池的名字資訊
MemoryMXBean提供整個虛拟機中記憶體的使用情況
MemoryPoolMXBean提供擷取各個記憶體池的使用資訊
OperatingSystemMXBean提供作業系統的簡單資訊
RuntimeMXBean提供運作時目前JVM的詳細資訊
ThreadMXBean提供對線程使用的狀态資訊
名稱解釋
下面舉一個小例子,讓大家有直覺的認識,如下:
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: %s\t max: %s\t used: %s\t committed: %s\t use rate: %s\n",
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: %s\t max: %s\t used: %s\t committed: %s\t use rate: %s\n",
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);
}
}
運作後的輸出如下:
init: 124MB max: 1751MB used: 2MB committed: 119MB use rate: 2%
init: 2MB max: 0MB used: 5MB committed: 7MB use rate: 66%
一般監控系統用的比較多,也就是和JavaAgent方式結合以後,就能在指定了監控的目标Java程序後,列印目标Java程序的一些系統資訊,如堆和非堆的參數。Arthas中dashboard指令中顯示的堆外記憶體大小就是通過JavaAgent加上JMX來實作的。
相關的工具有:
(1)JDK自帶的jconsole或VisualVM
(2)監控系統Spring Boot Actuator https://www.baeldung.com/spring-boot-actuators
(3)vjtools中的vjmxcli https://github.com/DarLiner/vjtools
7、JVMTI
JVMTI 本質上是對JVM内部的許多事件進行了埋點。通過這些埋點可以給外部提供目前上下文的一些資訊。甚至可以接受外部的指令來改變下一步的動作。外部程式一般利用C/C++實作一個JVMTIAgent,在JVMTIAgent裡面注冊一些JVM事件的回調。當事件發生時JVMTI調用這些回調方法。JVMTIAgent可以在回調方法裡面實作自己的邏輯。通過JVMTI,可以實作對JVM的多種操作,它通過接口注冊各種事件勾子,在JVM事件觸發時,同時觸發預定義的勾子,以實作對各個JVM事件的響應,事件包括類檔案加載、異常産生與捕獲、線程啟動和結束、進入和退出臨界區、成員變量修改、GC開始和結束、方法調用進入和退出、臨界區競争與等待、VM啟動與退出等等。
另外還有一種是JavaAgent,其底層的實作就是利用了JVMTI,不過可以使用Java語言來實作,但是功能沒有JVMTIAgent強大。
現在假設有一個需求,監控應用抛出的異常,如果出現異常,就在監控系統中提醒,這時候就需要JVMTI來實作了。
使用C++編寫JVMTIAgent,分别實作Agent_OnLoad()和Agent_OnUnload()函數,另外注冊異常回調函數,在發生異常時,列印異常的詳細資訊,實作如下:
#include <iostream>
#include <cstring>
#include "jvmti.h"
using namespace std;
//異常回調函數
static void JNICALL callbackException(
jvmtiEnv *jvmti_env,
JNIEnv *env,
jthread thr,
jmethodID methodId,
jlocation location,
jobject exception,
jmethodID catch_method,
jlocation catch_location
) {
// 得到方法對應的類
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);
//擷取目标方法的起止位址和結束位址
jlocation start_location_ptr; //方法的起始位置
jlocation end_location_ptr; //用于方法的結束位置
jvmti_env->GetMethodLocation(methodId, &start_location_ptr, &end_location_ptr);
//輸出測試結果
cout << "測試結果-定位類的簽名:" << class_signature << endl;
cout << "測試結果-定位方法資訊:" << method_name_ptr << " -> " << method_signature_ptr << endl;
cout << "測試結果-定位方法位置:" << start_location_ptr << " -> " << end_location_ptr + 1 << endl;
cout << "測試結果-異常類的名稱:" << exception_class_name << endl;
cout << "測試結果-輸出異常資訊(能夠分析行号):" << 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函數,如果agent是在啟動的時候加載的,也就是在vm參數裡通過-agentlib來指定,那在啟動過程中就會去執行這個agent裡的Agent_OnLoad函數
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);
// 建立一個新的環境
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;
// 設定目前環境
gb_jvmti->AddCapabilities(&caps);
// 建立一個新的回調函數
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
//異常回調
callbacks.Exception = &callbackException;
// 設定回調函數
gb_jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
// 開啟事件監聽(JVMTI_EVENT_EXCEPTION)
gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, nullptr);
return JNI_OK;
}
// Agent_OnUnload函數,在agent做解除安裝的時候調用,不過貌似基本上很少實作它
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) { }
通過相關指令将如上代碼編寫為動态連結庫,如下:
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
相關指令就不再過多解釋,有興趣可自行查閱相關資料。
現在編寫一個抛出異常的Java應用,如下:
public class CatchAllException {
public static void main(String[] args) throws Exception {
try {
throw new NullPointerException("空指針異常");
} catch (Exception e) {
// e.printStackTrace();
}
}
}
在啟動Java應用時,為虛拟機配置參數-agentpath:/home/mazhi/workspace/projectcplusplus/TestException/src/libdiaoyong.so,列印的異常資訊如下:
測試結果-定位類的簽名:LCatchAllException;
測試結果-定位方法資訊:main -> ([Ljava/lang/String;)V
測試結果-定位方法位置:0 -> 12
測試結果-異常類的名稱:Ljava/lang/NullPointerException;
測試結果-輸出異常資訊(能夠分析行号):
java.lang.NullPointerException: 空指針異常
at CatchAllException.main(CatchAllException.java:9)
如上功能的實作要依賴于JVMTI這套接口,有了這套接口能夠做出許多重要的功能。
JVMTI接口的中文文檔:https://blog.caoxudong.info/blog/2017/12/07/jvmti_reference
另外還有JavaAgent,他是JVMTIAgent的一個特例,可做的操作有限,但好處就是可用Java語言來實作。下面舉一個JavaAgent的例子。建立一個Maven工程,編寫JavaAgent,實作如下:
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>
在運作Maven Install後,會在target目錄下生成一個jar包,這就是JavaAgent,我們可以在啟動任何一個Java應用啟動時,通過-javaagent參數來指定JavaAgent,如下:
public class Test {
public static void main(String args[]){
System.out.println("execute main method ...");
}
}
運作結果如下:
Hello, Instrumentation!
execute main method ...
可以看到,通過JavaAgent對原有的Test類生成的位元組碼程式進行了增強,這就讓我們的想像空間變的非常大,因為你可以更改任何方法體中的位元組碼,甚至替換整個類。例如,可以在位元組碼前後列印時間,這樣就能輸出調用方法的耗時;可以給整個方法體增加異常捕獲的try-catch,在不修改、不重新部署應用程式的情況下修複某些Bug等等。
有些資料總結了Agent可以實作的功能,如下:
1、使用JVMTI對Class檔案加密
有時一些涉及到關鍵技術的Class檔案或者jar包不希望對外暴露,是以需要加密。使用一些正常的手段(例如使用混淆器或者自定義類加載器)來對Class檔案進行加密很容易被反編譯。反編譯後的代碼雖然增加了閱讀的難度,但花費一些功夫也是可以讀懂的。使用JVMTI可以将解密的代碼封裝成.dll或.so 檔案。這些檔案想要反編譯就很麻煩了。不過個人認為,這樣并不能完全避免代碼洩漏,不要忘記Agent中提供的一些API,這些API能夠将加載到虛拟機中的Class檔案的内容Dump出來。
2、使用JVMTI實作應用性能監控(APM)
在微服務大行其道的環境下,分布式系統的邏輯結構變得越來越複雜。這給系統性能分析和問題定位帶來了非常大的挑戰。基于JVMTI的APM能夠解決分布式架構和微服務帶來的監控和運維上的挑戰。APM通過彙聚業務系統各處理環節的實時資料,分析業務系統各事務處理的交易路徑和處理時間,實作對應用的全鍊路性能監測。
相關的工具有:
(1)開源的Pinpoint、ZipKin、Hawkular
(2)商業的AppDynamics、OneAPM、Google Dapper等都是個中好手。
3、産品運作時錯誤監測及調試
想要看生産環境的異常,最原始的方式是登入到生産環境的機器檢視日志。稍微進階一點的方式是通過日志監控或者APM等工具将異常采集上來。但是這些手段都有許多明顯的缺點。首先,不是所有的異常都會被列印到日志中,有些異常可能被代碼吃掉了;其次,列印異常的時候通常隻有異常堆棧資訊,異常發生時上下文的變量值很難擷取到(除非有經驗的程式員将其列印出來了),而這些資訊對定位異常的原因至關重要。基于JVMTI可以開發出一款工具來時事監控生産環境的異常。其基本的原理和如上執行個體的原理相同。
相關的工具:商業軟體OverOps
4、JAVA程式的調試(debug)。
一般JAVA的IDE都自帶了調試工具。例如Eclipse的調試器相信大部分人都使用過。它的調試器org.eclipse.jdt.debug插件底層就是調用的JVMTI來實作的。經常使用eclipse等工具對java代碼做調試,其實就利用了jre自帶的jdwp agent來實作的,隻是由于eclipse等工具在沒讓你察覺的情況下将相關參數(類似-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:61349)給自動加到程式啟動參數清單裡了,其中agentlib參數就是用來跟要加載的agent的名字,比如這裡的jdwp(不過這不是動态庫的名字,而JVM是會做一些名稱上的擴充,比如在linux下會去找libjdwp.so的動态庫進行加載,也就是在名字的基礎上加字首lib,再加字尾.so),接下來會跟一堆相關的參數,會将這些參數傳給Agent_OnLoad或者Agent_OnAttach函數裡對應的options參數。
随着服務雲化的發展,google甚至推出了雲端調試工具cloud debugger。它時一個web應用,可以直接對生産環境進行遠端調試,不需要重新開機或者中斷服務。阿裡也有類似的工具Zdebugger。
5、JAVA程式的診斷(profile)。
當出現CPU使用率過高、線程死鎖等問題時,需要使用一些JAVA性能剖析或者診斷工具來分析具體的原因。例如Alibaba開源的Java診斷工具Arthas,深受開發者喜愛。Arthas的功能十分強大,它可以檢視或者動态修改某個變量的值、統計某個方法調用鍊上的耗時、攔截方法前後,列印參數值和傳回值,以及異常資訊等。
6、熱加載
熱加載指的是在不重新開機虛拟機的情況下重新加載一些class。熱加載可以在本地調試代碼或線上修改代碼時不用頻繁重新開機。如spring-loaded,還有商業産品JRebel等。
相關工具:
(1)spring-loaded https://github.com/spring-projects/spring-loaded
(2)JRebel https://www.jrebel.com/
8、Serviceability Agent
SA是JDK提供的一個強大的調試工具集,可以用來調試運作着的Java程序、core檔案和虛拟機crash以後的dump檔案。是以我們在遇到CPU飙高、記憶體洩漏、應用奔潰等問題時,可以借助SA技術實作的工具來查找問題。在JDK自帶的工具中, jmap、jstack、jinfo、HSDB等工具都在使用着SA。SA 機制不需要與程序互動,通過直接分析目标程序的記憶體布局擷取目标 JVM 程序的運作時資料,如呈現出類對象、能夠識别出Java堆、堆邊界、堆内對象、載入的類描述、棧記憶體、線程狀态等資訊,是不是感覺黑科技?其實原理也并沒那麼難,我們平時所說的Java堆棧等記憶體模型都是虛拟機層面概念,虛拟機最終還是跑在作業系統上的,是以可以使用SA直接讀取目标程序的作業系統層面的記憶體資料。
一般在使用jmap、jstack工具時,使用的是attach方式(之前介紹的JavaAgent和JVMTIAgent同樣也使用了attach),這種方式就是與目标程序建立 socket 連接配接,目标程序處理後回傳用戶端,是以需要虛拟機本身代碼的支援,但是SA不需要在目标 VM 中運作任何代碼,SA 使用作業系統提供的符号查找和程序記憶體讀取等原語實作。是以當jmap、jstack等導不出堆棧資料時,可以采用SA的方式擷取資料,例如jstack加上-F選項來解決。有時候我們在運作這些工具時,會報如下錯誤:
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的操作,最主要是通過系統調用ptrace實作。ptrace會使核心暫停目标程序并将控制權交給跟蹤程序,使跟蹤程序得以察看目标程序的記憶體。這是一個很危險的操作,會造成機密資料洩漏,是以ptrace-scope為了防止使用者通路目前正在運作的程序的記憶體和狀态,預設情況下不允許再通路了,我們可以使用sudo賦于權限來解決這個問題。
常用工具:
(1)JDK自帶的 jmap、jstack、jinfo、HSDB等工具
(2)vjmap是分代版的jmap(新生代,存活區,老生代),是排查記憶體緩慢洩露,老生代增長過快原因的利器,也是利用了SA的原理,https://github.com/vipshop/vjtools/tree/master/vjmap
注意,當SA 開始分析時,整個目标JVM是停頓下來不工作的,讓SA可以從容讀取程序記憶體中的資料,直到斷開後才會恢複。是以在生産環境上使用有SA技術的工具時,必須先摘除流量。
9、Crash檔案
造成嚴重錯誤的原因有多種可能性。Java虛拟機自身的Bug是原因之一,但是這種可能不是很大。在絕大多數情況下,
是由于系統的庫檔案、API或第三方的庫檔案造成的;系統資源的短缺也有可能造成這種嚴重的錯誤。
當JVM發生緻命錯誤導緻崩潰時,會生成一個hs_err_pid_xxx.log這樣的檔案,該檔案包含了導緻 JVM crash 的重要資訊,我們可以通過分析該檔案定位到導緻 JVM Crash 的原因,進而修複保證系統穩定。
預設情況下,該檔案是生成在工作目錄下的,當然也可以通過 JVM 參數指定生成路徑:
java -XX:ErrorFile=/var/log/hs_err_pid<pid>.log
這個檔案主要包含如下内容:
- 日志頭檔案
- 導緻 crash 的線程資訊
- 所有線程資訊
- 安全點和鎖資訊
- 堆資訊
- 本地代碼緩存
- 編譯事件
- gc 相關記錄
- jvm 記憶體映射
- jvm 啟動參數
- 伺服器資訊
内容還是相對來說比較全的,但是顯示過于專業,一般虛拟機開發人員可能參考的比較多一些。
為幫助開發者們提升面試技能、有機會入職BATJ等大廠公司,特别制作了這個專輯——這一次整體放出。
大緻内容包括了: Java 集合、JVM、多線程、并發程式設計、設計模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat等大廠面試題等、等技術棧!
歡迎大家關注公衆号【Java爛豬皮】,回複【666】,擷取以上最新Java後端架構VIP學習資料以及視訊學習教程,然後一起學習,一文在手,面試我有。
每一個專欄都是大家非常關心,和非常有價值的話題,如果我的文章對你有所幫助,還請幫忙點贊、好評、轉發一下,你的支援會激勵我輸出更高品質的文章,非常感謝!
作者:鸠摩
出處:【全網首發】揭密Java常用性能調優工具的底層實作原理 | HeapDump性能社群