JVM-运行时内存结构

73 阅读8分钟

JVM运行时内存结构

image.png

  • 元空间(方法区):线程共享,用于存放虚拟机加载的类信息、运行时常量池、静态变量、类的方法定义、属性等所有字节码指令。

    • 运行时常量池程序运行期间,JVM会将字节码文件中的常量池加载到内存中,存放在运行时常量池中

    • 字节码文件中的常量池:存储的是在编译期间解析的各种类型、方法、字段等的符号引用,以及字面量等常量值,以支持程序的动态链接和执行

  • :线程共享,存放对象实例

  • :线程私有。每个线程有一个栈,每个方法对应一个栈帧(stack frame),方法调用对应于栈帧的入栈和出栈

    • java虚拟机栈:为虚拟机上执行java方法服务

    • 本地方法栈:为虚拟机上执行native方法服务

  • PC/程序计数器:线程私有,PC中存储的引用,指向下一条即将执行的指令

内存溢出

理论上会出现OOM的区域:(除了pc应该都有可能出现)

  • 堆空间:无法创建对象,且堆空间无法扩展 java.lang.OutofMemoryError:Java heap space

    • 出现原因
      • 存在内存泄漏,导致无用对象长时间占用内存空间
      • 代码中存在大对象分配
    • 解决方案
      • 加大堆内存-Xmx
      • 找到内存泄漏的地方,进行代码优化,及时销毁不需要的对象
      • 优化大对象的加载方式
  • 栈区:

    • 出现原因
      • 虚拟机栈区:如果线程调用深度大于虚拟机允许的最大深度,抛出stackOverFlowError(递归调用)
      • 多数java虚拟机都支持动态扩展虚拟机栈的大小,直到内存不足时,抛出oom。(创建大量的线程)
    • 解决方案
      • 增加栈内存-Xss
      • 优化代码,减少递归调用
  • 元空间

    • 出现原因
      • 使用大量动态生成类的框架(动态代理技术、热部署工具等)
      • 程序代码中大量使用反射,反射在大量使用时,因为使用缓存的原因,会导致ClassLoader和它引用的Class等对象不能被回收

内存泄漏

内存泄漏指的是,程序在申请内存后,无法释放已申请的空间

栈帧

jvm中,方法执行时的数据结构。栈帧的核心结构是:局部变量表和操作数栈。每个方法对应一个栈帧。

image.png

  • 局部变量表

    局部变量表是用于存放方法的参数和方法内部定义的局部变量,都是基本数据类型或对象引用。表内的元素不能互相赋值,必须通过操作数栈完成。

    非静态方法的局部变量表当中索引为0的位置在还没有进行初始化操作,就是存uninitialized_this变量;如果已经进行了初始化操作,就是存储this变量,参数依次存储在索引1开始的位置。而静态方法中参数依次存储在索引0开始的位置

    局部变量表的容量以slot为最小单位,一个slot可以存储一个32位的数据,所以一个局部变量占用的不一定是一个slot

  • 操作数栈

    方法开始执行时,操作数栈是空的,随着字节码指令的执行,会有数据入栈和出栈。操作数栈中的数据类型,必须要与当前需要执行的字节码指令匹配。

  • 一个引用【支持动态连接】

    栈帧中会存一个引用:指向当前方法所属类的运行时常量池

    多态:

    通过栈帧中的引用找到当前类的运行时常量池后,对被调用方法进行符号解析,拿到被调用方法的描述符

    invokevirtual 指令在运行时的解析过程可以分为以下几步:【多态的原理】

    • ①、找到操作数栈顶的元素所指向的对象的实际类型,记作 C。
    • ②、如果在类型 C 中找到与常量池中的描述符匹配的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;否则返回 java.lang.IllegalAccessError 异常。
    • ③、否则,按照继承关系从下往上一次对 C 的各个父类进行第二步的搜索和验证。
    • ④、如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常
  • 返回地址

    正常退出:一定是执行了某种RETURN指令。可能会有返回值传递给上层调用者

    一般来说,方法正常退出的时候,PC 计数器的值会作为返回地址,栈帧中很可能会保存这个计数器的值,异常退出时则不会。

栈帧映射

java6以后引入了栈帧映射,用于jvm快速校验字节码,加速了类型安全检查过程。是验证数据,不直接参与方法执行。但是栈帧是运行时数据结构的一部分。

stackMapTable中存储了一系列的栈帧映射【此刻栈、local的数据变化情况】。每个映射实例,都反映了与上一个映射之间的变化关系,如下:

