1. 运行时数据区域
名称 | 是否共享 | 作用 | 存在的异常 |
---|---|---|---|
程序计数器 | 线程私有 | 如果执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址 | |
java虚拟机栈 | 线程私有 | 每个方法在执行的同时会创建一个栈帧(局部变量表,操作数栈等),方法的调用过程,对应一个栈帧在虚拟机栈中的出入栈 | StackOverflowError,OutofMemoryError |
本地方法栈 | 线程私有 | 作用与虚拟机栈类似,但是它是为Native方法服务的 | StackOverflowError,OutofMemoryError |
java堆 | 线程共享 | 所有的对象实例以及数组都要在堆上分配,随着技术的发展,现在变得不是那么绝对了,垃圾回收比较多 | OutofMemoryError |
方法区 | 线程共享 | 用于存储已被虚拟机加载的类信息,常量,静态变量等。垃圾回收在此比较少 | OutofMemoryError |
运行时常量池 | 线程共享 | 属于方法区,用于存放编译期生成的 各种字面量和符号引用 | OutofMemoryError |
2. 对象探究
2.1 对象的创建
类加载检查:
- 遇到new指令时,检查这个指令的参数是否能在常量池中定位到一个类的符号引用。
检查这个符号引用代表的类是否已被加载,解析和初始化过.
- 分配内存:等同于把一块确定大小的内存从java堆中划分出来
- 指针碰撞:假设java堆中的内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,那么就可以用一个指针向空闲空间挪动一段与对象大小相等的距离。
- 空闲列表:假设内存是不规整的,那虚拟机就会维护一个列表,记录哪些内存块是可用的,然后去找一块足够大的划分给对象实例,并更新列表记录。
ps:还要考虑并发问题,有两种解决方案
- CAS来保证更新操作原子性
把内存分配按照线程划分到不同的空间之中,也就是给每一个线程预先分配TLAB(本地线程分配缓存),只有TLAB用完,才会同步锁定
初始化为零值
进行必要设置
比如这个对象是哪个类的实例,对象的哈希码等,还有是否启用偏向锁等,主要是对对象头进行设置
2.2对象的内存布局
2.2.1 对象头
第一部分用于存储对象自身的运行时数据
如哈希码,GC分代年龄,锁状态标识等
第二部分是类型指针
用来确定这个对象是哪个类的实例,如果对象是数组的话,还得有一块用于记录数组长度的数据。
2.2.2 实例数据
- 存储的是有效信息,也就是所定义的各种类型的字段内容。
- 这部分的存储顺序会受 虚拟机分配策略参数,字段在Java源码中的定义顺序的影响
2.2.3 对齐填充
这部分没什么含义的,主要起着占位符的作用,比如对象的大小必须是8字节的整数倍,当对象实例数据部分没有对齐时,就要通过它来填充
2.3对象的访问定位
主流的访问方式有 使用句柄 和直接指针 这两种
1. 使用句柄
- java 堆划分出一块内存空间来作为句柄池,reference中存的就是对象的 句柄地址
最大的好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改句柄中的实例数据指针,而reference本身不需要修改
2. 直接指针
reference中存的就是对象地址
最大的好处是速度更快,节省了一次指针定位的时间开销。
本书讨论的虚拟机是sun hotspot,它用的是直接指针方式