运行时数据区域
java虚拟机(JVM)是按照运行时数据的存储结构来划分内存区域的,有的区域随着java程序的启动而存在,有的区域随着线程的启动或结束而生成或者销毁,这些区域统称为运行时数据(Runtime Data)。运行时数据区包括java程序本身运行的数据信息和JVM运行java程序需要的额外信息,如记录当前线程指针执行位置的程序计数器(又叫PC指针)等。
在java虚拟机规范中,将运行时数据划分为以下:
程序计数器
虚拟机栈
本地方法栈
方法区
堆
程序计数器
程序计数器(Program Counter Register)是一块较小的内存,用于保存当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
我们知道,java多线程是依靠线程轮流切换并分配CPU时间片来实现的,所以当有多个线程交叉执行时,被中断的线程恢复需要继续执行被中断时的指令,而被中断的线程的指令就位于该线程的程序计数器中。
JVM规范只定义了java方法需要记录指针信息,程序计数器记录了当前方法的字节码的行号,而对于Native方法,记录为空。
Java虚拟机栈
与程序计数器一样,java虚拟机栈也是与线程绑定在一起的,每当创建一个线程时,都会有一个虚拟机栈在JVM中创建。虚拟机栈主要是用来描述java方法相关的信息的,线程执行的本质其实就是方法调用,当我们在线程中每调用一个方法时,就会在该线程所对应的栈帧中创建一个栈帧(用于存放局部变量表、操作数栈、动态链接、方法出口等信息),方法中调用方法会继续创建栈帧,每个方法的调用与释放,就对应着栈帧在虚拟机栈的压栈和出栈过程。
栈帧里面的局部变量表存放了编译器可知的各种基本数据类型、对象引用和returnAddress类型(指向一条字节码的地址)。
其中long和double等64位长度的会占用两个局部变量空间单位(Slot),其他类型各占用一个。 局部变量表所需的内存空间在编译期间就完成分配,当进入一个方法时,这个方法需要在虚拟机栈中分配多大的局部变量时完全可以确定的,在方法运行期间不会改变局部变量表的大小。
每当一个方法执行完成时,这个栈帧就会弹出栈帧的元素作为这个方法的返回值,并清除这个栈帧,Java栈的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,线程的程序计数器也会指向这个地址。只有该活动栈的本地变量可以被操作栈使用,当这个栈帧又去调用另外一个方法时,与之对应的另外一个栈帧就会创建,并且会将当前执行的栈帧压下去,新创建的栈帧又位于虚拟机栈的顶部,变为当前的活动栈。同样,只有当前的这个栈的本地变量能够被操作栈所使用。当这个栈帧的指令执行完成时,这个栈帧就会被弹出虚拟机栈,之前被压下去的栈就会变成活动栈继续执行。
由于Java栈是与线程所对应的,所以该部分的数据不是线程所共享的,所以不用关心数据一致性的问题,也不会存在同步锁的问题。
在虚拟机规范中,对这个区域做了两种内存异常的情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackoverFlowError异常,如果虚拟机栈可以动态扩展(当前大部分虚拟机栈都允许动态扩展,不过也可以固定大小),当java虚拟机栈扩展无法申请到足够内存时,将抛出OOM(OutOfMemoryError)异常。
一般单线程下只会抛出StackoverFlowError异常,且最容易发生的地方为多次调用递归方法,栈的内存=物理内存-jvm内存-方法区内存,当拥有多个线程时,每个线程的可分配的最大内存就会减小,所以多线程下虚拟机栈会容易发生OOM异常。
本地方法栈
本地方法栈是JVM为本地方法(Native Mehod)所准备的空间,他与前面的虚拟机栈功能是非常类似的,区别只不过是服务对象不同,虚拟机栈是为Java方法服务的,而本地方法栈是为native方法服务。JVM规范并没有对本地方法栈中方法的语言语式以及数据结构做强制规定,因此虚拟机可自由实现它,甚至有的虚拟机就直接将本地方法栈与虚拟机栈合二为一,比如sun的Hotspot虚拟机。与虚拟机栈一样,本地方法栈也会抛出OOM与StackOverflowError异常。
堆
java堆是虚拟机管理内存的最大一块,也就是虚拟机管理java对象的核心区域。堆得大小在第一次启动JVM的时候就一次性的向操作系统申请完成,通过-Xmx和-Xms来控制堆得大小,-Xmx表示堆的最大大小,-Xms表示初始启动时堆的大小,一旦分配完成,堆的大小就将固定。堆中内存由JVM来管理,对象的创建由java程序来控制,对象所占用的内存空间由管理堆的垃圾回收器来回收。根据GC算法的不同,内存回收的方式和时机也不同。。
java堆是被线程所共享的一块内存区域,几乎所有的对象实例都在这里分配,但是不是绝对的。
从内存分配的角度看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),不过无论如何划分,都与存放的内容无关,无论哪个区域存放的都是对象。
java堆和程序数据密切相关,java栈和线程密切相关,线程最基本的操作就是方法调用,每次方法调用都是java栈传递数据,所以栈主要是用来对堆中的数据进行运行处理的。所以栈应该主要是程序运行区,而堆主要是存储区。
java虚拟机规范堆可以是物理上不连续的内存空间,只要逻辑上连续即可,就像磁盘一样。大小即可以固定,也可以可扩展,当前主流都是可扩展的。如果在堆中GC回收完后仍然没有足够的内存完成实例分配,并且堆也无法再扩展时,将会抛出OOM异常。此处的异常由两种可能,一种是内存泄漏,一种是内存溢出,不过最终结果都是内存不够用,后面有时间再补充。
方法区
方法区与堆一样,是各个线程所共享的一块内存区域,他用于存储已经被类加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。java虚拟机规范把方法区描述为堆的一个部分,但他有一个别名叫做(Non-Heap),目的应该是与堆区分开来。
方法区存储区域的大小在启动一段程序性后随着需要加载的类都加载到JVM中就固定下来了。但是有可能随着类的动态编译,方法区存储区域就会增加。
方法区不等于永久带,说他相等仅仅是因为GC分代算法也扩展到了方法区,这样hotspot可以使用像管理堆一样的方法来管理方法区内存,对于其他虚拟机则不存在永久带,因为虚拟机的规范并没有要求方法区的实现一定要加GC算法。
方法区的内存回收目标主要是针对常量池与对类型的卸载,一般来说,这个区域的内存回收效果不 怎么好,尤其类型的卸载,条件相当苛刻,但这部分区域的内存回收也是必要的。当方法区内存无法满足内存分配时,也将抛出OOM异常。
运行时常量池
运行时常量池是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
一般来说,除了符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于class文件常量池来说一个重要特征便是具备动态性,java语言并不要求一定只有编译期才能产生,运行期间也可能将新的常亮放入池中,这种特性用的最多的是String的intern()方法。
运行时常量池也是方法区的一部分,当然也受到方法区内存的限制,当没有足够内存时会抛出OOM异常。
直接内存
直接内存不是虚拟机运行时数据区域的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁使用,也有可能会出现OOM异常,所以也在此讲一下。
JDK1.4中引入了NIO(New Input/Output)类,他是一种基于通道与缓冲区的IO方式,它可以使用本地函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer类的对象作为这块内存的引用进行操作,这样就避免了Java堆与本地堆中来回复制数据,在某些场景中能够显著提升性能。
显然,本机直接内存的分配不会受到java堆的限制,但是他会受到本机总内存大小以及处理器寻址空间的限制。当我们配置JVM时经常忽略本机内存的声誉使得各区域综合大于物理内存,从而导致动态扩展时出现OOM异常。
字面量:文本字符串、生命为final的常量值等
符号引用:类和接口的完全限定名、字段的名称和描述符以及方法的名称和描述符。