JVM运行时数据区简介

概述

在一个程序执行时JAVA虚拟机定义了一些运行时数据区域。其中一些数据区域是生命周期于虚拟机相同,另一些区域生命周期于线程相同。与JVM同周期的的数据区域线程共享,与线程同生命周期的区域各线程独享。
JVM运行时数据区

本文从线程是否共享出发,简述JVM内各部分数据区域的作用。本文主要参考JVM官方说明,也可以说是对官方文档的翻译,但增删了小部分内容。

线程共享

方法区(Method Area)

JAVA虚拟机有个方法区,被各个线程共享。方法区类似于传统语言存储编译后代码的区域或者操作系统的TEXT段。它存储每个类的结构比如:运行时常量池,类字段和方法的数据,以及方法和构造函数的代码包括类和实例初始化以及接口初始化中使用的特殊方法。
方法区在JVM启动时创建。虽然方法区逻辑上是堆的一部分,但是JVM在实现时可以选择既不垃圾回收也不压缩它。JVM规范不规定方法区管理策略以及存储的地方。方法区可以是不连续的内存。在运行java程序时可以使用“-XXPermSize”设置方法区初始大小,使用“-XXMaxPermSize”设置最大值。
如果方法区无法满足分配请求,JVM会抛出“OutOfMemoryError”。但并不是所有OOM都是方法区的问题,还可能时Heap的问题。
需要注意的是,在HotSport中将永久带作为方法区。

运行时常量池(Run-Time Constant Pool)

运行时常量池会在类加载时载入class文件中的常量池信息(constant_pool table)。它包含多种常量,从编译时已知的数字、字符串到必须在运行时解析的方法和属性引用。运行时常量池功能类似于传统编程语言的符号表(symbol table),但比典型的符号表要宽。
运行时常量池时方法区的一部分。当JVM创建类或接口时会构造其常量池。
当构造类或接口时,运行时常量池所需的内存大于方法区可用内存时,JVM会抛出OutOfMemory。

堆(heap)

JVM有个堆区域被各个线程共享。堆是内存中用于存所有类实例和数组的运行时数据区域。 JVM将堆内存分为年轻代(Yong generation)和老年代(Old generation),年轻代还划分为3个内存池,新生代(Eden space)和存活区(Survivor space), 在大部分GC算法中有2个存活区(S0, S1),在我们可以观察到的任何时刻,S0和S1总有一个是空的,但一般较小,也不浪费多少空间。
堆在JVM启动时创建。垃圾回收器管理的主要区域。堆中存储的对象只能被GC回收,永远不会被显示的释放。GC的实现策略有很多种,可根据需要选择。堆可以是不连续的内存。在运行JAVA时可以通过“Xms”指定JVM初始时堆的大小,通过“Xmx”指定JVM最大堆的大小。
如果计算时需要的堆大小超过了JVM可用的最大值,那么JVM会抛出“OutOfMemoryError”。

各线程独享

程序计数器(Program counter Register)

在JVM种每个线程都有自己的程序计数器。在每个时刻,每个线程都再执行某个方法的代码,即该线程的当前方法。如果这个方法不是原生方法(native methods),那么PC寄存器存储当前正在被执行的指令地址。如果正在执行原生方法,则程序计数器种的值是未定义。程序计数器是足够宽的,可以在特定平台保存returnAddress或native pinter。

JVM栈(JAVA Virtual Machine Stacks)

每个线程都有JVM栈,它和线程同时创建。JVM栈存储栈帧(见后)。JVM栈类似于传统语言(比如C)的栈,它保存局部变量和部分结果,并参与方法调用和返回。JVM除了压入弹出栈帧外不会被直接操作,所以栈帧可以有堆来分配。JVM栈使用的内存可以不连续。在运行JAVA时通过设置”-Xss”可以影响每个线程的JVM栈大小,默认1M,在线程较多且线程栈不深的情况可以适当调小为128K\256K。
如果线程所需JVM栈超过设置(如无返回的递归调用),则JVM虚拟机会抛出StackOverflowError。
如果JVM栈是可动态扩展的,当扩展时没有足够内存或者没有足够内存为新线程初始化JVM栈时,JVM会抛出OutOfMemoryError。

栈帧

栈帧用来存储数据和部分结果,同时也用来执行动态连接、返回方法值和抛出的异常。
在方法被调用时就会创建栈帧。当方法调用完成栈帧将被销毁,不是正常退出还是抛出异常。栈帧从线程所使用的JVM栈中分配。每个栈帧都由自己的局部变量(local variables),操作栈(operand stack),该方法所在类的运行时常量池的引用。
可以使用特定的一些用于实现的信息扩展栈帧,比如debug信息。
局部变量数组和操作栈的大小在编译时决定,与大小同时提供的还有与栈帧相关的方法代码。因此,栈帧的大小仅取决于JVM的实现,在方法调用时就可以确定分配给这个栈帧多少内存。
在一个线程中只有一个栈帧是活动的,即正在执行的方法的栈帧。这个帧被称为当前帧(current frame),这个方法即为当前方法(current method.)。当前方法所在的类被定义为当前类(current class)。局部变量和操作栈的操作通常引用当前帧。
当方法调用其它方法或该方法执行完成,当前栈帧会发生改变。当一个方法被调用,控制转移到新方法时,一个新帧被创建并成为当前帧。在方法返回时,当前帧将执行结果(如果有的话)传递回前一个栈帧。当前帧被抛弃,上一个帧成为当前帧。
注意,线程不能引用其它线程创建的栈帧。

