JVM之内存区域划分
JVM在执行Java过程中会把它所管理的内存划分为一下几个运行时数据区域:
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- Java堆
- 方法区
程序计数器
程序计数器是一块内存较小的空间,可以看作是当前线程执行的字节码的行号指示器,是每个线程私有的内存区域。
- 当前线程执行的是java方法时,计数器记录的是正在执行的虚拟机字节码指令的地址
- 当前线程执行的是Native方法时,计数器为空
- 程序计数器计数器是JVM规范中唯一没有规定任何OOM的区域
Java虚拟机栈
Java虚拟机栈也是线程私有的,生命周期与线程相同。
Java虚拟机栈描述的是Java方法执行的内存模型:每一个方法创建时会创建一个栈帧,每一个方法从开始调用到执行完成过程,对应着一个栈帧在虚拟机栈中的入栈和出栈的过程。
上面提到的栈帧是方法运行时的基础数据结构,用于存储
- 局部变量表
- 操作数栈
- 动态链接
- 方法出口
等信息。(图片来源于网络)
栈帧.jpg通常人们将Java内存分为堆内存和栈内存,这里所指的栈内存就是指的虚拟机栈或者说是虚拟机栈中局部变量表。
局部变量表存放着各种编译期可知数据:
- 各种基本数据类型(boolean、byte、char、short、int、float、long、double)
- 对象引用(reference类型,并不等同于对象,可能是指向对象起始地址的引用指针或者指向代表一个对象的句柄)
- returnAddress(一条字节码指令地址)
局部变量表存储数据的单位为slot(局部变量空间),其中64位长度的long和double占用2个slot,其余数据类型都占用一个slot。局部变量表所需要的内存在编译期就分配完成,当一个方法进入虚拟机栈时,所需要分配的局部变量空间(slot)时确定的,方法运行期间并不会改变局部变量表大小。
这里插播一条JVM对对象的访问:java通过reference 数据来操作堆上的具体对象,目前流行的对象的访问方式有两种:句柄和直接指针。
- 如果使用句柄访问的话Java堆会划分一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址。好处是reference存储的是稳定的句柄地址,对象被移动时reference本身并不需要改动
- 如果使用直接指针形式,reference中存储的直接就是使用对象。好处是速度快,节省一次执行定位的开销
这块区域会抛出两种异常分别是StackOverFlowError以及OutOfMemoryError:
- 当线程请求的栈的深度大于虚拟机栈的深度时会抛出StackOverFlowError。这个异常通常是单线程情况下发生的
- 若虚拟机栈可以动态扩展,在扩展过程中无法申请到足够内存时,抛出OutOfMemoryError。这个异常一般发生在多线程环境。
鉴于上面2点,当虚拟机栈中由于线程创建过多导致的OutOfMemoryError问题时,可以适当的调整单个线程的大小以增加线程个数。
本地方法栈
本地方法栈与虚拟机栈相似,只不过本地方法栈时位Native方法服务的。同理这块内存区域也会抛出StackOverFlowError以及OutOfMemoryError异常
Java堆内存
堆内存是JVM管理的最大的一块内存区域,此区域存在的目的唯一目的就是存放实例对象,几乎所有的实例对象都在此分配内存,这块内存是所有线程共享的。
由于堆内存是垃圾收集器管理的主要区域,通常情况下Java堆又被称为GC堆。
当对象分配内存过程,如果堆内存没有足够的内存空间完成实例分配时,抛出OutOfMemoryError异常。
方法区
方法区与堆内存一样是所有线程共享的,用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Hotspot虚拟机下,通常方法区被称为“永久代”。
运行时常量池
运行时常量池是方法区的一部分,Class文件中用于存储编译期生成的各种字面量和符号引用的常量池在类被加载后,将进入运行时常量池进行存储。
除了Class文件中的符号引用被爆存在运行时常量池中,一般情况下,被翻译出来的直接引用也将存储在运行时常量池中。
运行时常量池具备动态特性,除了编译期产生的常量,运行期也可以将新的常量放入池中。当运行时常量池无法在方法区申请到足够内存时,也会抛出OutOfMemoryError异常。
直接内存
直接内存并不是JVM运行时数据的一部分。
在JDK1.4版本中引入了NIO(New Input/Output)类,可以使用Native函数库直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,从而避免在Java堆中和Native堆中来回复制数据。
这个内存区域也会抛出OutOfMemoryError异常。
(图片源自网络)
JVM内存异常图.jpg
常量池分类
常量池概念有几个点,这里单独区分一下:
- 全局字符串池:这里保存的全局共享的字符串的引用,并不直接保存字符串
- Class文件常量池:java文件编译成.class文件后就会生成常量池,用于保存编译产生的字面量和符号引用(这里存放的只是引用)。注意:static final常量在编译期就将其结果放入了调用它的类的常量池中
- 运行时常量池:运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Class文件常量池),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。这个常量池,每个类实例对象都能引用到。运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性
参考书籍
本文摘录、整理自周志明的《深入理解Java虚拟机》一书,如想获得更详细介绍可自行查阅此书。