手游下载网

手游下载网

JVM内存Dump原理与在线分析实战

admin 124 13

文|Bruce得物技术

1.前言

当前我们微服务容器化部署JVM实例很多,常常需要进行JVMheapdumpanalysis,为了提升JVM问题排查效率,得物技术保障团队研究了JVM内存Dump原理与设计开发了JVM内存在线分析。

常见的JVMheapdumpanalysis工具如:MAT,JProfile,最常用的功能是大对象分析。功能上本地分析工具更全面,在微服务架构下,成千上万的实例当需要一次分析的时候,于是我们思考如何提供更方便更快的在线分析方便研发人员快速排障。

流程

传统

在线分析

相比

hprof获取

jmap

jmap

相同

hprof传输

1.上传ftp或对象存储。

2.生产环境涉及跨网脱敏。

3.跨网下载。

内网OSS(对象存储)传输。

目前jvm基本进入G1大内存时代。越大内存dump效果越明显耗时降低(100倍耗时降低)为大规模dump分析打下基础。

hprof分析

本地MAT、JProfiler等分析工具

在线分析、在线分析报告

优点:

不依赖任何软件。

操作简单,只需一键执行脚本。

分析耗时比本地工具更快。

不受内存限制,支持大内存dump分析。

自研不受商业限制。

微服务环境多实例同时并发分析,不受单机资源限制。

不足:

MAT,JProfile功能更丰富

2.JVM内存模型

首先我们快速过一下Java的内存模型,这部分不必深入,稍微了解不影响第三部分JVM内存分析原理。可回过头来再看。

JVM内存模型可以从共享和非共享理解,也可以从stack,heap理解。GC主要作用于heap区,stack的内存存在系统内存。

2.1Run-TimeDataAreas

Java程序运行起来后,JVM会把它所管理的内存划分为若干个不同的数据区域。其中一些数据区是在Java虚拟机启动时创建的,只有在Java虚拟机退出时才会销毁。其他数据区是每个线程。每线程数据区在创建线程时创建,并在线程退出时销毁。JVM的数据区是逻辑内存空间,它们可能不是连续的物理内存空间。下图显示了JVM运行时数据区域:

PCRegister

JVM可以同时支持多个执行线程。每个JVM线程都有自己的pc(程序计数器)寄存器。如果当前方法是native方法则PC值为undefined,每个CPU都有一个PC,一般来说每一次指令之后,PC值会增加,指向下一个操作指令的地址。JVM使用PC保持操作指令的执行顺序,PC值实际上就是指向方法区(MethodArea)的内存地址。

JVMStacks

每个JVM线程都有一个私有JVMStack(堆栈),用于存储Frames(帧)。JVMStack的每一Frame(帧)都存储当前方法的局部变量数组、操作数堆栈和常量池引用。

一个JVMStack可能有很多Frame(帧),因为在线程的任何方法完成之前,它可能会调用许多其他方法,而这些方法的帧也存储在同一个JVMStack(堆栈)中。

JVMStack是一个先进后出(LIFO)的数据结构,所以当前的执行方法位于栈顶,每一个方法开始执行时返回、或抛出一个未捕获的异常,则次frame被移除。

JVMStack除了压帧和弹出帧之外,JVM堆栈从不直接操作,所以帧可能是堆分配的。JVM堆栈的内存不需要是连续的。

NativeMethodStack

Native基本为C/C++本地函数,超出了Java的范畴,就不展开赘述了。接入进入共享区域Heap区。

2.2Heap

JVM有一个在所有JVM线程之间共享的堆。堆是运行时数据区,从中分配所有类实例和数组的内存。

堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象永远不会被显式释放。JVM没有假设特定类型的自动存储管理系统,可以根据实现者的系统要求选择存储管理技术。堆的内存不需要是连续的。

MethodArea

JVM有一个在所有JVM线程之间共享的方法区。方法区类似于常规语言编译代码的存储区,或类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量轮询、字段和方法数据,以及方法和构造函数的代码,包括在类和实例初始化和接口初始化中使用的特殊方法。

Method区域是在虚拟机启动时创建的。尽管方法区在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾收集或压缩它。方法区可以是固定大小,也可以根据需要进行扩展。方法区的内存不需要是连续的。

Run-TimeConstantPool

运行时常量池是方法区的一部分。Claas文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(ConstantPoolTable),用于存放编译期生成各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

2.3Thread

Java程序最终运行的主体是线程,那么JVM运行时数据区可以按线程间是否共享来划分:

单个线程内共享的区:PCRegister、JVMStacks、NativeMethodstacks。

所有线程共享的区:Heap、MethodArea、Run-timeConstantpool。

Pre-Threads:

JVMSystemThreads

PerThread

ProgramCounter

Stack

NativeStack

StackRestrictions

Frame

LocalVariablesArray

OperandStack

DynamicLinking

JVMSystemThreads:

如果你使用jconsole或者其他任何debug工具,有可能你会发现有大量的线程在后台运行。这些后台线程随着main线程的启动而启动,即,在执行publicstaticvoidmain(String[])后,或其他main线程创建的其他线程,被启动后台执行。

HotspotJVM主要的后台线程包括:

VMthread:这个线程专门用于处理那些需要等待JVM满足safe-point条件的操作。safe-point代表现在没有修改heap的操作发生。这种类型的操作包括:”stop-the-world”类型的GC,threadstackdump,线程挂起,或撤销对象偏向锁(biasedlockingrevocation)

Periodictaskthread:用于处理周期性事件(如:中断)的线程

GCthreads:JVM中,用于支持不同阶段的GC操作的线程

Compilerthreads:用于在运行时,将字节码编译为本地代码的线程

Signaldispatcherthread:接受发送给JVM处理的信号,并调用对应的JVM方法

ProgramCounter(PC)

当前操作指令或opcode的地址指针,如果当前方法是本地方法,则PC值为undefined。每个CPU都有一个PC,一般来说,每一次指令之后,PC值会增加,指向下一个操作指令的地址。JVM使用PC保持操作指令的执行顺序,PC值实际上就是指向方法区(MethodArea)中的内存地址。

Stack

每一个线程都拥有自己的栈(Stack),用于在本线程中正在执行的方法。栈是一个先进后出(LIFO)的数据结构,所以当前的执行方法位于栈顶。每一个方法开始执行时,一个新的帧(Frame)被创建(压栈),并添加到栈顶。当方法正常执行返回,或方法执行时抛出一个未捕获的异常,则此帧被移除(弹栈)。栈,除了压栈和弹栈操作外,不会被执行操作,因此,帧对象可以被分配在堆(Heap)内存中,并且不需要分配连续内存。

NativeStack

不是所有的JVM都支持本地方法,然而,基本上都会为每个线程,创建本地方法栈。如果JVM使用C-Linkage模型,实现了JNI(JavaNativeInvocation),那么本地栈就会是一个C语言的栈。在这种情况下,本地栈中的方法参数和返回值顺序将和C语言程序完全一致。一个本地的方法一般可以回调JVM中的Java方法(依据具体JVM实现而定)。这样的本地方法调用Java方法一般会使用Java栈实现,当前线程将从本地栈中退出,在Java栈中创建一个新的帧。

StackRestrictions

栈可以使一个固定大小或动态大小。如果一个线程请求超过允许的栈空间,允许抛出StackOverflowError。如果一个线程请求创建一个帧,而没有足够内存时,则抛出OutOfMemoryError。

Frame

每一个方法被创建的时候都会创建一个frame,每个frame包含以下信息:

本地变量数组LocalVariableArray

返回值

操作对象栈OperandStack

当前方法所属类的运行时常量池

LocalVariablesArray

本地变量数组包含所有方法执行过程中的所有变量,包括this引用,方法参数和其他定义的本地变量。对于类方法(静态方法),方法参数从0开始,然后对于实例方法,参数数据的第0个元素是this引用。

本地变量包括:

基本数据类型

bits

bytes

boolean

32

4

byte

32

4

char

32

4

long

64

8

short

32

4

int

32

4

float

32

4

double

64

8

reference

32

4

reference

32

4

所有类型都占用一个数据元素,除了long和double,他们占用两个连续数组元素。(这两个类型是64位的,其他是32位的)

OperandStack

在执行字节代码指令过程中,使用操作对象栈的方式,与在本机CPU中使用通用寄存器相似。大多数JVM的字节码通过压栈、弹栈、复制、交换、操作执行这些方式来改变操作对象栈中的值。因此,在本地变量数组中和操作栈中移动复制数据,是高频操作。

Frame被创建时,操作栈是空的,操作栈的每个项可以存放JVM的各种类型,包括long/double。操作栈有一个栈深,long/double占用2个栈深,操作栈调用其它有返回结果的方法时,会把结果push到栈上。

下面举例说明,通过操作对象栈,将一个简单的变量赋值为0.

Java:

inti;

编译后得到以下字节码:

0:iconst_0//将0压到操作对象栈的栈顶1:istore_1//从操作对象栈中弹栈,并将值存储到本地变量1中

DyanmicLinking

每个帧都包含一个引用指针,指向运行时常量池。这个引用指针指向当前被执行方法所属对象的常量池。

当JavaClass被编译后,所有的变量和方法引用都利用一个引用标识存储在class的常量池中。一个引用标识是一个逻辑引用,而不是指向物理内存的实际指针。JVM实现可以选择何时替换引用标识,例如:class文件验证阶段、class文件加载后、高频调用发生时、静态编译链接、首次使用时。然后,如果在首次链接解析过程中出错,JVM不得不在后续的调用中,一直上报相同的错误。使用直接引用地址,替换属性字段、方法、类的引用标识被称作绑定(Binding),这个操作只会被执行一次,因为引用标识都被完全替换掉,无法进行二次操作。如果引用标识指向的类没有被加载(resolved),则JVM会优先加载(load)它。每一个直接引用,就是方法和变量的运行时所存储的相对位置,也就是对应的内存偏移量。

ShareBetweenThreads

Heap

MemoryManagement

Non-HeapMemory

JustInTime(JIT)compication

MethodArea

ClassFilestructure

classloader

FasterclassLoading

Whereisthemethodarea

RunTimeConstantpool

ExceptionTable

SymbolTable

InternedStrings(StringTable)

Heap

堆用作为class实例和数据在运行时分配存储空间。数组和对象不能被存储在栈中,因为帧空间在创建时分配,并不可改变。帧中只存储对象或者数组的指针引用。不同于原始类型,和本地变量数组的引用,对象被存储在堆中,所以当方法退出时,这些对象不会被移除。这些对象只会通过垃圾回收来移除。

YoungGeneration,年轻代-在Eden和Survivor中来回切换

OldGeneration(TenuredGeneration),老年代或持久带

PermanentGeneration

MemoryManagement

对象和数据不会被隐形的回收,只有垃圾回收机制可以释放他们的内存。

典型的运行流程如下:

a.新的对象和数组使用年轻代内存空间进行创建

b.年轻代GC(MinorGC/YoungGC)在年轻代内进行垃圾回收。不满足回收条件(依然活跃)的对象,将被移动从eden区移动到survivor区。

c.老年代GC(MajorGC/FullGC)一般会造成应用的线程暂停,将在年轻代中依然活跃的对象,移动到老年代OldGeneration(TenuredGeneration)。

区的GC会随着老年代GC一起运行。其中任意一个区域在快用完时,都会触发GC操作。

Non-HeapMemory

属于JVM内部的对象,将在非堆内存区创建。

非堆内存包括:

PermanentGeneration-themethodarea,方法区-internedstrings,字符串常量

CodeCache,代码缓存。通过JIT编译为本地代码的方法所存储的空间。

JustInTime(JIT)Compilation

Java字节码通过解释执行,然后,这种方式不如JVM使用本地CPU直接执行本地代码快。为了提供新能,OracleHotspot虚拟机寻找热代码(这些代码执行频率很高),把他们编译为本地代码。本地代码被存储在非堆的codecache区内。通过这种方式,HotspotVM通过最适当的方式,开销额外的编译时间,提高解释执行的效率。

java运行时数据区域可以按线程每个内部共享和所有线程是否共享来理解。

MethodArea

方法区中保存每个类的的详细信息,如下:

ClassloaderReference

RunTimeConstantPool

Numericconstants

Fieldreferences

MethodReferences

Attributes

Fielddata

Perfield

Name

Type

Modifiers

Attributes

Methoddata

Permethod

Name

ReturnType

ParameterTypes(inorder)

Modifiers

Attributes

Methodcode

Permethod

Bytecodes

Operandstacksize

Localvariablesize

Localvariabletable

Exceptiontable

Perexceptionhandler

Startpoint

point

PCoffsetforhandlercode

Constantpoolindexforexceptionclassbeingcaught

2.4ClassFile数据结构

Java:

ClassFile{u4magic;u2minor_version;u2major_version;u2constant_pool_count;cp_infocontant_pool[constant_pool_count–1];u2access_flags;u2this_class;u2super_class;u2interfaces_count;u2interfaces[interfaces_count];u2fields_count;field_infofields[fields_count];u2methods_count;method_infomethods[methods_count];u2attributes_count;attribute_infoattributes[attributes_count];}

magic,minor_version,major_version:JDK规范制定的类文件版本,以及对应的编译器JDK版本.

constant_pool:类似符号表,但存储更多的信息。查看“RunTimeConstantPool”章节

access_flags:class的修饰符列表

this_class:指向constant_pool中完整类名的索引。如:org/jamesdbloom/foo/Bar

super_class:指向constant_pool中父类完整类名的索引。如:java/lang/Object

interfaces:指向存储在constant_pool中,该类实现的所有接口的完整名称的索引集合。

fields:指向存储在constant_pool中,该类中所有属性的完成描述的索引集合。

methods:指向存储在constant_pool中,该类中所有方法签名的索引集合,如果方法不是抽象或本地方法,则方法体也存储在对应的constant_pool中。

attributes:指向存储在constant_pool中,该类的所有和级别的标注信息。

2.5JVM运行时内存总结图

随着JDK版本和不同厂商的实现,JVM内部模型有些细微的不同,如永久代-元数据空间等等,大体的JVM模型还是差不多。

3.JVM内存分析原理

JVM内存分析的总目的是希望能够清楚JVM各个部分的情况,然后完成TOPN统计,给出一份分析报告,方便快递定位判断问题根因。

我们一般使用jmap对正在运行的java进程做内存dump形成Hprof文件,然后下载到本地离线分析。那么我们在线分析工具面临的第一个问题就是对hprof文件的解析。

3.1Hprof数据结构

当我们使用jmap生成Hprof文件,因为它是二进制文件直接打开如下:

这种文件非常紧凑没有“分隔符”错一个字节,就会失败,通过jvm源码可以查到其有数据结构:

hprof总体结构

c++:

HPRO_FILE{header""(以0为结束)u4标识符大小,标识符用于表示,UTF8Strings、Objects、Stacktraces等,该值通常与机器CPU位数相关,32位是4,64位是8。u4highwordu4lowword高位+地位共同表达从1970年以来的毫秒数,得到dump时的时间[record]*record数组}

hprofrecord总体结构

Bash:

Record{u1Tagu4微妙,记录从header得到的时间以来[u1]*bytes数组,代表该record的内容}

hprofrecordtags列表

Recordtags列表比较长,可直接看在线源码:

Bash:

TAGBODYnotes----------------------------------------------------------HPROF_UTF8aUTF8-encodednameidnameID[u1]*utf8字符(notrailingzero)HPROF_LOAD_CLASS新加载classu4classserialnumber(class编号)idclassobjectIDu4stacktraceserialnumber(堆栈跟踪序列号)idclassnameIDHPROF_UNLOAD_CLASS卸载classu4classserial_number(class编号)HPROF_FRAMEaJavastackframeidstackframeIDidmethodnameIDidmet:normal正常-1:unknown未知-2:compiledmethod编译方法-3:nativemethod本地方法HPROF_TRACEaJavastacktraceu4stacktraceserialnumber(stacktrace编号)u4threadserialnumber(thread编号)u4numberofframes(frames数量)[id]*stackframeIDs(堆栈帧id)HPROF_ALLOC_SITESgc之后,heap分配的site点u2flags0x0001:增量与全量0x0002:按需分配与实时排序0x0004:是否强制gsu4cutoffratio(截止率)u4totallivebytesu4totalliveinstancesu8totalbytesallocatedu8totalinstancesallocatedu4numberofsitesthatfollow[u1is_array:0:normalobject2:objectarray4:booleanarray5:chararray6:floatarray7:doublearray8:bytearray9:shortarray10:intarray11:longarrayu4classserialnumber(序列号,启动时可能为0)u4stacktraceserialnumber(stacktrace序列号)u4numberofbytesalive(活着的字节数)u4numberofinstancesalive(活着的实例数)u4numberofbytesallocated(分配的字节数)u4]*numberofinstanceallocated(分配的实例数)HPROF_START_THREAD一个新的线程u4threadserialnumber(thread序列号)idthreadobjectIDu4stacktraceserialnumberidthreadnameIDidthreadgroupnameIDidthreadgroupparentnameIDHPROF_END_THREAD一个终止线程u4threadserialnumberHPROF_HEAP_SUMMARYheap概要u4totallivebytesu4totalliveinstancesu8totalbytesallocatedu8totalinstancesallocatedHPROF_CPU_SAMPLESasetofsampletracesofrunningthreadsu4totalnumberofsamplesu4ofsamplesu4]*stacktraceserialnumberHPROF_CONTROL_SETTINGSthesettingsofon/offswitchesu40x00000001:alloctraceson/off0x00000002:cpusamplingon/offu2stacktracedepthWhentheheaderis""""are:HPROF_HEAP_DUMP_SEGMENTdenoteaheapdumpsegment[heapdumpsub-records]*Thesamesub-recordtypesallowedbyHPROF_HEAP_DUMPHPROF_HEAP_DUMP_ENDdenotestheofaheapdump

HPROF_HEAP_DUMP内容较多,单独从上面抽出来:

Bash:

HPROF_HEAP_DUMP内存dump真正存放数据的地方[heapdumpsub-records]*有4中类型sub-records:u1sub-recordtypeHPROF_GC_ROOT_UNKNOWNunknownroot(未知root)idobjectIDHPROF_GC_ROOT_THREAD_OBJthreadobjectidthreadobjectID(通过JNI新创建的可能为0)u4threadsequencenumberu4stacktracesequencenumberHPROF_GC_ROOT_JNI_GLOBALJNIglobalrefroot(JNI全局引用跟)idobjectIDidJNIglobalrefIDHPROF_GC_ROOT_JNI_LOCALJNIlocalrefidobjectIDu4threadserialnumberu4frameinstacktrace(-1表示empty)HPROF_GC_ROOT_NATIVE_STACKNativestack(本地方法)idobjectIDu4threadserialnumberHPROF_GC_ROOT_STICKY_CLASSSystemclassidobjectIDHPROF_GC_ROOT_THREAD_BLOCKReferencefromthreadblockidobjectIDu4threadserialnumberHPROF_GC_ROOT_MONITOR_USEDBusymonitoridobjectIDHPROF_GC_CLASS_DUMPclass对象的dumpidclassobjectIDu4stacktraceserialnumberidsuperclassobjectIDidclassloaderobjectIDidsignersobjectIDidprotectiondomainobjectIDidreservedidreservedu4instancesize(inbytes)u2sizeofconstantpool(常量池大小)[u2,constantpoolindex,(常量池索引)ty,type2:object4:boolean5:char6:float7:double8:byte9:short10:int11:longvl]*andvalueu2numberofstaticfields[id,staticfieldname,ty,type,vl]*(不包括super)[id,instancefieldname,ty]*typeHPROF_GC_INSTANCE_DUMP正常object实例的dumpidobjectIDu4stacktraceserialnumberidclassobjectIDu4numberofbytesthatfollow[vl]*instancefieldvalues(先是class的,然后是super的,再super的super,这里只是这些字段值的bytes,还需要按字段类型转换)HPROF_GC_OBJ_ARRAY_DUMPdumpofanobjectarrayidarrayobjectIDu4stacktraceserialnumberu4numberofelementsidarrayclassID[id]*elementsHPROF_GC_PRIM_ARRAY_DUMPdumpofaprimitivearrayidarrayobjectIDu4stacktraceserialnumberu4numberofelementsu1elementtype4:booleanarray5:chararray6:floatarray7:doublearray8:bytearray9:shortarray10:intarray11:longarray[u1]*elements

HPROFtags

Bash:

enumtag{//top-levelrecordsHPROF_UTF8=0x01,HPROF_LOAD_CLASS=0x02,HPROF_UNLOAD_CLASS=0x03,HPROF_FRAME=0x04,HPROF_TRACE=0x05,HPROF_ALLOC_SITES=0x06,HPROF_HEAP_SUMMARY=0x07,HPROF_START_THREAD=0x0A,HPROF_END_THREAD=0x0B,HPROF_HEAP_DUMP=0x0C,HPROF_CPU_SAMPLES=0x0D,HPROF_CONTROL_SETTINGS=0x0E,//1.0.2recordtypesHPROF_HEAP_DUMP_SEGMENT=0x1C,HPROF_HEAP_DUMP_END=0x2C,//fieldtypesHPROF_ARRAY_OBJECT=0x01,HPROF_NORMAL_OBJECT=0x02,HPROF_BOOLEAN=0x04,HPROF_CHAR=0x05,HPROF_FLOAT=0x06,HPROF_DOUBLE=0x07,HPROF_BYTE=0x08,HPROF_SHORT=0x09,HPROF_INT=0x0A,HPROF_LONG=0x0B,//data-dumpsub-recordsHPROF_GC_ROOT_UNKNOWN=0xFF,HPROF_GC_ROOT_JNI_GLOBAL=0x01,HPROF_GC_ROOT_JNI_LOCAL=0x02,HPROF_GC_ROOT_JAVA_FRAME=0x03,HPROF_GC_ROOT_NATIVE_STACK=0x04,HPROF_GC_ROOT_STICKY_CLASS=0x05,HPROF_GC_ROOT_THREAD_BLOCK=0x06,HPROF_GC_ROOT_MONITOR_USED=0x07,HPROF_GC_ROOT_THREAD_OBJ=0x08,HPROF_GC_CLASS_DUMP=0x20,HPROF_GC_INSTANCE_DUMP=0x21,HPROF_GC_OBJ_ARRAY_DUMP=0x22,HPROF_GC_PRIM_ARRAY_DUMP=0x23}

Hprof解析

现在我们知道hprof虽然是二进制格式的文件,但其也有数据结构,就是一条一条record记录。那么解析就按照对应的格式来完成其格式解析。

核心解析伪代码:

Go:

for{r,err:=()}func(p*HProfParser)ParseRecord()(interface{},error){{//处理()}tag,err:=()iferr!=nil{returnnil,err}recordHeader,_:=()switchtag{caseTagString:(recordHeader)default:returnnil,("unknownrecordtype:0x%x",tag)}}func(p*HProfParser)ParseSubRecord()(interface{},error){tag,err:=()iferr!=nil{returnnil,err}switchtag{caseTagGcRootUnknown:()default:returnnil,("unknownheapdumprecordtype:0x%x",tag)}}

上面代码完成对Hprof文件的不停readbytes并将其解析转换成结构化的record。当我们能完成对其转换成record记录之后,面临两个问题:一个存储问题,最简单直接存储在内存中,但这种方式依赖主机的物理内存,分析大内存dump文件会受限制,一个是格式问题,最简单的是存储record的json格式,这种方式阅读性强,但弱点是数据量比较大,于是我们做了一下调研:

1Gheapdump文件预计有1300W条record记录。

2Gheapdump文件预计有2700W条record记录。

3Gheapdump文件预计有4000W条record记录。

12Gheapdump文件预计有1亿5千万条record记录。

满足insert要求只能选择LSM-tree数据结构类型的KV数据库,淘汰了图数据库。

选用存储编码后的二进制数据比存入json格式数据,在耗时和大小上均有1倍以上的提升。

综合选择了LSM-tree数据结构类型的KV数据库leveldb配合proto3进行二进制编码压缩。进过分析产出报告存入后台mongo。

3.2Hprof分析

当我们理解了jvm内存分布,理解并完成了hprof文件的解析、存储。那么剩下最后一个步完成对其分析,产出分析报告,这里我们举两个例子:1、线程分析2、大对象分析。

下面我们以下面这段代码做成jar运行,然后jmap生成文件进行分析。

Java:

#{publicstaticvoidmain(String[]args){String[]aaa=newString[100000];for(inti=0;i100000;i++){aaa[i]="aaa";}("=============");try{(300000);}catch(Exceptionee){();}}}

线程信息分析

我们本地数据库最终得到的是大量的record记录,那么这些record之间的关联关系,以及如何使用我们通过几个例子初步了解一下。(jstack能获得更详细的线程信息,从Heapdump也能获得线程信息哦),首先我们通过常用的三个线程来感受一下record的关系。

main线程:

Java:

RootThreadObject:objectid:33284953712threadserialnum:5stacktraceserialnum:6InstanceDump:objectid:33284953712stacktraceserialnum:1classobjectid:33285008752instancefieldvalues:threadLocalRandomSecondarySeed=0threadLocalRandomProbe=0threadLocalRandomSeed=0uncaughtExceptionHandler=0blockerLock=33284954824blocker=0parkBlocker=0threadStatus=225tid=1nativeParkEventPointer=0stackSize=0inheritableThreadLocals=0threadLocals=33284954176inheritedAccessControlContext=33284954136contextClassLoader=33285041480group=33284949288target=0stillborn=falsedaemon=falsesingle_step=falseeetop=1536threadQ=0priority=5name=33284954088InstanceDump:objectid:33284954088stacktraceserialnum:1classobjectid:33284980264instancefieldvalues:hash=0value=33284954112PrimitiveArrayDump:objectid:33284954112stacktraceserialnum:1numberofelements:4elementtype:charelement1:melement2:aelement3:ielement4:n

通过上面例子个跟踪我们基本能获得虽然都是record但是其不同的类型代表不一样的信息,而将他们关联的东西其实就是上面JVM运行时数据区里面的描述对应。有class--objectinstance--primitiveArray等等。这里需要读者理解JVMRun-timeDataAreas以及CLassFile的数据结构,来完成record的关系。

伪代码:

Go:

func(j*Job)ParserHprofThread()error{err:=(func(thread*)error{trace,_:=(uint64())iflen()!=0{instance,_:=()threadName:=(instance)stackTrace:=""for_,frameId:={frame,_:=(frameId)method_name,_:=()source_file_name,_:=()loadclass,_:=()className,_:=()stackStr:=""{stackStr=("%s.%s(%s:%d)\n",className,method_name,source_file_name,)}else{stackStr=("%s.%s(%s)\n",className,method_name,source_file_name)}stackTrace+=stackStr}heapThread:=HeapThread{Id:,ThreadName:threadName,ThreadStackTrace:stackTrace,}=app(,heapThread)}returnnil})iferr!=nil{returnerr}returnnil}

获得效果图:

3.3大对象分析

大对象分析思路分别获得Instance、PrimitiveArray、ObjectArray这三种对象数据进行TOPN排序。

伪代码:

Go:

func(a*Analysis)DoAnalysis(identifierSizeuint32)([]*DumpArray,uint64){varallDumpVec[]*DumpArrayvartotalDataSizeuint64classesDumpVec,classTotalDataSize:=(identifierSize)allDumpVec=app(allDumpVec,classesDumpVec)totalDataSize=(DumpArrayWrapper{allDumpVec,func(p,q*DumpArray)bool{}})returnallDumpVec,totalDataSize}func(a*Analysis)analysisClassInstance(identifierSizeuint32)([]*DumpArray,uint64){classesInstanceCounters:=make(map[uint64]*ClassInstanceCounter)_=(func(instance*)error{size:=+identifierSize+8counter:=classesInstanceCounters[]ifcounter!=nil{(,uint64(size))}else{classesInstanceCounters[]=ClassInstanceCounter{arrayObjectIds:[]uint64{},Type:0,numberOfInstance:1,maxSizeSeen:uint64(size),totalSize:uint64(size),}}returnnil})vartotalDataSizeuint64varclassesDumpVec[]*DumpArraypq:=queue_()forclassId,counter:=rangeclassesInstanceCounters{totalDataSize+=:=(classId)dumpArray:=DumpArray{Str:className,ArrayObjectIds:,Type:,NumberOfSize:,MaxSizeSeen:,TotalSize:,}(dumpArray,)()10{()}}count:=()fori:=0;icount;i++{item,_:=()array:=item.(*DumpArray)classesDumpVec=app(classesDumpVec,array)}(DumpArrayWrapper{classesDumpVec,func(p,q*DumpArray)bool{}})returnclassesDumpVec,totalDataSize}

效果图:

可以看见最大的对象就是String数组,与我们源码写的一致。

4.JVM分析平台架构

通过上面我们完成了对一个jvmheapdump文件的解析、存储、分析。于是我们更近一步完成工具平台化,支持在线分析、多JVM同时分析、支持水平扩容、支持大内存dump分析、在线开报告等等。

平台架构图:

(整体上也是微服务架构,前面网关后面跟各个模块,分析器单独运行,这样可以支持多个并发分析任务。)

使用流程图:

(对应用户来说我们提供了一键命令执行,这张图介绍一键命令背后的逻辑关系。)

成品效果图:

能看见各个分析任务的实时进度。

分析完成之后可查看详细分析报告。

5.总结与展望

本文主要介绍了得物技术保障团队在Java内存在线分析方案设计和落地的过程中遇到的问题以及解决方案,解决了研发人员对任何环境JVM实例进行快速内存Dump和在线查看分析报告,免去一些列dump文件制作、下载、安装工具分析等等。

未来得物技术保障团队计划继续开发Java线程分析,JavaGClog分析等等。形成对一个JVM实例从内存、线程、GC情况全方位分析,提升排查Java应用性能问题效率。

Reference:

《Java虚拟机规范(JavaSE8版)》

《深入理解Java虚拟机》

*文/Bruce