第2章 Java内存区域与内存溢出异常
2.1 概述
2.2 运行时数据区域
- Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图所示
2.2.1 程序计数器
概念模型:代表了所有虚拟机的统一外观,但是各个虚拟机并不一定完全按照概念模型的定义来设计
- 程序计数器是一块较小的内存空间
- 可以看作是当前线程的一个行号指示器,用于帮助字节码解释器根据程序计数器的值选取下一条要执行的字节码指令
- 也即程序控制流的指示器:分之、循环、跳转、异常处理、线程恢复等
- “线程私有”的内存:每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储
Java虚拟机处理多线程是通过线程切换,分配处理器执行时间来实现的,当某一线程未分配到处理器资源时,需要记录当前线程的执行位置,方便线程切换后能从正确的位置开始执行
- 线程执行Java方法,计数器记录的为正在执行的虚拟机字节码指令的地址
- 线程执行本地(Native)方法:计数器值为空
native方法:方法实现由非Java语言实现,比如C/C++。用于实现Java与其他语言的互动
执行本地方法时,计数器值为空。在线程切换时如何保证准确执行应该执行的指令?
2.2.2 Java虚拟机栈
- Java虚拟机栈是线程私有的,生命周期与线程同步
- Java虚拟机在每个方法被执行的时候都会创建一个栈帧
- 栈帧存放内容:
- 局部变量表
- 操作数栈
- 动态连接
- 方法出口等
- 每个方法被调用直到执行完毕,就对应着一个栈帧入栈到出栈
提到虚拟机栈,人们通常关注的是虚拟机栈中的局部变量表部分
- 局部变量表(编译期可知)存放内容:
- 基本数据类型:boolean、byte、char、short、int、 float、long、double
- 对象引用:reference类型,不等同于对象本身,可能是一个指向对象起始 地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置
- returnAddress类型:指向了一条字节码指令的地址
- 以局部变量槽(Slot)为单位对上述数据类型进行存储,64位长度的long和double占2个变量槽,其余占一个
- 由于局部变量表存放的数据类型是编译期可知的,因此在进入一个方法时,在帧栈中分配多大的局部变量空间是固定的
注意:分配空间固定是指槽的数量固定,具体占用多大的内存空间,由虚拟机确定,例如一个槽占32bit、64bit还是更多
- Java虚拟机栈存在的两种异常情况
- 1.StackOverflowError:当线程请求的栈深度超出虚拟机允许的最大深度
- 2.OutOfMemoryError:虚拟机容量可扩展时,在栈扩展时申请不到足够的内存。或者不允许动态扩展时,线程没成功申请到所需栈空间也会抛出此异常
2.2.3 本地方法栈
- 作用与虚拟机栈类似
- 虚拟机栈为虚拟机执行Java方法服务
- 本地方法栈为虚拟机执行本地方法服务
2.2.4 Java堆
- Java堆在虚拟机启动时创建
- Java堆被所有线程共享(只是共享了空间,但是每个线程创建的数据是独享的)
- Java堆唯一的目的是存放对象实例(几乎所有的对象实例都是在堆中分配内存)
- Java堆是垃圾收集器管理的内存区域(因此也称为GC堆)
- Java堆中也可以划分出多个线程私有的分配缓冲区,用于提升分配效率
- Java堆可以处于物理上不连续的区域,但是逻辑上是连续的
- 关于异常,若堆中没有内存用于实例分配,且堆空间也无法拓展,Java虚拟机会抛出OutOfMemoryError
2.2.5 方法区
- 方法区是各个线程共享的内存区域
- 存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- 常量和静态变量的区别
- JDK7时的HotSpot将原本放在永久代的字符串常量池、静态变量移出
- JKD8时该用在本地内存中实现的元空间来代替
2.2.6 运行时常量池
- 运行时常量池是方法区的一部分
- Class文件包含:类的版本、字段、方法、接口以及常量池表
- 常量池表用于存放:编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
- 由于Java并不要求常量在编译期产生,运行期间也可以将新的常量放入池中。因此运行时常量池相较于Class文件中的常量池而言,具备动态性
- 关于异常,当常量池无法申请到内存时会抛出OutOfMemoryError