主要记录一下学习阅读周志明先生的《深入理解Java虚拟机》,并且总结相关知识。
运行时数据区域
Java虚拟机会在执行Java程序时把内存划分为不同的内存区域。不同的内存区域的生命周期、所产生的异常都是不一样的。Java虚拟机所管理的内存包括以下几个运行时数据区域:
程序计数器
该内存区域能够产生的异常: 程序计数器是Java运行时数据区域唯一一个不会发生OOM
异常的区域。
程序计数器(Program Counter Register):可以把它看作是执行的字节码的行号指示器。字节码解释工作就是通过程序计数器
的值来选取下一条需要执行的指令。程序中分支、循环、跳转、异常处理以及线程的刮挂起/恢复都是由程序计数器来指示。
线程私有:多线程是通过CPU上下文切换,并且分配处理时间,实际上同一时刻都只会执行一条指令。为了在切换过程中能够准确地找到下一条执行指令。程序计数器是线程私有的。
对于Java方法,程序计数器记录的是当前正在执行的字节码指令的地址;对于Native方法,程序计时器的值为空(Undefined
)。
Java虚拟机栈
线程私有:在Java方法被调用时,Java虚拟机会为此创建一个栈帧(Stack Frame),用来存储局部变量表
、操作数栈
、动态连接
、方法出口
等信息,每个方法从调用到完成,对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表
- 存储数据
- 存储了在编译期可知的Java基本数据类型(Java的8大基本数据类型):
boolean
、byte
、char
、short
、int
、float
、long
、double
- 存储了对象的引用(
Reference
类型,可能是一个指向对象的起始地址的引用指针,或者是一个能标识对象位置的句柄等) - 存储了方法执行的字节码指令的地址,
Return Address
- 存储方式
局部变量表中的存储空间用局部变量槽(slot
)来表示。其中64位的long
和double
占用了两个变量槽。其他数据类型占一个
局部变量表所占用的内存空间在编译时就已经被分配好,在运行期间是无法改变的
该内存区域能够产生的异常:
- Out Of Memory Error:根据书中介绍,在HotSpot虚拟机中,虚拟机栈很难出现内存溢出异常,方法所需的栈帧内存大小在编译期间就已经确定了,因此只要能申请内存空间成功,就一定不会出现OOM。而当申请失败时,会直接导致OOM。
- StackOverFlow Error:如果线程执行的栈深度大于了虚拟机所允许的深度,会抛出该异常。例如死递归(无递归出口和递归终止条件)。
本地方法栈
本地方法栈与虚拟机栈大体相同,唯一的区别是本地方法栈(Native Method Stacks
)是为虚拟机调用本地方法服务的。
Java堆
首先,Java堆是内存空间最大的一块区域。用来存放Java对象。
线程共享:Java堆是所有线程共享的,Java对象的内存分配几乎都在Java堆上完成。
管理方式:Java堆是通过垃圾收集器来进行管理的,通过垃圾回收(Garbage Collected)来对所有对象进行管理。
Java堆能产生的异常:Java堆内存大小是可以扩展的,在启动Java虚拟机时,通过配置Jvm参数-Xms:初始堆内存大小
和-Xmx:最大堆内存大小
,来指定堆内存。因此当没有足够的空间来为对象分配内存时,会抛出OOM
异常。
方法区
方法区(Method Area):存储了已被虚拟机加载的类基本信息、静态变量、常量以及被即时编译器(Just In Time,JIT)编译好的代码缓存等,比较常见的就是:字符串常量池。
永久代(JDK8之前):永久代是实现Java方法区的一种方案。Java虚拟机规范中并未规定方法区的具体实现细节。永久代为了沿用堆中的垃圾回收算法进行管理,因此是放在Java堆上,但是由于这种设计模式更容易导致OOM的发生。目前已被元空间(Meta Space)替代。采用本地内存来实现方法区。
该区域能产生的异常:当无法为常量等分配内存空间时,会抛出OOM异常。
运行时常量池
运行时常量池:是方法区的组成部分之一。方法区会存储Class文件的相关信息:类的版本、字段、方法、接口等类的描述信息。同时,也会存储编译期生成的各种字面量和符号引用,这部分信息则是存储在运行时常量池(Constant Pool Table
)中。
动态性:运行时常量池是具备动态性的,在运行期间,也是可以往里面放新的常量的,例如java.lang.String#intern
,当字符串不存在时,则会往常量池放入该字符串,并返回字符串的地址。
该区域能产生的异常:既然是方法区的一部分,那么肯定也是在无法分配内存时抛出OOM异常。
直接内存(非Java运行时数据区域)
直接内存:不受Java虚拟机内存限制,但是会受到本机内存的限制,Java中有用到操作直接内存的,比较常见的是NIO
包,基于网络和缓存实现的IO模式,可以通过Native方法分配直接内存,从而提高性能。
DirectByteBuffer
构造器代码,直接通过UnSafe
包进行内存分配
1 | DirectByteBuffer(int cap) { // package-private |
该区域能产生的异常:当各个区域的内存总和大于物理内存总和时,也是会抛出OOM异常。