Java学习笔记——JVM之内存区域划分

前言

上一篇博客说的是重载的实质——动态绑定以及它在JVM中的实现细节,然而里面却有很多名词和概念没有详细说明,如“直接引用”、“方法区”、对象在JVM中如何存储、类如何加载进JVM等。接下来我会针对《深入理解Java虚拟机》这本书,写很多博客来学习这些“基础”的问题。这篇文章的语言大部分都是出自于书中并结合自己的理解写出来的。

*********************************以下是正经内容 *******************************************

对于C和C++来说,内存管理是程序员要干的事情,而对Java程序员来说,内存管理要轻松一些,因为有JVM帮助程序员管理内存。但是,正因为Java程序员把内存控制的程序交给了JVM,才要了解JVM是怎样使用内存的,从而在出现内存泄漏和溢出问题时排查错误。

内存分区概述

稍微了解JVM的人都会知道,JVM管理内存会分区。让我门引用《深入理解Java虚拟机》中的一段话来明确这一概念。

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

都有那些区域呢,具体如下图(网上找的):
Java学习笔记——JVM之内存区域划分
我们关注的部分是上面的运行时数据区部分,其中灰色的是由所有线程共享的数据区,白色的是线程隔离的数据区,即每个线程都有这样一个数据区,且各条线程之间的数据区互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。下面我们来详细解释每个数据区。

一、程序计数器

程序计数器(Program Counter Register)是一块比较小的内存空间,它是一个线程私有的内存,每个线程都需要有一个程序计数器且各个线程计数器之间互不影响,独立存储。它可以看作是当前线程所执行的字节码的行号指示器,在概念模型中(可能会有更高效的方法),JVM在执行字节码时,用它的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个计数器来完成。

关于异常:
此区域是唯一一个在Java虚拟规范中没有规定任何OutOfMemoryError情况的区域。

二、虚拟机栈

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈是在Java执行方法时用到的,Java每个方法执行的同时都会创建一个栈帧(Stack Frame)来存储方法执行的一些必要数据,这些数据包括局部变量表、操作数栈、动态链接、方法出口等。每一个方法调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

在栈帧存储的数据这些数据中,我们需要了解的是局部变量表这一部分。局部变量表中存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是指向对象起始地址的引用指针,异常也可能是指向一个代表对象的句柄或其他与此对象相关的位置) 和returnAddress类型(指向了一条字节码指令的地址)。

在局部变量表中,64位长度的long和double类型会占用2个局部变量表空间(Slot),其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量表是完全确定的,在方法运行期间不会改变局部变量表的大小。

关于异常:
在Java虚拟机规范中,对这个区域规定了两种异常状况:
1.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常;
2.在虚拟机栈可以动态扩展的情况下,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

三、本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用非常相似,但服务的对象却不同,虚拟机栈是为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。什么是Native方法?简单地讲,一个Native方法就是一个由非Java语言实现的方法。这样一来就很清晰了,也就是说,Java方法的信息存储用虚拟机栈,非Java方法的信息存储用本地方法栈。而且大家注意到了吗,前面说的程序计数器,它可以看成JVM在线程执行字节码时的行号指示器,如果线程正在执行的是一个Java方法(字节码),这个计数器记录的是一条字节码的指令地址,如果正在执行的是Native方法,计数器还会有值吗?答案是没有。当线程正在执行一个Native方法(非Java)时,程序计数器的值为空(Undefined)。

关于异常:
与虚拟机栈一样,本地方法栈也会抛出*Error和OutOfMemoryError异常。

四、Java堆

Java堆是被所有线程共享的一块区域,在虚拟机启动时创建。对大多数应用来说,Java堆(Java Heap)是JVM所管理的内存中最大的一块。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”(Garbage Collected Heap),关于垃圾回收是一个大的部分,这里就不细说了。Java堆就像我们的磁盘空间一样,物理上可以处于不连续的内存空间中,只要逻辑上连续即可。

关于异常:
Java堆在实现时,既可以实现成固定大小的,也可以是可扩展的(主流都是可扩展的)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

五、方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java方法区把方法区描述成堆的一个逻辑部分,但它却有一个别名叫Non-Heap(非堆),目的应该是与Java堆区分开来。

Java虚拟机规范对方法区的限制非常宽松,除了 和Java堆一样不需要连续的内存 和 可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。此区域内存回收目标主要是针对常量池的回收和对类型的卸载(虽然很麻烦,但是很必要)。

关于异常:
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

六、运行时常量池

刚才提到了,方法区内存回收目标之一就是对常量池进行回收。那么,常量池是什么呢?

运行时常量池(Runtime Constant Pool)是方法区的一部分。它主要在类加载进入方法区后存放编译期生成的各种字面量和符号引用。常量池也是Class文件中描述的信息中的一项。

有人可能会对字面量和符号引用这两个概念有疑惑,这里解释一下:

1.字面量:用于表达源代码中固定值的表示法。比如:String s = ”hello“; 中,hello就是字面量
2.符号引用:Java代码在进行编译的时候,并不像C、C++那样有”连接“的步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,Class文件中不回保存各个方法、字段真正的内存地址,只能用符号引用代替。JVM运行时,先从常量池获取符号引用,再在类创建时或运行时解析、翻译到具体的内存地址当中。

关于异常:
既然运行时常量池是方法区的一部分,自然受到方法去内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

七、直接内存

直接内存(Direct Memory)并不属于虚拟机管理内存的一部分,但在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的IO方式。它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能(避免了在Java堆和Native堆来回复制数据)。

关于异常:
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,总要受到本机总内存大小的限制,如果只忽略直接内存而只看虚拟机内存的大小,可能使各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。