java内存划分

时间:2024-09-06 08:08:01

运行时数据区域

Java虚拟机在执行Java的过程中会把管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,而有的区域则依赖线程的启动和结束而创建和销毁。

Java虚拟机包括下面几个运行时数据区域:

java内存划分

程序计数器

程序计数器是一块较小的区域,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的模型里,字节码指示器就是通过改变程序计数器的值来指定下一条需要执行的指令。分支,循环等基础功能就是依赖程序计数器来完成的。

由于java虚拟机的多线程是通过轮流切换并分配处理器执行时间来完成,一个处理器同一时间只会执行一条线程中的指令。为了线程恢复后能够恢复正确的执行位置,每条线程都需要一个独立的程序计数器,以确保线程之间互不影响。所以程序计数器是“线程私有”的内存。

如果虚拟机正在执行的是一个Java方法,则计数器指定的是字节码指令对应的地址,如果正在执行的是一个本地方法,则计数器指定问空undefined。程序计数器区域是Java虚拟机中唯一没有定义OutOfMemory异常的区域。

Java虚拟机栈

和程序计数器一样也是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

通常所说的虚拟机运行时分为栈和堆,这里的栈指的就是虚拟机栈或者说虚拟机栈中的局部变量表部分。

局部变量表存放了编译器可知的各种基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。局部变量表所需的内存空间在编译器完成分配,当进入一个方法时这个方法需要在帧中分配多大的内存空间是完全确定的,运行期间不会改变局部变量表的大小。(64为长度的long和double会占用两个局部变量空间,其他的数据类型占用一个)

Java虚拟机栈可能出现两种类型的异常:1. 线程请求的栈深度大于虚拟机允许的栈深度,将抛出*Error。2.虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

堆栈溢出错误一般是递归调用嘛。下面的代码就可以出现:

java内存划分
package T20131009;
public class *Test {
public static void main(String[] args) {
method();
}
public static void method(){
for(;;)
method();
}
}
java内存划分

运行结果:

  java内存划分

 2.OutOfMemoryError

  内存溢出一般是出现在申请了较多的内存空间没有释放的情形。下面的代码就可以出现:

java内存划分
package T20131009;
import java.util.ArrayList;
import java.util.List;
public class OutOfMemoryTest {
public static void main(String[] args){
List list=new ArrayList();
for(;;){
int[] tmp=new int[1000000];
list.add(tmp);
}
}
}
java内存划分

运行结果:

java内存划分

本地方法栈

本地方法栈和虚拟机栈基本类似,只不过Java虚拟机栈执行的是Java代码(字节码),本地方法栈中执行的是本地方法的服务。本地方法栈中也会抛出*Error和OutOfMemory异常。

堆是Java虚拟机所管理的内存中最大的一块。堆是所有线程共享的一块区域,在虚拟机启动时创建。堆的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配,不过随着JIT编译器的发展和逃逸技术的成熟,栈上分配和标量替换技术使得这种情况发生着微妙的变化,对上分配正变得不那么绝对。

附:在Java编程语言和环境中,即时编译器(JIT compiler,just-in-time compiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。当你写好一个Java程序后,源语言的语句将由Java编译器编译成字节码,而不是编译成与某个特定的处理器硬件平台对应的指令代码(比如,Intel的Pentium微处理器或IBM的System/390处理器)。字节码是可以发送给任何平台并且能在那个平台上运行的独立于平台的代码。

Java堆是垃圾收集器管理的主要区域,所以也称为“GC堆”。由于现在的垃圾收集器基本上都是采用分代收集算法,所以Java堆还可细分为:新生代和老生代。在细致一点可分为Eden空间,From Survivor空间,To Survivor空间。如果从内存分配的角度看,线程共享的Java堆可划分出多个线程私有的分配缓冲区。不过无论如何划分,都与存放内容无关,无论哪个区域,都是用来存放对象实例。细分的目的是为了更好的回收内存或者更快的分配内存。

Java堆可以是物理上不连续的空间,只要逻辑上连续即可,主流的虚拟机都是按照可扩展的方式来实现的。如果当前对中没有内存完成对象实例的创建,并且不能在进行内存扩展,则会抛出OutOfMemory异常。

方法区

方法区也是线程共享的区域,用于存储已经被虚拟机加载的类信息,常量,静态变量和即时编译器(JIT)编译后的代码等数据。Java虚拟机把方法区描述为堆的一个逻辑分区,不过方法区有一个别名Non-Heap(非堆),用于区别于Java堆区。

Java虚拟机规范对这个区域的限制也非常宽松,除了可以是物理不连续的空间外,也允许固定大小和扩展性,还可以不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的(所以常量和静态变量的定义要多注意)。方法区的内存收集还是会出现,不过这个区域的内存收集主要是针对常量池的回收和对类型的卸载。

一般来说方法区的内存回收比较难以令人满意。当方法区无法满足内存分配需求时将抛出OutOfMemoryError异常。

运行时常量池

运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等信息以外,还有一项信息是常量池用于存储编译器生成的各种字面量和符号引用,这部分信息将在类加载后存放到方法区的运行时常量池中。Java虚拟机对类的每一部分(包括常量池)都有严格的规定,每个字节用于存储哪种数据都必须有规范上的要求,这样才能够被虚拟机认可,装载和执行。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java虚拟机并不要求常量只能在编译期产生,也就是并非预置入Class文件常量池的内容才能进入方法区的运行时常量池中,运行期间也可将新的常量放入常量池中。

常量池是方法区的一部分,所以受到内存的限制,当无法申请到足够内存时会抛出OutOfMemoryError异常。

对象访问

对象访问在Java语言中无处不在,即使是最简单的访问,也会涉及到Java栈,java堆,方法区这三个最重要的内存区域之间的关联关系。如下面的代码:

 Object obj = new Object();

       假设这段代码出现在方法体中,那么“Object obj”部分的语义将会反映到Java栈的本地变量表中,作为一个reference类型的数据存在。而“new Object();”部分的语义将会反应到Java堆中,形成一块存储Object类型所有实例数据值(Instance Data)的结构化内存,根据具体类型以及虚拟机实现的对象分布的不同,这块内存的长度是不固定的。另外,在JAVA堆中还必须包含能查找到此对象内存数据的地址信息,这些类型数据则存储在方法区中。

       由于reference类型在Java虚拟机中之规定了指向对象的引用,并没有规定这个引用要通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此虚拟机实现的对象访问方式会有所不同。主流的访问方式有两种:句柄访问方式和直接指针。

1. 如果使用句柄访问方式,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

java内存划分

2. 如果通过直接指针方式访问,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象的地址。

java内存划分

两种方式各有优势,局并访问方式最大的好处是reference中存放的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,而reference本身不需要被修改。而指针访问的最大优势是速度快,它节省了一次指针定位的开销,由于对象访问在Java中非常频繁,一次这类开销积少成多后也是一项非常可观的成本。

具体的访问方式都是有虚拟机指定的,虚拟机Sun HotSpot使用的是直接指针方式,不过从整个软件开发的范围来看,各种语言和框架使用句柄访问方式的情况十分常见。