<返回更多

揭密Java常用性能调优工具的底层实现原理

2023-06-05  今日头条  JAVA后端架构
加入收藏

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: %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);
    }
}

运行后的输出如下:

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

这个文件主要包含如下内容:

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

 

作者:鸠摩

出处:【全网首发】揭密Java常用性能调优工具的底层实现原理 | HeapDump性能社区

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>