本文内容
运行时数据区域

以上是 Java虚拟机规范 中定义的区域,实际的 JVM 实现可能会有不同的称呼和实现
程序计数器
它可以看作是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里面,解释器就是通过改变这个行号来选取下一条要执行的字节码指令。
这个程序计数器,每个线程都有一个,用来表示当前线程执行到了哪里。当发生线程切换后,依靠这个程序计数器来将线程恢复到正确的执行位置。
每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
也就是说,这个程序计数器所在的很小的这块儿内存,是属于一个线程私有的内存,线程消亡后就会将其回收
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。
好像也没法 OOM,毕竟它就是一个数字而已,线程创建后就有它,也不需要为它再动态申请更多内存了
Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期也是与线程相同。
虚拟机栈类似于 C语言 里的执行栈:
- 线程里每个方法被执行的时候,Java 虚拟机都会为它创建一个栈帧,栈帧用来存储局部变量表、操作数栈、动态连接、方法出口等信息。
- 每个方法开始执行时,这个栈帧就进入到了线程的这个虚拟机栈
- 每个方法执行完毕后,这个栈帧就从线程的虚拟机栈中移除了
也就是说,线程有一个栈结构,叫做虚拟机栈。栈中每一项都是一个栈帧对象的引用,因此栈中的每一项大小是固定的,也就是一个指针8个字节或者压缩后是4个字节。这个栈帧对象,里面保存有局部变量表、操作数栈、动态连接、方法出口等信息
局部变量表
局部变量表处于一个栈帧中,存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
- 首先,可以理解它是一个数组,在编译的时候,数组的长度就已经确定了。
- 数组中的每一项就是一个槽位 slot,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个
- 每个槽位的大小,4个字节还是8个字节,这个是由虚拟机自己来定义的,规范中没有规定
当我们定义了一个方法,方法里可能会有一些局部变量,例如定义了一个
int a = 3或者是String str = getStr()那么a就进入了局部变量表,可能被存储在了第 100 个槽位,当解释器需要读取的时候,就是使用 100 这个索引去局部变量表中读取这个a。而str这个指针也进入了局部变量表,可能是第 200 个槽位,存储的内容就是这个字符串在内存中的地址,那么解释器使用索引 200 去局部变量表读取到str的值,例如说是0xFF0F,接着就会去0xFF0F这个内存地址,读取一个字符串出来
在 Java虚拟机规范中,Java虚拟机栈会发生两类异常
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。
- 栈的深度没有直接定义的方法,但是栈的总内存大小可以定义,由于栈内存等于栈帧内存之和,所以栈帧内存不变的情况下,增加栈内存,就可以增加栈深度。栈帧内存变大的情况下,栈的深度就会减少
如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常
HotSpot虚拟机的栈容量是不可以动态扩展的,以前的Classic虚拟机倒是可以。所以在HotSpot虚拟机上是不会由于虚拟机栈无法扩展而导致OutOfMemoryError异常——只要线程申请栈空间成功了就不会有OOM,但是如果申请时就失败,仍然是会出现OOM异常的
也就是说只有在刚开始申请时才会出现 OOM
本地方法栈
本地方法栈与虚拟机栈很相似,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
HotSpot 虚拟机将虚拟机栈和本地方法栈合二为一了,因此,虽然设置本地方法栈大小的参数 (-Xoss)存在,但是无效,栈容量只能由-Xss参数设定。这个设置的是虚拟机栈和本地方法栈的总大小
Java 堆
对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。(标量替换等技术可能导致直接在栈上分配内存给对象)
Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”
Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据(一些元数据)
类信息:类的元数据(metadata),也就是通过反射能拿到的信息,比如说访问权限、实现的接口、类要继承的类......我们之所以可以通过反射获取对象,就是因为类的元数据被加载到了方法区中。 方法区不是线程独享的,而是线程共享的(堆也是),Java线程安全问题由此而生。
和永久代的区别
JDK 8 以前,HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区,于是经常用永久代来称呼方法区。这种实现中,由于采用了永久代的设计,永久代有最大永久代大小的限制,于是JDK 8 之前会有永久代的OOM异常。
JDK 8 以后,使用元空间来重新实现了方法区,有如下改变:
- 字符串常量池、静态变量被挪到了堆中。
- 使用本地内存实现了方法区,因此方法区不会再有永久代的OOM异常,方法区可以使用的内存就是剩余的物理内存(因为元空间默认没有限制大小,不主动限制的话就可以使用所有剩余内存)
这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池
运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。(比如类名、方法名、字段名、参数名等)
常量池

class文件常量池,是class文件的一部分,用于保存编译时确定的数据。可以看出来,上面的数据确实是编译时就能确定的。符号引用包括有本身的类名,以及引用的类名等(比如 new 了一个别的类,那么这个类的类名就会作为一个符号引用进入常量池)
Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,比如字符串的 intern 方法,这些常量被放在运行时常量池中。
类加载后,常量池中的数据会在运行时常量池中存放!
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。
JDK 1.4 引入了 NIO,它可以使用直接缓冲区,这块缓冲区的内存不在 Java 堆中,而是直接使用的本地内存,这样提高了 IO 效率。由于不在堆中,因此不会受到堆的大小限制的影响,但是会受限于物理内存的大小。由于一般都会配置堆内存,但是忘记了配置直接内存,从而导致OOM。(直接内存的大小默认等于堆的最大内存限制,也许我们配置了堆内存是 4g,物理内存总共 6g,此时直接内存的最大值其实也等于堆内存 4g,于是当直接内存想扩大到 4g 时,就会发生 OOM,因为实际内存没有这么多)