运行时数据区域
1. 程序计数器 Program Counter Register
通过改变计数器值选取下一条指令完成分支、循环、跳转、异常处理、线程恢复等基础功能。
线程私有,互不影响。
2. Java虚拟机栈 JVM Stacks
线程私有,生命周期与线程相同。
每个方法执行时创建栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等,方法调用与完成对应入栈和出栈。
Java内存区粗略分为堆和栈,栈即虚拟机栈。
配置 -Xss
参数设定栈帧大小
3. 本地方法栈 Native Method Stack
执行本地方法的栈。
4. Java堆 Java Heap
线程共享,虚拟机启动时创建,几乎所有对象在这里分配内存。
Java堆分为新生代(Eden
、From Survivor
、To Survivor
)和老年代。
-Xms
最小值,-Xmx
最大值
5. 方法区 Method Area
线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
被成为永久代 Permanent Generation
,配置 -XX:MaxPermSize
上限。
6. 运行时常量池 Runtime Constant Pool
运行时常量池是方法区中一部分,jdk8被放到 MetaSpace
空间。
类的版本、字段、方法、接口等描述信息,还有常量池(Constant Pool Table
),存放编译期生成的各种字面量和符号引用。
jdk7之前,字符串常量池在方法区中,使用 String.intern()
方法可将字符串放入。
7. 直接内存 Direct Memory
不是虚拟机运行时数据区的一部分,也可能OOM异常。
jdk1.4加入NIO,是一种基于通道 Channel
和缓冲区 Buffer
的I/O方式,可以使用 Native
函数库直接分配对外内存,然后通过堆中 DirectByteBuffer
对象作为这块内存的引用进行操作,避免Java堆与Native堆中数据复制来提高效率。
对象
对象的创建
-
遇到new指令时,首先检查指令参数能否在常量池中定位到一个类的符号引用,并检查这个这个符号引用代表的类是否被加载、解析和初始化过。
-
如果没有,执行相应的类加载过程。
-
类加载通过后,虚拟机为新生对象分配内存。
如何分配内存? 对象所需内存大小在加载完成后便完全确定,所以将一块确定大小内存从Java堆中划分出来即可。 若内存规整,采用指针碰撞分配算法,将指针向空闲空间挪动。 若非连续存放,须维护空闲列表,找到足够大空间划分。 `Serial` 串行收集器、`ParNew` 多线程串行收集器等带 `Compact` 过程收集器,分配算法采用指针碰撞。 `CMS` 这种基于 `Mark-Sweep` 算法,采用空闲列表。
-
内存分配完成后,将分配到的内存空间初始化为零值。
-
虚拟机对对象进行必要设置,该实例所属类、类的元数据信息、对象哈希姆、对象GC分代年龄信息等,存放在对象头中。
-
执行
<init>
构造方法,按照程序员的意愿进行初始化,对象引用入栈。
对象的内存布局
对象在内存中存储布局分为 3 块区域:对象头(Header
)、实例数据(Instance Data
)和对齐填充(Padding
)。
-
对象头包括两部分信息
第一部分是存储对象自身的运行时数据
Mark Word
。如哈希码、GC分代年龄、锁状态标识、线程持有锁、偏向线程ID、偏向时间戳等。
另一部分是类型指针,即对象指向它的元数据的指针。
虚拟机通过这个指针来确定该对象是那个类的实例。
-
实例数据
对象真正存储的有效信息,即代码中定义的各类型字段内容。
-
对齐填充
仅是占位符作用。
对象的访问定位
通过栈上的引用 reference
来操作堆上具体对象,主流的访问方式有使用句柄和直接指针两种。
-
句柄访问。
Java堆中将划分出句柄池,'reference' 指向句柄地址,句柄中存放了对象实例数据与类型数据各自的具体地址信息。 优势:在对象被移动时只会改变句柄中实例数据指针,reference本身不需要修改。
-
直接指针访问。
'reference' 直接存储对象地址,Java堆中对象布局中须防止访问类型数据信息。 优势: 访问速度快,因节省了一次指针定位的时间开销。 HotSpot使用直接指针进行对象访问。