[toc]
系列索引
运行时数据区: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字节码中的数据做完解析之后装到方法区中的内容
具体来说,方法区的存储内容包括
类型信息(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位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,只需要存放它的一个引用地址即可
这个例子中,我们创建了a,b,temp,f,g,e 6个局部变量,由于g,e没有被使用,编译器不会将它放到局部变量表中,并且所有的局部变量表的第一个位置放置的都是this引用。
操作数栈
存放java方法执行的操作数的先进后出的栈结构。操作的的元素可以是任意的java数据类型,一个方法刚刚开始的时候,这个方法的操作数栈是空。在运行过程中,会将中间结果存放到操作数栈中。
动态链接
Java语言特性多态,一般确定的方法比如,静态方法或者private方法,不会被重载,因此可以载编译期间,将对方法的引用索引直接替换为方法的地址,这个过程叫做静态绑定。
如果方法不能载编译器确定,那仍然需要保留方法的索引,根据索检索运行时常量区中真实的方法的地址。
方法出口地址
方法调用结束之后,会根据方法出口地址,将pc寄存器中的内容改为方法出口地址 + 1的值,这样当当前方法的栈帧出栈后,pc寄存器就能找到下一条指令继续执行。
异常处理
每个方法都会有一个异常表,当方法中出现try/catch逻辑时,异常表会记录异常表的作用范围,以及出现异常后的跳转地址
如图包含一个try/catch方法,它生成的异常表跳转位置为第6条pc指令,对应源码的位置为第5行,也就是 catch(Exception e) 这个位置。
附加信息
附加信息中比较重要的是lockRecoder,它用于在对象在多线程访问时,记录锁对象的信息,当对象中的偏向锁升级为轻量级锁和重量级锁时,对象的markword中存放的是指向栈中lockRecoder的引用.
2.栈帧的工作原理
线程在运行时,在执行每个方法的时候都会打包成一个栈帧,存储了局部变量表,操作数栈,动态链接,方法出口等信息,然后放入栈。每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程
如图,方法一调用方法二,方法二调用方法三,对应虚拟机栈中就存在三个栈帧,栈顶元素为方法三的栈帧,方法三正常执行结束后会根据方法返回地址,返回方法二中当前指令行,依次类推。