接触Java不久,就了解到Java内存区域的简单划分,如寄存器,堆,堆栈等。在阅读《深入理解Java虚拟机》后,对于内存的划分又有了新的理解。
运行时数据区域
Java虚拟机在执行Java程序时会把内存划分为几个不同的数据区域,分别为程序计数器,虚拟机栈,本地方法栈,堆,方法区。而除了程序计数器之外的每个区域都会有内存溢出的现象,通过下图可以对其内存区域进行分类:
程序计数器
程序计数器是一块较小的空间,可以看作是当前线程所执行字节码的行号指示器,简单来讲,计数器的值可以用来选取下一条需要执行的指令。Java虚拟机在一个时刻只能执行一个线程中的指令。因此,在多线程下,为了保证线程切换能恢复到正确的位置,每条线程都会有独立的程序计数器。由于各线程计数器互不影响,可以称此类内存区域为“线程私有”的内存。
如果线程正在执行一个Java方法,则计数器存储的是下一条指令的地址。如果执行的是Native方法,则计数器为空。程序计数器是虚拟机中唯一一个不会抛出OOM异常的区域。
Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期也线程相同。Java虚拟机描述的是Java方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧,用来存储局部变量表,操作栈,动态链接,方法出口等信息。一个方法从调用到执行完成,意味着一个栈帧在虚拟机中入栈到出栈。
上述提到了几个知识点:局部变量表,操作栈,动态链接,方法出口。可以具体的了解一下(参考来源:http://blog.csdn.net/ns_code/article/details/17565503):
局部变量表
局部变量表存放了编译器可知的八大数据类型,对象引用和returnAddress类型(指向下一条指令的地址)。局部变量表所占用的内存空间在编译期间完成分配。当进入一个方法时,这个方法需要多大的局部变量空间是完全确定的,方法执行的过程中不会改变局部变量表的大小。
操作栈
操作栈,也叫操作数栈。其最大深度在编译期间就已经确定。当方法开始执行前,栈是空的,随后会有各种指令向该栈提取或存入数据,即入栈出栈操作。
动态链接
每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
方法出口
当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
本地方法栈
与虚拟机栈相似,但不同点在于虚拟机栈执行的是Java方法,而本地方法栈执行的是虚拟机使用到的Native方法。与虚拟机栈相同,都会有可能抛出OOM和SOF异常。
Java堆
Java堆是Java虚拟机中内存最大的一块,它被所有线程共享,在虚拟机启动时自动创建。几乎所有的对象实例都会在此处分配内存。同时Java堆是垃圾收集的主要区域。
Java堆可以在物理上不连续,但只要逻辑上连续就可以。堆中如果没有内存来存储对象实例,就会抛出OOM异常。
方法区
方法区也是线程共享的一块内存区域吗,用于存储被虚拟机加载的类信息,常量,静态变量,即时编译的代码等数据。此区域也与堆类似,不需要连续的内存。也还可以不需要垃圾回收。
其中,需要注意到在方法区中存在一个叫运行时常量池的内存区域。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Class文件常量池),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。在实际的开发过程中,可以使用String的intern()方法来为常量池添加一个新的常量。
如果无法满足内存需求分配,同样会抛出OOM异常。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。直接内存的分配不会受到Java堆大小的限制,但会受到本机总内存或是处理器寻址空间的限制。也有可能出现OOM异常。
对象访问
通过上述的叙述,会有点感觉捉摸不透。一个对象被生成后具体会使用到哪些区域?
以Object obj = new Object()为例,Object obj 这一块则会存储到Java虚拟机栈的变量表中,作为一个引用类型。而new Object()则会存储到Java堆中。另外Object的对象类型,实现的接口,方法等则会存储到方法区中。当我们需要访问这个Object对象时,通常虚拟机会有这两种方法:
- 使用句柄访问方式
- 使用直接指针访问方式
具体的可以通过下图来理解:
1.
2.
其中,句柄访问方式优点在于当对象被移动时,只需改变句柄池中对于的地址。而直接指针访问方式优点在于其访问的速度更快。至于选取那种访问方式各个虚拟机可能会有所不同。