本文总结自《深入理解Java虚拟机》一书。主要是对java虚拟机运行时各数据区域作用以及何时抛出何种异常等内容作出整理。
一、运行时数据区
想想还是先上一张表格,照着表描述思路更清晰:
数据区 | 线程私有 | 作用 | 异常 |
---|---|---|---|
程序计数器 | 是 | 记录当前线程执行到的字节码行号 | 无任何异常 |
虚拟机栈 | 是 | 存放栈帧(方法执行时的基础数据结构,存储局部变量表等信息)以及入栈出栈 | StackoverflowError与OutOfMemoryError |
本地方法栈 | 是 | 与虚拟机栈类似,用于Native方法执行 | StackoverflowError与OutOfMemoryError |
堆 | 否 | 存放对象实例 | OutOfMemoryError |
方法区 | 否 | 存储已被虚拟机加载的类信息、常量、静态变量和JIT编译后的代码等数据 | OutOfMemoryError |
1. 程序计数器
即当前线程所执行字节码的行号指示器。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间实现的,同一时刻一个处理器内核只会执行一条线程中的指令。为了线程切换后能回到正确的执行位置,因此每个线程必须要有一个计数器来记录线程执行的位置。各线程计数器之间互不影响,独立存储。
另外,当线程在执行Java方法时计数器记录的值是字节码指令的地址,而执行Native方法时值则为空。程序计数器是Java虚拟机规范中唯一一个没有规定任何OOM情况的区域。
2. 虚拟机栈
虚拟机栈描述的是Java方执行的内存模型:每个方法在执行同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
我们平时说的java的“堆”、“栈”,其中“栈”就是虚拟机栈,或者是虚拟机栈中局部变量表部分。局部变量表存放编译器可知的基本数据类型、对象引用和returnAddress类型,其中long和double占两个局部变量空间(Slot),其它基本数据类型占1个。局部变量表所需内存在编译时完成分配,当进入一个方法时,需要在栈帧中分配多大的局部变量空间是确定的,不会在运行期间改变其大小。
Java虚拟机规范中对虚拟机栈规范了两种异常:
- 当线程请求的栈深大于虚拟机栈允许的深度时,就会抛出StackOverflowError;这个很好测出来,我们可以循环调用方法本身就可以出现这样的问题。(可以使用-Xss虚拟机参数减小栈大小);
- 如果虚拟机栈可以动态扩展,但是扩展时无法申请到足够内存,抛出OOM异常。这个单线程并不好测试,多线程情况下不停的创建线程跑一个无限循环任务,则可以出现OOM,但是该异常是因为过多线程(虚拟机栈和本地方法栈是线程私有的,每个线程都会有,也是耗内存的)导致没有足够内存去创建线程,而不是上述没内存去扩展虚拟机栈导致。
3. 本地方法栈
这个不重复说了,也是线程私有的,与虚拟机栈类似,只是执行Native方法,也会抛出StackOverflowError以及OOM异常。另外,本地方法栈对方法使用的语言、方式和数据结构没有强制要求。此外,HotSpot虚拟机将本地方法栈与虚拟机栈合二为一。
4. 堆
用于分配存放对象,线程共享的。堆是GC回收的主要区域,由于主流虚拟机都采用分代收集算法,所以堆可以细分为:新生代与老年代。按Java虚拟机规范规定,Java堆可以处于物理上不连续的内存中,只要逻辑上是连续的即可。就像磁盘空间一样。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,会抛出OOM异常。这个可以通过不停的new对象,同时要持有对象引用比如放到list中,防止内存回收,即可出现OOM。
5.方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等数据,也是线程共享的内存区域。HotSpot上被称为“永久代”。该区域内存回收主要是针对常量池以及对类型的卸载。但是回收的效果其实相对堆回收来说很微小。当内存无法满足内存分配需求时,抛出OOM。
5-1.运行时常量池
运行时常量池是方法区的一部分,Class文件中除了类的版本、字段、方法、接口等描述信息,还有就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。Java并不要求常量只有编译期才能产生,运行期间也可以产生,比如用String类的intern()方法。当常量池无法申请到足够内存时会抛出OOM异常。如前一句所述,用intern不停地创建不同的常量就可以测出来。
二、直接内存
这个区域单独拿出来,是因为他并不是虚拟机管理的内存,但也可能OOM。即通过Native方法直接分配堆外内存。如果各内存区域总和大于物理内存限制,动态扩展时会抛出OOM。我们可以通过UnSafe类的allocateMemory()方法来直接申请内存复现这个异常。
最后,引用一张网上找来的图来帮助大家更好的理解虚拟机内存划分: