当前位置:网站首页 > 编程语言 > 正文

jvisualvm分析dump实战(jvisualvm如何分析dump文件)



1.前言

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

常见的JVM heap dump analysis 工具如: 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.1 Run-Time Data Areas

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


    • PC Register

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

    • JVM Stacks

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

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

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

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

    • Native Method Stack

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

    2.2 Heap

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

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

    • Method Area

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

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

    • Run-Time Constant Pool

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

    2.3 Thread

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

    单个线程内共享的区: PC Register、JVM Stacks、Native Method stacks。

  • 所有线程共享的区: Heap、Method Area、Run-time Constant pool。

    • Pre-Threads:

    JVM System Threads

    Per Thread

    Program Counter

    Stack

    Native Stack

    Stack Restrictions

    Frame

    Local Variables Array

    Operand Stack

  • Dynamic Linking



    • JVM System Threads

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

    Hotspot JVM 主要的后台线程包括:

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

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

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

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

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



    • Program Counter (PC)

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

    • Stack

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

    • Native Stack

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

    • Stack Restrictions

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

    • Frame

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

    本地变量数组 Local Variable Array

    返回值

    操作对象栈 Operand Stack

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

    • Local Variables Array

    本地变量数组包含所有方法执行过程中的所有变量,包括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位的)

    • Operand Stack

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

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

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





    • Dyanmic Linking

    • Share Between Threads

    Heap

  • /code>

    • Heap

    • Memory Management

    • Non-Heap Memory

    • Just In Time (JIT) Compilation

    • Method Area

    Classloader Reference

  • Run Time Constant Pool

  • Numeric constants

  • Field references

  • Method References

  • Attributes

  • Field data

  • Per field

  • Name

  • Type

  • Modifiers

  • Attributes

  • Method data

  • Per method

  • Name

  • Return Type

  • Parameter Types (in order)

  • Modifiers

  • Attributes

  • Method code

  • Per method

  • Bytecodes

  • Operand stack size

  • Local variable size

  • Local variable table

  • Exception table

    Per exception handler

  • Start point

  • End point

  • PC offset for handler code

  • Constant pool index for exception class being caught

    2.4




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

  • constant_pool:类似符号表,但存储更多的信息。查看“Run Time Constant Pool”章节

  • 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中,该类的所有RetentionPolicy.CLASS和RetentionPolicy.RUNTIME级别的标注信息。

    2.5 JVM 运行时内存总结图


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

    3.JVM 内存分析原理

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

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

    3.1 Hprof 数据结构

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


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

    https://hg.openjdk.java.net/jdk/jdk/file/ee1d592a9f53/src/hotspot/share/services/heapDumper.cpp#l62

    • hprof 总体结构

    c++:




    hprof record总体结构




    • hprof record tags 列表

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

    https://hg.openjdk.java.net/jdk/jdk/file/ee1d592a9f53/src/hotspot/share/services/heapDumper.cpp#l87

    Bash:




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

    https://hg.openjdk.java.net/jdk/jdk/file/ee1d592a9f53/src/hotspot/share/services/heapDumper.cpp#l175

    Bash:




    • HPROF tags
    Bash:

    • Hprof 解析

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

    核心解析伪代码:

    Go:




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


    1G heap dump 文件预计有1300W 条 record 记录。

    2G heap dump 文件预计有2700W 条 record 记录。

    3G heap dump 文件预计有4000W 条 record 记录。

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

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

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

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

    3.2 Hprof 分析

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

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

    Java:



    • 线程信息分析

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

    main 线程:




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

    伪代码:

    Go:




    获得效果图:


    3.3 大对象分析

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

    伪代码:

    Go:




    效果图:


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

    4.JVM分析平台架构

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

    平台架构图:


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

    使用流程图:


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

    成品效果图:


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


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

    5.总结与展望

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

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

    Reference:

    《Java 虚拟机规范(Java SE 8 版)》

    《深入理解Java 虚拟机》

    https://www.taogenjia.com/2020/06/19/jvm-runtime-data-areas/

    https://wu-sheng.github.io/me/articles/JVMInternals.html

    https://wu-sheng.github.io/me/articles/JVMInternals-p2.html

    *文/Bruce

    社区运营中转化与留存——四大常见问题

    到此这篇jvisualvm分析dump实战(jvisualvm如何分析dump文件)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!















    版权声明


    相关文章:

  • ip地址换了怎么连打印机(ip地址换了打印机怎么设置)2025-06-25 22:36:04
  • 重绘和回流和重排(重绘和回流和重排的区别)2025-06-25 22:36:04
  • 拆包英文(拆包英文游戏)2025-06-25 22:36:04
  • 手机号被频繁发送验证码(手机号被频繁发送验证码怎么回事)2025-06-25 22:36:04
  • win10和linux双系统卸载linux(win10卸载linux子系统)2025-06-25 22:36:04
  • XP虚拟机安卓版(winxp虚拟机安卓版)2025-06-25 22:36:04
  • gjk(gjk算法)2025-06-25 22:36:04
  • u盘怎么安虚拟机(u盘怎么安虚拟机系统)2025-06-25 22:36:04
  • 如何破解pdf文件(怎样破解pdf密码破解)2025-06-25 22:36:04
  • dv试验和pv试验(汽车dv试验和pv试验)2025-06-25 22:36:04
  • 全屏图片