-
运行时数据区域
- 根据《《Java虚拟机规范》》的规定,Java虚拟机所管理的内存包括以下几个运行时数据区域。 其中方法区和堆都是线程“共享”的,而虚拟机栈,本地方法栈和程序计数器则是每个线程“私有“的数据区。
值得注意的是,上图运行时数据区域的划分只是概念模型,就像OSI七层模型一样,实际实现时并不是严格按图所示划分,而且不同的虚拟机实现方式也不同,这里主要讨论HotSpot VM。
-
程序计数器
- 程序计数器是一块较小的内存空间,我们都知道java代码都是以字节码指令的形式在jvm上执行(先不考虑JIT),而程序计数器就可以看做当前线程执行的字节码指令的行号指示器。 通过改变计数器的值就可以改变下一条要执行的指令,也就是说代码的分支,循环,跳转,异常处理,线程恢复等功能都需要依赖计数器来完成。
- 程序计数器是唯一一个没有规定任何OutOfMemoryError的数据区。
当执行的是java方法,计数器记录的是字节码指令的地址,而执行Native方法时,计数器的值为空(Undefined)。
-
Java虚拟机栈
- 虚拟机栈是描述java方法执行的内存模型,它的本质是一个由栈帧组成的栈,每个栈帧都用于存储着局部变量表,操作数栈,动态链接,方法出口等信息。 每一个方法的执行都会创建一个栈帧,两者是相对应的,而每一个方法的调用到结束的过程也对应了一个栈帧的入栈和出栈操作。
- 我们常常把Java内存粗略的区分为”堆内存“和”栈内存“,其中的”栈内存“其实就是指Java虚拟机栈,或者准确来说是虚拟机栈中的局部变量表。
- 局部变量表存放了编译可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象应用(reference)和returnAddress类型(指向了一条字节码指令的地址)。局部变量所需的内存空间在编译期间完成分配,所以一个方法需要在帧中分配多大的局部变量空间是确定的,且在运行期间不会改变大小。
- Java虚拟机栈会抛出两种异常,分别是线程请求的栈深度大于虚拟机允许的深度,将抛出的StackOverflowError异常,和虚拟机栈动态扩展时无法申请到足够的内存就会抛出的OutOfMemoryError异常。
虚拟机访问对象的方式有两种——句柄和直接指针,当使用句柄访问时reference存储的就是对象的句柄地址,相反则存储的是对象的直接地址。HotSpot VM用的是后者。
-
本地方法栈
- 本地方法栈的作用跟虚拟机栈的作用非常相似,只不过虚拟机栈对应的是java方法,而本地方法栈对应的是Native方法。由于虚拟机规范没有对本地方法栈中方法使用的语言、使用方式和数据结构进行规范,所以不同的虚拟机有不用的实现方法,而HotSpot VM直接把本地方法栈和虚拟机栈合二为一。
-
Java堆
- Java堆是所有线程共享的一块内存区域,对很多应用来说是虚拟机管理的最大的一块区域,也是垃圾收集器管理的主要区域。
- 根据虚拟机规范Java堆的唯一目的就是存放对象,几乎所有的对象实例都在这里分配内存。该区域的内存分配可以从两个角度来看:从内存回收的角度看,由于现在收集器大多采用分代收集的算法,所以Java堆可以细分为:新生代和老年代。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。( 分配缓冲区是为了解决多线程创建对象分配内存时的线程安全问题,而为每个线程划分的内存空间,不同线程在各自的缓冲空间中分配内存,只有缓冲区用完并分配新的缓冲区时,才同步锁定)
- Java堆可以通过-Xmx和-Xms来固定大小,当堆中内存不足且无法扩展时,就会抛出OutOfMemoryError异常。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上联系即可
-
方法区
- 方法区与java堆一样是线程共享的内存区域,它用于存储被虚拟机加载的类的类信息,常量,静态变量,即时编译器编译后的代码等数据。在HotSpot VM中用”永久代“来实现方法区,但是由于”永久代“有内存大小上限(-XXMaxPermSize)所以在一些动态创建对象的应用中(如jsp)容易出现内存溢出的问题,在JDK1.8后”永久代“被”元空间“取代。
- 运行时常量池是方法区的一部分。我们平常所说的常量池就是指运行时常量池,实际上常量池有两种形态,一种是指.class文件中的常量池——.class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项就是常量池,用于存放字面量和符号引用。 其中的字面量也就是java语言层面上的常量,如字符串,和final修饰的常量值。而符号引用则包括三种:类和接口的全限命名、字段名称和修饰符号、方法名称和修饰符号。 而.class文件的常量池在虚拟机加类加载完后都放在运行时常量池中存放。
- 运行时常量池中的常量不一定在编译期间产生,意思是并非只有class文件中的常量可以置入常量池中,在运行期间也可以将新常量放入池中,用得最多的是String的intern()方法。
也因此在jdk1.7中字符串常量池从”永久代“中移出。