当前我们微服务容器化部署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等分析工具 |
在线分析、在线分析报告 |
优点:
不足:
|
首先我们快速过一下Java 的内存模型, 这部分不必深入,稍微了解不影响第三部分 JVM 内存分析原理。可回过头来再看。
JVM 内存模型可以从共享和非共享理解,也可以从 stack,heap 理解。GC 主要作用于 heap 区, stack 的内存存在系统内存。
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 区。
JVM 有一个在所有 JVM 线程之间共享的堆。堆是运行时数据区,从中分配所有类实例和数组的内存。
堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象永远不会被显式释放。JVM 没有假设特定类型的自动存储管理系统,可以根据实现者的系统要求选择存储管理技术。堆的内存不需要是连续的。
Method Area
JVM 有一个在所有 JVM 线程之间共享的方法区。方法区类似于常规语言编译代码的存储区,或类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量轮询、字段和方法数据,以及方法和构造函数的代码,包括在类和实例初始化和接口初始化中使用的特殊方法。
Method 区域是在虚拟机启动时创建的。尽管方法区在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾收集或压缩它。方法区可以是固定大小,也可以根据需要进行扩展。方法区的内存不需要是连续的。
Run-Time Constant Pool
运行时常量池是方法区的一部分。Claas 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
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.
Java:
int i;
编译后得到以下字节码:
C:
0: iconst_0 // 将0压到操作对象栈的栈顶
1: istore_1 // 从操作对象栈中弹栈,并将值存储到本地变量1中
Dyanmic Linking
每个帧都包含一个引用指针,指向运行时常量池。这个引用指针指向当前被执行方法所属对象的常量池。
当Java Class被编译后,所有的变量和方法引用都利用一个引用标识存储在class的常量池中。一个引用标识是一个逻辑引用,而不是指向物理内存的实际指针。JVM实现可以选择何时替换引用标识,例如:class文件验证阶段、class文件加载后、高频调用发生时、静态编译链接、首次使用时。然后,如果在首次链接解析过程中出错,JVM不得不在后续的调用中,一直上报相同的错误。使用直接引用地址,替换属性字段、方法、类的引用标识被称作绑定(Binding),这个操作只会被执行一次,因为引用标识都被完全替换掉,无法进行二次操作。如果引用标识指向的类没有被加载(resolved),则JVM会优先加载(load)它。每一个直接引用,就是方法和变量的运行时所存储的相对位置,也就是对应的内存偏移量。
Share Between Threads
Heap
Memory Management
Non-Heap Memory
Just In Time(JIT) compication