Java虚拟机系列三:运行时数据区解析

702 阅读7分钟

[toc]

系列索引

Java虚拟机系列一: Java文件如何被加载执行

Java虚拟机系列二: class字节码详细分析

Java虚拟机系列三: 运行时数据区解析

运行时数据区:Runtime-Data-Area,后续简称RDA.

一.为什么需要看jvm运行时数据区(RDA)?

RDA说明了指令执行,对象分配,对象存储,对象读取,对象释放等关键问题,熟悉RDA可以帮助我们分析:

  • 什么情况下会Gc , 频繁gc造成卡顿怎么办?
  • OOM/StackOverflow是怎么产生的?
  • 指令计数器会发生溢出吗?
  • 并发有什么问题,指令重排是怎么回事,volatile起什么作用?

这些问题不会全部在这一篇回答,但是实打实的,不懂RDA,就如雾里看花,似懂非懂。

二.RDA组成结构

RDA是为了方便存储运行时需要的信息,方便cpu执行指令的时候,查找对应的内容,比如类信息,对象信息,当前运行的指令行号,一些常量信息等等。

RDA 由以下五个部分组成

  • pc寄存器
  • 虚拟机栈
  • 本地方法栈
  • 方法区

其中,虚拟机栈和方法区相对比较复杂

运行时数据区

1.pc寄存器

  • 线程私有的
  • 对于Java方法,它指向方法当前执行的指令地址
  • 它的大小与系统的位宽一样,因此可以存放任何地址,也因此不会出现oom CPU位数 = CPU中寄存器的位数 = CPU能够一次并行处理的数据宽度

2.虚拟机栈

  • 线程私有
  • 是一个后进先出的栈结构
  • 存储的内容方法执行的上下文,称为栈帧,见下文分析。
  • 在方法执行过程中,开辟的栈帧数量超过上限时,会抛出StackOverflowError
  • 如果栈可以自动扩展空间,并且在扩展空间时无法申请到更多内存,会抛出OutofMemoryError

3.本地方法栈

  • 线程私有
  • 当从java调用public final native xxxx ,被调用的native方法(一般是c/c++)会进入本地方法栈
  • 如果本地方法栈需要调用java方法,会新开一个java栈帧,并压入虚拟机栈
  • 异常处理机制与虚拟机帧一致

4.堆

  • 线程共享
  • 堆空间不要求是连续的内存空间
  • 它存放对象和数组信息
  • 它不需要显示回收,回收工作交给gc处理,回收算法在后续篇幅
  • 当无法申请更多内存,抛出OutOfMemoryError
  • 新创建的对象一般分配在新生代,经过gc之后还存活的对象进入老年代,如果对象过大,新生代内存不够,也可能直接在老年代分配。

5.方法区

  • 线程间共享
  • 当方法区无法满足内存的分配需求时,会出现outOfMemoryError
  • jdk 7 之前方法区在堆的永久代中,jdk8放到元空间,不占用虚拟机内存大小
  • 存储加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存,这些信息实际上就是对class字节码中的数据做完解析之后装到方法区中的内容

jvm_method_area

具体来说,方法区的存储内容包括

类型信息(class_info)

这里的类型种类包括:class/interface/enum/annotation 对于这些类型,在方法区中保存以下内容:

  • 完整的类型名,即包名.类名
  • 类型的访问限定符 private/public/protected/static/final
  • 直接父类的完整类型名

属性/域信息(field_info)

  • 属性名称
  • 属性访问限定符
  • 属性的类型信息

方法信息 (method_info)

  • 方法名称
  • 方法访问限定符
  • 方法返回类型
  • 方法参数类型
  • 异常表 异常表标记了异常处理的指令的开始和结束范围,以及出现异常后pc寄存器应该跳转的指令位置

运行时常量池 (constant_pool)

运行时常量池的内容包括了constant_pool中定义的常量信息,一般我们把在class文件中的常量信息称为静态常量池,而加载到虚拟机方法区之后称为运行时常量池,运行时常量池的内容还会存储在运行期间产生的常量。

三.栈帧

栈帧是方法区中虚拟机栈中存储的内容,虚拟机通过它来管理方法的运行。

1.栈帧的定义

A frame is used to store data and partial results, as well as to perform dynamic linking, return values for methods, and dispatch exceptions.

根据定义,栈帧用于存放本地变量表,中间运行结果,动态链接,调用返回数据,处理异常信息以及一些附加信息(比如lock Recoder)。

局部变量表

局部变量的表,用于存放我们的局部变量(方法中的变量)。首先它是一个4字节长度,主要存放我们的Java的八大基础数据类型,如果是64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,只需要存放它的一个引用地址即可 jvm_local_var

这个例子中,我们创建了a,b,temp,f,g,e 6个局部变量,由于g,e没有被使用,编译器不会将它放到局部变量表中,并且所有的局部变量表的第一个位置放置的都是this引用。

操作数栈

存放java方法执行的操作数的先进后出的栈结构。操作的的元素可以是任意的java数据类型,一个方法刚刚开始的时候,这个方法的操作数栈是空。在运行过程中,会将中间结果存放到操作数栈中。

动态链接

Java语言特性多态,一般确定的方法比如,静态方法或者private方法,不会被重载,因此可以载编译期间,将对方法的引用索引直接替换为方法的地址,这个过程叫做静态绑定。

如果方法不能载编译器确定,那仍然需要保留方法的索引,根据索检索运行时常量区中真实的方法的地址。

方法出口地址

方法调用结束之后,会根据方法出口地址,将pc寄存器中的内容改为方法出口地址 + 1的值,这样当当前方法的栈帧出栈后,pc寄存器就能找到下一条指令继续执行。

异常处理

每个方法都会有一个异常表,当方法中出现try/catch逻辑时,异常表会记录异常表的作用范围,以及出现异常后的跳转地址

jvm_method_exception

如图包含一个try/catch方法,它生成的异常表跳转位置为第6条pc指令,对应源码的位置为第5行,也就是 catch(Exception e) 这个位置。

附加信息

附加信息中比较重要的是lockRecoder,它用于在对象在多线程访问时,记录锁对象的信息,当对象中的偏向锁升级为轻量级锁和重量级锁时,对象的markword中存放的是指向栈中lockRecoder的引用.

2.栈帧的工作原理

线程在运行时,在执行每个方法的时候都会打包成一个栈帧,存储了局部变量表,操作数栈,动态链接,方法出口等信息,然后放入栈。每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程

jvm_stack_frame

如图,方法一调用方法二,方法二调用方法三,对应虚拟机栈中就存在三个栈帧,栈顶元素为方法三的栈帧,方法三正常执行结束后会根据方法返回地址,返回方法二中当前指令行,依次类推。