局部变量(local variables)

每个栈帧都包含一个变量列表被成为局部变量。栈帧中局部变量数组的大小在编译时决定,保存在类编译后的class文件中的code属性。单个局部变量可以保存一个boolean、byte、char、short、int、float、引用或返回地址的值。两个局部变量能保存long或者double。
局部变量通过索引寻址。第一个局部变量的索引是0。只有范围在0到局部变量大小的整数才能被认为是局部变量索引。
long与double类型占据两个连续的局部变量。该值通过第一个变量的索引寻址。例如,一个double类型的值存储在局部变量索引为n(其实占据了局部变量n和n+1的索引),但使用n+1无法索引到该值。n+1可以存储其它值,但是,这样会使本地变量n的内容无效。
JVM 不需要n为偶数。在直观上来讲,类型double和long在局部变量数组中不需要是64位对齐的,JVM实现可以自由决定用合适的方法诸如用两个局部变量来存储该值。
Java虚拟机使用局部变量在方法调用上传递参数。一个类方法引用中的任何参数通过连续的局部变量来传递(从局部变量0开始)。在一个实例方法引用中,局部变量0通常用来传递调用该方法对象实例的引用。其后任何参数通过从局部变量1开始的连续局部变量传递。

操作栈(Operand Stacks)

每个栈帧都有一个后进先出(LIFO)栈,被称为操作栈。一个栈帧的操作栈最大深度在编译时决定。如果上下文明确,我们会把当前栈帧的操作栈简称为操作栈。
当栈帧创建时,其中的操作栈是空的。通过JVM提供的指令加载常数、局部变量的值或域到操作栈。其他JVM指令从操作栈中获取操作数,对它们进行操作,并将结果推回操作数堆栈。操作栈同时也用来准备参数传递给方法,接收方法的返回值。
例如,iadd指令将两个整数值相加。它要求相加的两个整数值是操作数栈顶的两个值(由以前的指令压入在那里)。两个整数值都从操作数栈弹出,他们相加,相加的和压回操作数栈。子计算可以嵌套在操作数栈上,其结果值可以被相邻的计算使用。
每个操作栈项可保存任何JVM类型的值,包括long和double。
操作栈的值必须用与他们的值类型相适当的方法来操作。例如,压入两个int值把它们当作long来处理或者压入两个float值随后通过iadd指令将他们相加,是不可能的。一小部分JVM指令(dup和swap)在运行数据区域操作作为原始值而不需要关心他们的类型,这些指令的定义方式使它们不能用于修改或分解单个值。class文件校验器保证了操作栈的限制。
在任何时间点操作数栈有一个关联的深度,long和double类型深度为两个单位,其他类型深度为一个单位。

动态链接(Dynamic Linking)

每个栈帧都包含一个当前方法所在类型的运行时常量池的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。在class文件里,一个方法如果要调用其它方法,或者访问成员变量,则需要通过符号引用(symbolic reference)来表示,动态连接的作用就是将这些以符号引用表示的方法转换为对实际方法的直接引用。类加载的过程中将要解析尚未转换的符号引用,并将对变量的访问转化为变量在程序运行时,位于存储结构中的正确偏移量。
由于对其它类中的方法和变量进行了晚期绑定(late binding),所以即使哪些类发生变化,也不会影响调用它的方法。

方法正常结束(Normal Method Invocation Completion)

方法调用在没有引起异常抛出时正常的结束(异常直接由JVM ,或者执行显示throw语句抛出)。如果当前方法调用正常的结束,则一个值可以返回到正在调用该方法的方法。这在调用的方法执行return指令时发生,指令返回的选择必须与被返回值的类型(如果有)相合适。
方法正常结束,当前栈帧需要用来恢复调用者的状态(包括局部变量,操作数栈,适当增加调用者的程序计数器)。返回值(如果有)压入调用方法帧的操作栈中,程序继续运行。

方法异常结束(Abrupt Method Invocation Completion)

方法调用异常是指某些指令导致了JAVA虚拟机抛出异常,并且虚拟机派出的异常在该方法中没有办法处理,或者在执行过程中于道了athrow指令并显式的派出异常,同时该方法内部没有捕获异常。如果方法异常结束,那么一定不会有值返回给调用者。

本地方法栈(Native Method Stacks)

Java虚拟机可以使用传统的栈,俗称”C栈(C stacks)”,用来支持原生方法(用JAVA以外的语言编写的方法)。用C语言实现的JVM指令集解释器也可以使用本地方法栈。不加载本地方法且不依赖传统栈的JVM实现不需要支持本地方法栈。如果JVM支持本地方法栈,那么通常在创建线程时为其分配本地方法栈。可以使用”-Xoss”设置本地方法栈大小,但在HotSport中无效,因为HotSport中本地方法栈和JVM栈时一体的,都由”-Xss”设置。
它的报错方式和JVM栈一样,不再赘述。

参考

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
https://286.iteye.com/blog/1928180