学习内容:第2章 - Java 内存区域与内存溢出异常
相比C、C++等语言,开发 Java 程序时在虚拟机自动内存管理机制的帮助下,不需要手动对对象生命周期进行维护。但如果出现内存泄漏或者内存溢出方面的问题,很难排查错误、修正问题。
-> 学习内容:了解虚拟机如何控制内存
-> 学习目的:掌握排查、修正内存泄漏和溢出方面的问题的能力
思考:通过对中间层对复杂操作进行封装是我们常考虑的操作,相比与白盒操作,这降低了系统的上手难度,一定程度上降低了大型系统的开发难度。与之对应的可能就是可靠性上的降低,这一点需要进行斟酌。
运行时数据区域
按照 Java 虚拟机规范,Java 虚拟机管理的内存将会包含以下几个运行时数据区域:
- 程序计数器(Program Counter Register)
- Java 虚拟机栈(Java Virtual Machine Stack)
- 本地方法栈(Native Method Stacks)
- Java 堆(Java Heap)
- 方法区(Method Area)
程序计数器(Program Counter Register)
特点:
- 是线程私有的内存区域,每个线程都有一个独立的程序计数器,互不影响
- 在虚拟机规范中,该区域是唯一没有规定任何
OutOfMemoryError
情况的区域
工作内容:
字节码解释器工作时通过改变计数器的值来选取下一条需要执行的字节码指令。如果线程执行的是一个 Java 方法,计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是本地(Native)方法,计数器的值为空
作用:
是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复这些基础功能都依赖它来完成
Java 虚拟机栈(Java Virtual Machine Stack)
特点:
- 线程私有
- 生命周期与线程相同
工作内容:
在每个方法执行的时候,虚拟机都会同步创建一个栈帧(Stack Frame)来存储局部变量表、操作数栈、动态连接、方法出口等信息。方法调用直至执行完毕,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
作用:
描述了 Java 方法执行的线程内存模型
局部变量表
存放了编译期可知的各种 Java 虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,不是对象本身,可能是指向对象地址的引用指针,也可能是其他与此对象相关的位置)和 returnAddress 类型(指向一条字节码指令的地址)
这些数据类型在局部变量表中以局部变量槽(Slot)来表示,64 位长度的 long 和 double 会占用两个 Slot,其它类型只占用一个。局部变量表所需内存空间在编译期完成分配,方法运行期间不会改变其大小。而虚拟机真正使用多大内存空间来实现一个变量槽,也完全由虚拟机自行决定。
异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出
StackOverflowError
异常。 - 如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存,就会抛出
OutOfMemoryError
异常
HotSpot 虚拟机的栈容量不可以动态扩展,Classic 虚拟机可以。所以在 HotSpot 虚拟机上不会因为虚拟机栈无法扩展而导致
OutOfMemoryError
异常。但是如果线程申请时就失败,仍然会出现 OOM 异常。
本地方法栈(Native Method Stacks)
特点:
- 与虚拟机栈发挥的作用非常相似,其区别虚拟机栈为虚拟机执行 Java 方法(字节码)服务,而本地方法栈则是为虚拟机用到的本地(Native)方法服务
- 本地方法栈中方法使用的语言、使用方式与数据结构没有任何强制规定,具体虚拟机可以根据需求进行自由实现,像 HotSpot 就直接把本地方法栈和虚拟机栈合二为一
异常状况:
- 与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出
StackOverflowError
和OutOfMemoryError
异常