运行时数据区域
程序计数器
当前线程所执行的字节码的行号指示器
Java虚拟机栈(java方法的内存模型)
每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息
局部变量表中可以存放基本数据类型、引用类型、returnAddress类型(指向了一条字节码指令的地址)
其中long和doube类型的数据会重用2个局部变量表空间,其余的数据类型只占用1个。
本地方法栈
与Java虚拟机栈所发挥的作用相似,只不过Java虚拟机栈执行java方法,而本地方法栈为Native方法服务
Java堆
所有的对象实例和数组都要在堆上分配。
垃圾收集器管理的主要区域
新生代、老年代、Eden空间、Survivor空间...
方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 类的版本
- 字段
- 接口
- 方法
运行时常量池
方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。
例如:
String s1 = "abc";//放入常量池
String s2 = "abc";//放入常量池(可以理解常量池为 HashSet)
//常量池s3 引用堆内存中的 new String("abc");
String s3 = new String("abc");
s1 == s2 //true
s1 == s3 //false
直接内存
对象
对象的创建
1、当虚拟机遇到内存创建的指令的时候,来到了方法区,找到方法区中有没有符号引用(类信息存在的一种原始形式,字符串)
2、检查该符号引用有没有被加载、解析和初始化过,如果没有则执行类加载过程,否则直接准备为新的对象分配内存
3、分配内存分为指针碰撞和空闲列表两种方式;分配内存还要要保证并发安全,有两种方式。
指针碰撞:前提是堆内存中的空闲空间十分的规整,使用与未使用的空间全部为连续,只需移动一下指针就可以了
空闲列表:针对堆内存中的空间零散的存在,虚拟机维护着一个列表,记录着哪里被分配了,哪里还空闲
CAS 命令的方式来控制操作是同步的
本地线程分配缓冲TLAB(Thread Local Allocation Buffer):在堆中为每一个线程分配一小块独立的内存,这样以来就不存并发问题了,Java 层面与之对应的是 ThreadLocal 类的实现
4、分配完内存后要对对象的头(Object Header)进行初始化,这新信息包括:该对象对应类的元数据、该对象的GC代、对象的哈希码
5、最后,一个新对象的产生后还需要执行构造器中的命令,来完成Java层面的初始化,在 JVM 里为 方法。到此一个新生的对象就产生了出来,准备被使用。
对象的内存模型
对象的头(Object Header)
在对象头中有两类信息:标志信息(Mark Word)和元信息指针(Kclass Pointer)
标识信息用来存放对象一些固有属性的状态,这些属性从对象创建就有,而不是 Java 的使用者定义的:
- 哈希码:对象的唯一标识符
- 对象的分代年龄:与垃圾回收有关
- 线程持有的锁
- 锁的状态
- 偏向线程 ID、偏向时间戳
- 数组长度:如果该对象是数组,会有数组长度信息
元信息指针是指向方法区中类元信息的指针。
实例的信息
实例的信息存放的是一些对 Java 使用者真正有效的信息,也就是类中定义的各个字段,其中还包括从父类继承的字段。
对齐填充
对其填充这段内存段存在与否取决于前面两部分的长度,为了保证对象内存模型的长度为 8 字节的整数倍,这也是虚拟机自动内存管理的要求。
使用对象
对象创建起来之后,就会在虚拟机栈中维护一个本地变量表,用于存储基础类型和基础类型的值,引用类型与引用类型的值。
其中引用类型的值就是堆中对象地址。如何引用堆中地址有两种方式:
- 句柄:在堆中维护一个句柄池,句柄中包含了对象地址,当对象改变的时候,只需改变句柄,不需要改变栈中本地变量表的引用
-
直接指针:对象的地址直接存储在栈中,这样做的好处就是访问速度变快