运行时方法区已经讲完了, 那么new的对象是在堆中的, 它的类信息在方法区, 而局部变量在虚拟机栈中. 接下来我们梳理的是内存层面对象到底是怎么实例化, 内存布局是怎样的.
对象创建
对象创建的方式如下:
创建对象步骤
从字节码角度看, 用如下代码测试.
public class ObjectTest {
public static void main(String[] args) {
Object obj = new Object();
}
}
0: new #2 //加载Object类, 在堆中开辟内存空间(为int, byte等变量), 并对内存初始化
3: dup //栈空间中建立引用
4: invokespecial #1 //调用Object构造器, 如果有参, 就要放在操作数栈中, 并执行static代码块等赋值操作
7: astore_1
8: return
可以总结为6步.
- 首先判断对象对应的类是否加载, 链接, 初始化. 虚拟机遇到一条new指令, 首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用, 并且检查这个符号引用代表的类是否已经被加载, 解析和初始化. 若没有就双亲委派, 使用当前类加载器以ClassLoader+包名+类名为Key进行查找对应的class文件. 如果没有找到文件, 则抛ClassNotFoundException异常, 若找到, 就加载类, 并生成对应的Class类对象.
- 为对象分配内存. 首先计算对象占用空间大小, 接着在堆中划分一块内存给对象. 如果内存规整, 使用指针碰撞. 如果内存不规整,虚拟机需要维护一个列表, 使用空闲列表分配.
- 处理并发安全问题(1. CAS失败重试, 区域加锁: 保证指针更新操作的原子性; 2. TLAB把内存分配的动作按照线程划分在不同的空间之中进行, 即每个线程在Java堆中预先分配一小块内存, 称为本地线程分配缓冲区).
- 初始化分配到的空间. 虚拟机将分配到的内存空间都初始化为零值(除对象头). 这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用.
- 设置对象的对象头. 将对象的所属类(即类的元数据信息), 对象的HashCode和对象的GC信息, 锁信息等数据存储在对象的对象头中. 这个过程的具体设置方式取决于JVM实现.
- 执行init方法进行初始化. 初始化成员变量, 执行实例化代码块, 调用类的构造方法, 并把堆内对象的首地址赋值给引用变量.
对象的内存布局
对于代码
public class CustomerTest {
public static void main(String[] args) {
Customer cust = new Customer();
}
}
public class Customer {
int id = 1001;
String name;
Account acct;
{
name = "匿名客户";
}
public Customer() {
acct = new Account();
}
}
内存空间状态如下:
main这个线程的局部变量表就有args和cust(静态变量没有this). 而cust指向堆空间的Customer实例. 类型指针指向了方法区.
对象的访问定位
那么怎么通过栈帧中的对象引用访问到其内部的对象实例的呢?
一般分为句柄访问和直接指针(HotSpot采用), 分别为如下两张图.
直接内存
直接内存指Java堆外的, 直接向系统申请的内存区间. jdk8后的元空间就是用的直接内存.
todo
参考
comments powered by Disqus