Java虚拟机(一)——Java内存区域与内存溢出异常

运行时数据区域

根据《虚拟机规范SE 7版》规定,Java虚拟机管理的内存将会包括以下几个运行时数据区域。

程序计数器(programming counter register)

可以看作当前线程所执行字节码的行号指示器。
JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多个处理器来说是一个内核)只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的位置,每条线程都需要有一个独立的程序计数器,各条线程程序计数器之间互不影响,独立存储。我们称这类内存区域为线程私有的内存。

虚拟机栈(VM stack)

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。栈中存放的是基本类型变量的值和引用类型变量的地址。虚拟机栈是Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至完成的过程,对应者一个栈帧在虚拟机中的入栈和出栈过程。
通常所说的栈内存(stack)和堆内存(heap)与对象内存分配最密切,实际上Java内存区域的划分远比这个复杂。其中的栈内存就是此处所说的虚拟机栈。
这个区域规定了两种异常状态:如果线程请求的栈深度大于虚拟机允许的深度,将会抛出StackOverflowError异常;如果虚拟机可以动态扩展,而扩展时无法申请到足够的内存,则抛出OutOfMemoryError异常。

本地方法栈(native method stack)

本地方法栈与虚拟机栈所发挥的作用非常相似,它们的区别在于虚拟机栈为虚拟机执行Java方法(即字节码)服务,而本地方法栈为虚拟机使用的Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError异常和OutOfMemoryError异常。

Java堆(Java heap)

Java堆是虚拟机管理内存中最大的一块,是被所有线程共享的一块内存区域。
Java堆用于存放几乎所有的对象实例和数组。但随着JIT编译器的发展和逃逸技术的逐渐成熟,栈上分配、标量替换优化技术将导致所有的对象都在堆上分配不那么绝对了。
Java堆是垃圾回收器管理的主要区域,因此也常被称为GC堆(garbage collected heap)。从内存回收的角度看,回收器基本采用分代收集算法,Java堆可分为新生带和老年代;从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(thread local allocation buffer,TLAB)。
Java堆可以处于物理上不连续的区域,但只要逻辑上连续即可。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时(当前主流虚拟机都是按照可扩展来实现的),将会抛出OutOfMemoryError异常。

方法区(method error)

与Java堆一样,是各个线程共享的内存区域,它用于分配已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它也叫非堆(non-heap),目的是与堆区分开来。
Java规范对方法区的限制非常放松,除了和Java堆一样不需要连续物理内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾回收。但并非数据进入方法区后就永久存在,这个区域的内存回收的主要目标是常量池的回收和对类的卸载
当方法区无法满足内存需求时,将抛出OutOfMemoryError异常。
常量池(constant pool table):用于存放编译阶段生成的各种字面量和符号引用,这部分内容将会在类加载后进入方法区的运行时常量池(runtime constant pool)存放。运行时常量池是方法区的一部分
运行时常量池对于class文件常量池的另一个重要特征是具备动态性,并不要求常量一定只在编译阶段产生,运行阶段也可能将新的常量放入池中。典型的是String类的intern()方法。
当常量无法再申请到方法区的内存时,将抛出OutOfMemoryError异常。
字面量:文本字符串;八种基本类型的值;被声明为final的常量等。
符号引用:类和方法的全限定名;字段(成员变量、属性)的名称和描述符;方法的名称和描述符。
注意:在jdk1.6之前,字符串常量池存放在方法区中 ,但到了jdk1.7之后,常量池被移出到Java堆中了。
栈与堆的区别:
(1)栈中数据大小和生命周期确定;堆中不确定。
(2)说到大小,栈中存放的局部变量(8种基本数据类型和对象引用)实际值基本都是一串二进制数据,所以数据很小。而堆中存放的对象类型数据更大。
(3)栈中的数据在其所属方法或代码块执行结束后,就被释放;而堆中的数据由垃圾回收机制进行管理,无法确定合适会被回收释放。

直接内存(direct memory)

直接内存并不是Java运行时数据区域的一部分,也不是虚拟机规范中定义的内存区域。但这部分被频繁使用,也可能导致OutOfMemoryError异常。
在JDK1.4中新加入了NIO(new input/output)类,引入了一种基于通道(channel)和缓冲(buffer)的I/O方式,可以使用Native函数直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象为这块内存的引用进行操作。避免了Java堆和Native堆(本机的内存)来回复制数据。
本机直接地址的分配不会受到Java堆的限制,但会受到本机内存(包括RAM、SWAP区域或分页文件)以及处理器寻址空间的限制。若直接内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemory异常。