JVM要点笔记
来源
- 内容来自《深入理解Java虚拟机》。
- 这里只是要点笔记。
运行时数据区域
程序计数器
是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
线程私有。
线程执行本地方法时,计数器值为空。
不存在
OutOfMemoryError
。
Java虚拟机栈
- 线程私有,生命周期与线程相同。
- 描述
Java
方法执行的线程内存模型,每个方法执行时,Java
虚拟机都会同步创建栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 - 线程请求栈深大于虚拟机所运行的深度,抛出
StackOverflowError
。 - 若Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出
OutOfMemoryError
。(HotSpot
不支持栈容量动态扩展,故不存在因虚拟机栈无法扩展导致的OutOfMemoryError
,只存在线程申请栈空间失败时导致的OutOfMemoryError
。)
本地方法栈
- 本地方法栈为虚拟机使用到的本地方法服务。
- 线程私有。
- 栈深度溢出抛出
StackOverflowError
。栈扩展失败抛出OutOfMemoryError
。 Hotspot
虚拟机直接将本地方法栈和虚拟机栈合二为一。
Java堆
- 线程共享的内存区域。
- 几乎(逃逸分析技术,栈上分配、标量替换优化)所有的对象实例在
Java
堆里分配。 - 线程共享的
Java
堆可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer
,TLAB
)。 - 没有内存完成实例分配且堆无法再扩展(
-Xmx
,-Xms
设定),抛出OutOfMemoryError
。
方法区
- 线程共享的内存区域。
- 存储虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- 方法区的垃圾回收针对的是常量池的回收和类型的卸载。
- 方法区无法满足内存分配需求,抛出
OutOfMemoryError
。
方法区、永久代、元空间的关系?
永久代和元空间都是方法区的实现方式。
Jdk8
前,HotSpot
使用永久代实现方法区,使用的堆内存,为了方法区的垃圾回收跟Java堆的机制保持一致,省去方法区内存管理的额外工作。Jdk8
后,HotSpot
跟JRockit对齐,采用元空间实现方法区,使用的本地内存,减少了内存溢出的问题。
运行时常量池
- 方法区的一部分。
- 存放类文件中的常量池表的内容(编译期生成的各种字面量与符号引用)。
直接内存
- 不是虚拟机运行时数据区的一部分。
NIO
使用的Java虚拟机堆外分配的内存区域。- 没有内存满足动态扩展(
-XX:MaxDirectMemorySize
)时,抛出OutOfMemoryError
。
直接内存和本地内存的区别?
Java
直接内存和本地内存不是完全相同的概念。Java
直接内存是通过Java NIO
库中的ByteBuffer
类创建的,它们是在Java虚拟机堆外分配的内存区域。本地内存是指操作系统分配的内存区域,它可以通过
Java
的JNI
(Java Native Interface
)访问。都可以被用于提高性能,但是它们在实现和使用上有一些不同。
对象
对象的创建
主要包括类加载、分配内存、初始化、引用赋值等过程。
类加载
在 Java
中,对象的创建必须先加载类。当 JVM
第一次遇到某个类的实例化请求时,就会通过类加载器加载该类。
对象(普通Java
对象)的创建通常(复制、反序列化除外)通过new
关键字创建。虚拟机中:
- 检查字节码指令
new
的参数是否在常量池中定位类的符号引用。 - 检查符号引用代表的类是否被加载、解析和初始化过。
- 没有的情况下,进行相应的类加载过程。
分配内存
在类加载完成后,JVM 会在堆中分配内存空间来存储该对象。分配内存的方式有两种:指针碰撞和空闲列表。在指针碰撞的方式中,堆被划分为两个区域,一边是已经被占用的内存,另一边是未被占用的内存。在空闲列表的方式中,JVM 会维护一个列表来记录哪些内存块是可用的。
初始化
在分配完内存后,JVM 会对对象进行初始化。初始化过程包括为对象的成员变量赋初值、执行构造方法等操作。
引用赋值
在对象初始化完成后,JVM 会将对象的引用返回给程序员。程序员可以将该引用赋值给变量,从而实现对对象的引用。
对象的内存布局
对象头
运行时数据MarkWord
哈希码、
GC
分代年龄、锁状态标志、线程持有的锁、偏向锁线程ID
、偏向时间戳等。HotSpot
虚拟机对象头的MarkWord
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
- 未开启压缩指针情况下,32位虚拟机占4个字节,64位虚拟机占8个字节。
- 开启压缩指针情况下,32位虚拟机占4个字节,64位虚拟机占4个字节。
类型指针
- 对象指向它类型元数据的指针,JVM通过该指针确定该对象属于哪个类的实例。(直接指针访问对象的情况下)
- 不是所有的虚拟机实现都必须在对象数据上保留类型指针。(句柄访问对象的情况下)
- 未开启压缩指针情况下,32位虚拟机占4个字节,64位虚拟机占8个字节。
- 开启压缩指针情况下,32位虚拟机占4个字节,64位虚拟机占4个字节。
数组长度
- 对象是
Java
数组情况下,用于记录数组长度的数据。 - 占4个字节。
实例数据
存储对象的字段信息
对齐填充
如何计算一个Java对象占用多少内存?
对象的访问定位
句柄访问
reference
存储的对象的句柄地址。- 句柄包含对象实例数据这类型数据各自具体地址。
直接指针访问
reference
存储对象地址。直接指针访问好处速度快。
HotSpot
采用直接指针进行对象访问。