JVM(Java 虚拟机)是Java语言实现平台无关性的具体实现,是一个虚拟的计算机设备。
以Java SE 7为标准,Java虚拟机在执行Java程序时把所管理的内存划分为若干不同的数据区域:
由于下面有些讲解的需要,先说明几个概念:
方法区(Method Area)[线程共享区域]
是各个线程共享区域,方法区是保存类的信息:
- 包括常量、类的类型(Class or Interface)、访问修饰符(public private等)等;
- 运行时常量池(Runtime Constant Pool),用于存放字段、方法信息、静态变量等(可以被多次调用)。
举个栗子:
0. main创建一个类的对象
- 一开始内存中是没有该类的信息的(不可能一开始就把所有类的信息加载到内存中)
- 类加载器(ClassLoader)首先定位到这个类的.class文件
- 然后读取这个文件(IO操作> ),JVM提取类信息,并将其存到方法区中。
- 下一次在需要创建该类的对象时,就从方法区中取该类的信息。
方法区的大小可以可以动态调整的,当无法再扩展时就抛出 OutOfMemoryError。
当方法区中的某各类不在被需要(不可达时),该类的信息就被卸载,即被垃圾回收。
堆(Heap)[线程共享区域]
几乎所有的对象实例都在堆上分配,即堆空间的唯一目的就是用于存放对象。
Java虚拟机规范对其描述是:堆是所有实例、数组分配内存的运行时数据区。
也因此,堆是垃圾回收作用的空间。
目前大部分的 JVM 实现,堆的大小都是可扩展的(通过-Xmx和-Xms来控制)
如果在堆中没有内存完成实例分配,并且堆无法再扩展时,会抛出OutOfMemoryError。
本地方法栈(Native Method Stack)[线程私有]
和虚拟机栈类似,不过该部针对的不是java方法,而是native方法。
同样会抛出StackOverflowError和OutOfMemoryError。
程序计数器(Program Counter Register)[线程私有]
直译应该是PC寄存器:用于存放下一条指令所在的地址。
存在两种情况:
- 正在执行的是Java方法,PC寄存器存的则是正在执行的编译好的字节码的指令的地址;
- 正在执行的是Native方法,PC寄存器为空。
该区域是唯一一个不会OutOfMemoryError的区域。
虚拟机栈(VM Stack)[线程私有]
实际上虚拟机栈(VM Stack)就是按照这种方式运行的:
虚拟机栈是这样描述java方法执行的:每个方式执行时都会创建一个栈帧(Stack Frame),用于保存局部变量表,操作数栈,动态链接,返回地址等。
每一个方法从调用直至执行完成的过程,对应着一个栈帧在vm stack中入栈到出栈的过程
局部变量表
顾名思义,局部变量表就是用于存放编译期间各种变量的值(基本类型变量)、引用对象地址、returnAddress的空间。
- 对于基本类型的变量(8种),其值是直接保存在局部变量表中的
- 引用类型(String,对象)是保存了一个该引用的地址,而对象的实际数据是在堆(Heap)中
- 函数返回的地址(returnAddress),可以理解为该函数返回时应该返回到哪里
操作数栈
操作数栈中保存的也是数据,但不同的是,操作数栈的作用是实际完成函数中的具体操作,比如各种运算等。
熟悉栈的都知道,一般栈可以用来完成各种操作运算。实际上在java虚拟机中,这种运算操作就是通过栈来实现的。
为了理解局部变量表和操作数栈的区别以及两者的工作原理,可以看下图:
图中函数实现的功能是计算和。
局部变量表一开始就有一个该函数的数据变量的快照,即保存了所有变量,然后使用操作数栈来完成具体的计算操作,先是两个操作数入栈(a,b),然后出栈进行运算,运算结果再入栈,最后结果保存到局部变量表的变量c中。
由此可见,虚拟机栈也是线程私有的,其生命周期和线程相同。
关于局部变量表和操作数栈,参考博客:局部变量表和操作数栈
动态链接
动态链接即表明该栈帧指向方法区运行时常量池的引用地址。
实际上函数是保存在方法区中的运行时常量池中的,那么如何知道栈帧是属于哪一方法呢?这就是动态链接的作用了。
方法返回地址
顾名思义,就是该方法返回时返回到先前调用该方法的地址的地方,以便程序能接着执行。
异常
在Java虚拟机规范中,对于虚拟机栈规定了两种异常状况:
StackOverflowError
在虚拟机栈中和其操作数栈中,栈都是有深度限制的,若栈帧数或者操作数的数量超过了其栈的允许值,那么就会报StackOverflowError。
OutOfMemoryError
如果虚拟机栈的数量限制可以动态扩展(大部分虚拟机都支持动态扩展),而在扩展时无法申请到足够的内存,则会抛出OutOfMemoryError。