运行时数据区
程序计数器/PC寄存器
线程私有
,程序计数器是一块较小的内存空间,当前线程所执行的字节码的行号指示器。
字节码解释器工作时就是通过改变计数器的值来选择下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
如果线程在执行java方法,计数器记录的是正在执行的虚拟机字节码指令地址。如果执行的是Native方法,计数器值为空。
此内存区域是唯一一个在Java虚拟器规范中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈
线程私有
,生命周期与线程相同。描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧。
栈帧用于储存局部变量表、操作数栈、常量池指针、动态链接、方法返回值等信息。每个方法的调用到完成对应着栈帧在虚拟机栈中的入栈到出栈。
每一次方法调用创建一个帧,并压栈,退出方法时,修改栈顶指针就可以把栈帧中的内容销毁。
局部变量表存放了编译期可知的各种基本数据类型和引用数据类型,每个slot(插槽)存放32位的数据,long、double占两个槽位。
栈的优点:存取速度比堆快,仅次于寄存器
栈的缺点:存在栈中的数据大小、生存期是在编译器决定的,缺乏灵活性
此区域可能出现的两种异常:
- 如果出现方法递归调用出现死循环的话就会造成栈帧过多:抛出StackOverflowError异常
- 线程请求的栈深度大于虚拟机所允许的深度:抛出StackOverflowError异常
- 扩展时无法申请到足够内存:抛出OutOfMemoryError异常
本地方法栈
作用与虚拟机栈相似,区别是虚拟机栈为虚拟机执行的Java方法(也就是字节码)服务,而本地方法栈为虚拟机使用的Native方法服务。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常
Java堆
线程共享区域
,是整个虚拟机内存中最大的一块。在虚拟机启动时创建,用于存放对象实例。
垃圾收集器管理的主要区域,从垃圾回收角度看,由于现在收集器基本采用分代回收算法,Java堆可细分为:新生代
和老年代
;再细致有Eden
空间、From Survivor
空间、To Survivor
空间等。
堆的优点:运行期动态分配内存大小,自动进行垃圾回收
堆的缺点:效率相对较慢
可利用参数 -Xms
-Xmx
进行堆内存控制。
此区域可能出现的异常:
- 如果堆中没有完成实例分配,并且堆无法扩展:抛出OutOfMemoryError异常
方法区(JDK1.7)
线程共享区域
,方法区主要用于存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。 这块区域也被称为永久代。
可利用参数 -XX:PermSize
-XX:MaxPermSize
控制初始化方法区和最大方法区大小。
此区域可能出现的异常:
- 当方法区无法满足内存分配需求时:抛出OutOfMemoryError异常
元数据区(JDK1.8)
在 JDK1.8 中已经移除了方法区(永久代),并使用了一个元数据区域进行代替(Metaspace)。
默认情况下元数据区域会根据使用情况动态调整,避免了在 1.7 中由于加载类过多从而出现 java.lang.OutOfMemoryError: PermGen。但也不能无线扩展,因此可以使用 -XX:MaxMetaspaceSize
来控制最大内存。
元数据区由一个或多个虚拟空间(Virtual Space)组成。虚拟空间是操作系统的连续存储空间,是按需分配的。当被分配时向操作系统预留空间,但是没有提交。
元数据区的预留空间(reserve)指全部虚拟空间,虚拟空间的最小分配单元是Chunk,当新的Chunk被分配到虚拟空间时,与Chunk相关的内存空间会被提交(committed),元数据区的提交区域(committed)指所有Chunk占有空间。
每个Chunk占据空间不同,当一个类加载器被回收时,与之关联的Chunk会被释放(freed)。元数据区的容量(capacity)指所有未被释放的Chunk占据的空间。
运行时常量池
运行时常量池是方法区的一部分。是Class文件中每个类或接口的常量池表,在运行期间的表示形式,通常包括:类的版本、字段、方法、接口等信息
直接内存
直接内存不是虚拟机运行时数据区的一部分。它是通过在堆内存中的 DirectByteBuffer
对象操作的堆外内存,避免了堆内存
和堆外内存
来回复制交换复制。
既然是内存,那也得是可以被回收的。但由于堆外内存不直接受 JVM 管理,所以常规 GC 操作并不能回收堆外内存。它是借助于老年代产生的 fullGC 顺便进行回收。同时也可以显式调用 System.gc() 方法进行回收(前提是没有使用 -XX:+DisableExplicitGC 参数来禁止该方法)。
值得注意的是:由于堆外内存也是内存,是由操作系统管理。如果应用有使用堆外内存则需要平衡虚拟机的堆内存和堆外内存的使用占比。避免出现堆外内存溢出。
对象的创建、内存布局、访问定位
对象的创建
(以下探讨的对象限于java对象,不包含数组和Class对象)
- 虚拟机遇到一条new指令时,首先检查这个指令的参数能否在常量池定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有必须先执行相应的类加载过程。
- 在类加载检查完成后,虚拟机将为新生对象从Java堆中分配内存。
- 虚拟机将分配到的内存空间初始化为零值(不包括对象头),接下来将对象是哪个类的实例、如何找到类的元数据信息、对象哈希码、对象的GC分代年龄等信息存放到对象头(Object Header)。至此对象在虚拟机视角已经产生了,但是从java程序时间看,对象创建才刚开始,new指令后会执行
方法,对象按程序员意愿进行初始化,真正可用的对象才创建完成。
对象的内存布局
在HotSpot虚拟机中,对象在内存中的存储布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头
对象头包含两部分:
- 第一部分存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这部分信息称为“Mark Word”;Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间。长度在32位和64位的虚拟机中分别为32bits 和 64bits 空间。
- 第二部分为类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
(如果对象是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组元数据中无法确认数组大小)。32位虚拟机环境下占用32bits 空间,在64位虚拟机开启压缩指针的环境下占用32bits 空间,不开启压缩指针占用64bits 空间。
32位HotSpot虚拟机对象头Mark Word内容如图所示:
实例数据
存放对象实例数据
对齐填充
不一定存在,占位符,没有特殊含义,HotSpot要求对象起始地址必须是8的整数倍,对象实例部分没有对齐时需要通过对齐填充补齐。
对象的访问定位
对象的访问定位取决于具体的虚拟机实现。创建对象实例后,需要通过虚拟机栈中的reference类型数据来操作堆上的对象。现在主流的访问方式有使用句柄和直接指针两种:
使用句柄访问
Java堆会划分一块内存作为句柄池,reference中存的是对象的句柄地址,句柄中包含对象实例数据和类型数据各自的具体地址信息。
优势:reference中存储的是稳定的句柄指针,对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接内存访问
优势:速度更快,节省了一次指针定位开销。
Trace追踪和参数配置
- 打印GC简要信息: -Xlog:gc (jdk9+)
- 打印GC详细信息: -Xlog:gc*
- 指定GC log位置,以文件输出:-Xlog:gc:gc.log
- 每次GC后打印堆信息:-Xlog:gc+heap=debug
GC日志格式(G1):
- GC发生时间,即JVM启动以来秒数
- 日志级别信息、日志类型标记
- GC识别号
- GC类型和说明GC原因
- 容量:GC前容量->GC后容量(该区域总容量)
- GC持续时间,单位秒
参数:
- -Xms :初始堆大小,默认物理内存1/64
- -Xmx :最大堆大小,默认物理内存1/4
- -Xmn :新生代大小,默认物理内存3/8,推荐大小为总堆大小25%-50%
- -XX:+UseConcMarkSweepGC 使用CMS垃圾回收器
- -XX:NewRatio 老年代与新生代比值
- -XX:SurvivorRatio eden区与survivor区比值
- -XX:+HeapDumpOnOutOfMemoryError导出内存溢出的堆信息
- -XX:HeapDumpPath 指定dump导出路径
- -XX:MetaspaceSize 元空间初始大小
- -XX:MaxMetaspaceSize 元空间最大内存,默认无限制
- -XX:MinMetaspaceSizeFreeRatio GC后最小元空间剩余容量百分比
- -XX:MaxMetaspaceSizeFreeRatio GC后最大元空间剩余容量百分比