image.png 在1.6之前,是将所有可能得分支都列出来,进行类校验。1.6引入了栈帧映射,可快速进行类型安全检查。jvm优化后,规定每个条件分支后必须有一个映射表示栈帧的变化

stackMapTable在字节码阶段就会生成,用于运行时快速进行类的校验,如下图,展示了一个静态方法以及其字节码

fun t(flag: Boolean) {
    var num = 0
    if (flag) {
        num = 1
    } else {
        num = 2
    }
    println(num)
}

  // access flags 0x19
  public final static t(Z)V
   L0
    LINENUMBER 16 L0
    ICONST_0
    ISTORE 1 //将num赋值为0
   L1
    LINENUMBER 17 L1
    ILOAD 0
    IFEQ L2
   L3
    LINENUMBER 18 L3
    ICONST_1
    ISTORE 1 //将num赋值为1 [假如后续我们删除这行指令]
    GOTO L4
   L2
    LINENUMBER 20 L2
   FRAME APPEND [I] //栈帧扩充一个local I类型变量 此时栈帧:[I, I][]
    ICONST_2
    ISTORE 1 //将num赋值为2
   L4
    LINENUMBER 22 L4
   FRAME SAME //标识栈帧保持不变,此时栈帧为:[I, I][]
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ILOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (I)V
   L5
    LINENUMBER 23 L5
    RETURN
   L6
    LOCALVARIABLE num I L1 L6 1
    LOCALVARIABLE flag Z L0 L6 0
    MAXSTACK = 2
    MAXLOCALS = 2

为什么要有类校验

jvm加载类后,会进行校验,主要进行元数据验证(类文件相关信息)、字节码验证、控制流验证等,为了保证类的运行时符合安全规范,避免潜在风险。

这其中,字节码验证是要确保字节码能够被正确执行,这其中会进行类型安全检查。

类型安全:通过任何分支到达同一指令处,操作数栈和本地变量的数据类型需要始终保持一致,避免出现类型错误。该检查就是使用stackmapTable进行的。

实例说明类型安全校验的重要性:为了保证程序运行的正确性

如上述代码中,调用这个t(true)方法,正常运行应该输出1 image.png 如果删除if分支中对于num赋值的指令,继续运行就会报错,verifyError,提示栈帧映射不一致。如果关闭校验(运行时假如-noverify),程序能正常运行,但是显然并不符合逻辑预期。 image.png

asm如何写stackMapTable实例

MethodVisitor.visitFrame()用于写入栈帧映射,在每个跳转分支后的visitLabel调用之后调用。

public void visitFrame(int type,
 int numLocal,
 Object[] local,
 int numStack,
 Object[] stack)

详解visitFrame方法参数:

  1. type

    • 取值:
      • F_NEW:ClassWriter被指定为扩展栈帧映射时使用
      • F_SAME:栈帧相较于之前没有变化,并且栈为空
      • F_SAME!:局部变量表没有变化,栈上有一个元素
      • F_APPEND:栈帧相较于之前,局部变量表上向后扩充1,2,3个元素
      • F_CHOP:栈帧相较于之前,局部变量表上少了1,2,2个元素,栈为空
      • F_FULL:表示完整的栈帧内容【尽可能不用,不然会导致信心臃肿】
  2. numLocal:变化的局部变量的个数

  3. object[] local:变化的局部变量元素,类型描述符,如果这个slot为空(不是为空对象)则用TOP代替。该数组的长度为numLocal

  4. numstack 如上

  5. object[] stack 如上

ASM自动计算frame

构建ClassWriter时,可以设定flags属性,来决定当前类中需要自动计算什么

/**
flag:
COMPUTE_FRAMES:自动计算栈帧映射、stack和local的最大容量值
COMPUTE_MAXS: 自动计算stack和local的最大容量值
0:什么都不自动计算
*/
public ClassWriter(final ClassReader classReader, final int flags) {
    super(/* latest api = */ Opcodes.ASM9);
    symbolTable = classReader == null ? new SymbolTable(this) : new SymbolTable(this, classReader);
    if ((flags & COMPUTE_FRAMES) != 0) {
      this.compute = MethodWriter.COMPUTE_ALL_FRAMES;
    } else if ((flags & COMPUTE_MAXS) != 0) {
      this.compute = MethodWriter.COMPUTE_MAX_STACK_AND_LOCAL;
    } else {
      this.compute = MethodWriter.COMPUTE_NOTHING;
    }
}