性能文章>【全网首发】揭密Java常用性能调优工具的底层实现原理>

【全网首发】揭密Java常用性能调优工具的底层实现原理原创

https://a.perfma.net/img/2382850
1年前
6257310

当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的调用栈,不过有些工具可一步到位,例如usefulscriptsshow-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 启动参数
  • 服务器信息

内容还是相对来说比较全的,但是显示过于专业,一般虚拟机开发人员可能参考的比较多一些。

分析Crash文件的工具有:CrashAnalysis https://github.com/xpbob/CrashAnalysis

 

 

点赞收藏
鸠摩

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

请先登录,查看3条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
10
3