Java虚拟机在执行Java程序的过程当中会把他所管理的内存划分为若干个不一样的数据区域。Java虚拟机规范将JVM所管理的内存分为如下几个运行时数据区:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。下面详细阐述各数据区所存储的数据类型。java
程序计数器(Program Counter Register)编程
一块较小的内存空间,它是当前线程所执行的字节码的行号指示器,字节码解释器工做时经过改变该计数器的值来选择下一条须要执行的字节码指令,分支、跳转、循环等基础功能都要依赖它来实现。每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响,所以该区域是线程私有的。数组
当线程在执行一个Java方法时,该计数器记录的是正在执行的虚拟机字节码指令的地址,当线程在执行的是Native方法(调用本地操做系统方法)时,该计数器的值为空。另外,该内存区域是惟一一个在Java虚拟机规范中么有规定任何OOM(内存溢出:OutOfMemoryError)状况的区域。网络
Java虚拟机栈(Java Virtual Machine Stacks)数据结构
该区域也是线程私有的,它的生命周期也与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每一个方法被执行的时候都会同时建立一个栈帧,栈它是用于支持续虚拟机进行方法调用和方法执行的数据结构。对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的全部字节码指令都只针对当前栈帧进行操做。栈帧用于存储局部变量表、操做数栈、动态连接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中须要多大的局部变量表、多深的操做数栈都已经彻底肯定了,而且写入了方法表的Code属性之中。所以,一个栈帧须要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。多线程
在Java虚拟机规范中,对这个区域规定了两种异常状况:性能
一、若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError异常。this
二、若是虚拟机在动态扩展栈时没法申请到足够的内存空间,则抛出OutOfMemoryError异常。spa
这两种状况存在着一些互相重叠的地方:当栈空间没法继续分配时,究竟是内存过小,仍是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。在单线程的操做中,不管是因为栈帧太大,仍是虚拟机栈空间过小,当栈空间没法分配时,虚拟机抛出的都是StackOverflowError异常,而不会获得OutOfMemoryError异常。而在多线程环境下,则会抛出OutOfMemoryError异常。操作系统
下面详细说明栈帧中所存放的各部分信息的做用和数据结构。
一、局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,其中存放的数据的类型是编译期可知的各类基本数据类型、对象引用(reference)和returnAddress类型(它指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,即在Java程序被编译成Class文件时,就肯定了所需分配的最大局部变量表的容量。当进入一个方法时,这个方法须要在栈中分配多大的局部变量空间是彻底肯定的,在方法运行期间不会改变局部变量表的大小。
局部变量表的容量以变量槽(Slot)为最小单位。在虚拟机规范中并无明确指明一个Slot应占用的内存空间大小(容许其随着处理器、操做系统或虚拟机的不一样而发生变化),一个Slot能够存放一个32位之内的数据类型:boolean、byte、char、short、int、float、reference和returnAddresss。reference是对象的引用类型,returnAddress是为字节指令服务的,它执行了一条字节码指令的地址。对于64位的数据类型(long和double),虚拟机会以高位在前的方式为其分配两个连续的Slot空间。
虚拟机经过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的Slot数量,对于32位数据类型的变量,索引n表明第n个Slot,对于64位的,索引n表明第n和第n+1两个Slot。
在方法执行时,虚拟机是使用局部变量表来完成参数值到参数变量列表的传递过程的,若是是实例方法(非static),则局部变量表中的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中能够经过关键字“this”来访问这个隐含的参数。其他参数则按照参数表的顺序来排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和做用域分配其他的Slot。
局部变量表中的Slot是可重用的,方法体中定义的变量,做用域并不必定会覆盖整个方法体,若是当前字节码PC计数器的值已经超过了某个变量的做用域,那么这个变量对应的Slot就能够交给其余变量使用。这样的设计不只仅是为了节省空间,在某些状况下Slot的复用会直接影响到系统的而垃圾收集行为。
二、操做数栈
操做数栈又常被称为操做栈,操做数栈的最大深度也是在编译的时候就肯定了。32位数据类型所占的栈容量为1,64为数据类型所占的栈容量为2。当一个方法开始执行时,它的操做栈是空的,在方法的执行过程当中,会有各类字节码指令(好比:加操做、赋值元算等)向操做栈中写入和提取内容,也就是入栈和出栈操做。
Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操做数栈。所以咱们也称Java虚拟机是基于栈的,这点不一样于Android虚拟机,Android虚拟机是基于寄存器的。
基于栈的指令集最主要的优势是可移植性强,主要的缺点是执行速度相对会慢些;而因为寄存器由硬件直接提供,因此基于寄存器指令集最主要的优势是执行速度快,主要的缺点是可移植性差。
三、动态链接
每一个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态链接。
四、方法返回地址
当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,而且该异常没有在方法体内获得处理。不管采用何种退出方式,在方法退出以后,都须要返回到方法被调用的位置,程序才能继续执行。方法返回时可能须要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。通常来讲,方法正常退出时,调用者的PC计数器的值就能够做为返回地址,栈帧中极可能保存了这个计数器值,而方法异常退出时,返回地址是要经过异常处理器来肯定的,栈帧中通常不会保存这部分信息。
方法退出的过程实际上等同于把当前栈帧出站,所以退出时可能执行的操做有:恢复上层方法的局部变量表和操做数栈,若是有返回值,则把它压入调用者栈帧的操做数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
本地方法栈(Native Method Stacks)
该区域与虚拟机栈所发挥的做用很是类似,只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操做系统(Native)方法服务。
Java堆(Java Heap)
Java Heap是Java虚拟机所管理的内存中最大的一块,它是全部线程共享的一块内存区域。几乎全部的对象实例和数组都在这类分配内存。Java Heap是垃圾收集器管理的主要区域,所以不少时候也被称为“GC堆”。
根据Java虚拟机规范的规定,Java堆能够处在物理上不连续的内存空间中,只要逻辑上是连续的便可。若是在堆中没有内存可分配时,而且堆也没法扩展时,将会抛出OutOfMemoryError异常。
方法区(Method Area)
方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区域又被称为“永久代”,但这仅仅对于Sun HotSpot来说,JRockit和IBM J9虚拟机中并不存在永久代的概念。Java虚拟机规范把方法区描述为Java堆的一个逻辑部分,并且它和Java Heap同样不须要连续的内存,能够选择固定大小或可扩展,另外,虚拟机规范容许该区域能够选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域比较少出现。该区域的内存回收目标主要针是对废弃常量的和无用类的回收。运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Class文件常量池),用于存放编译器生成的各类字面量和符号引用,这部份内容将在类加载后存放到方法区的运行时常量池中。运行时常量池相对于Class文件常量池的另外一个重要特征是具有动态性,Java语言并不要求常量必定只能在编译期产生,也就是并不是预置入Class文件中的常量池的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的是String类的intern()方法。
根据Java虚拟机规范的规定,当方法区没法知足内存分配需求时,将抛出OutOfMemoryError异常。
直接内存(Direct Memory)
直接内存并非虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它直接从操做系统中分配,所以不受Java堆大小的限制,可是会受到本机总内存的大小及处理器寻址空间的限制,所以它也可能致使OutOfMemoryError异常出现。在JDK1.4中新引入了NIO机制,它是一种基于通道与缓冲区的新I/O方式,能够直接从操做系统中分配直接内存,即在堆外分配内存,这样能在一些场景中提升性能,由于避免了在Java堆和Native堆中来回复制数据。关于NIO的详细使用能够参考个人Java网络编程系列中关于NIO的相关文章。
这里有一点要重点说明,在多线程状况下,给每一个线程的栈分配的内存越大,反而越容易产生内存溢出异常。操做系统为每一个进程分配的内存是有限制的,虚拟机提供了参数来控制Java堆和方法区这两部份内存的最大值,忽略掉程序计数器消耗的内存(很小),以及进程自己消耗的内存,剩下的内存便给了虚拟机栈和本地方法栈,每一个线程分配到的栈容量越大,能够创建的线程数量天然就越少。所以,若是是创建过多的线程致使的内存溢出,在不能减小线程数的状况下,就只能经过减小最大堆和每一个线程的栈容量来换取更多的线程。
另外,因为Java堆内也可能发生内存泄露(Memory Leak),这里简要说明一下内存泄露和内存溢出的区别:
内存泄露是指分配出去的内存没有被回收回来,因为失去了对该内存区域的控制,于是形成了资源的浪费。Java中通常不会产生内存泄露,由于有垃圾回收器自动回收垃圾,但这也不绝对,当咱们new了对象,并保存了其引用,可是后面一直没用它,而垃圾回收器又不会去回收它,这边会形成内存泄露,
内存溢出是指程序所须要的内存超出了系统所能分配的内存(包括动态扩展)的上限。
对内存分配状况分析最多见的示例即是对象实例化:
Object obj = new Object();
这段代码的执行会涉及java栈、Java堆、方法区三个最重要的内存区域。假设该语句出如今方法体中,及时对JVM虚拟机不了解的Java使用这,应该也知道obj会做为引用类型(reference)的数据保存在Java栈的本地变量表中,而会在Java堆中保存该引用的实例化对象,但可能并不知道,Java堆中还必须包含能查找到此对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等),这些类型数据则保存在方法区中。
另外,因为reference类型在Java虚拟机规范里面只规定了一个指向对象的引用,并无定义这个引用应该经过哪一种方式去定位,以及访问到Java堆中的对象的具体位置,所以不一样虚拟机实现的对象访问方式会有所不一样,主流的访问方式有两种:使用句柄池和直接使用指针。
这两种对象的访问方式各有优点,使用句柄访问方式的最大好处就是reference中存放的是稳定的句柄地址,在对象呗移动(垃圾收集时移动对象是很是广泛的行为)时只会改变句柄中的实例数据指针,而reference自己不须要修改。使用直接指针访问方式的最大好处是速度快,它节省了一次指针定位的时间开销。目前Java默认使用的HotSpot虚拟机采用的即是是第二种方式进行对象访问的。