一、对象的访问
Java 中,操作一个对象是通过指向这个对象的引用。对象存在堆中,这个引用存在虚拟机栈中。那么引用通过什么方式去定位堆中对象的位置呢?
1️⃣直接指针法(HotSpot 实现):引用中直接存储的就是堆中对象的地址。好处就是一次定位速度快,缺点是对象移动(GC 时对象移动)引用本身需要修改。
二、创建对象流程
1️⃣当虚拟机遇到一条 new 指令时,会去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查代表的类是否已经被类加载器加载。如果没有被加载那么必须先执行这个类的加载。
2️⃣类加载检查通过后,虚拟机将为新对象分配内存,对象所需内存的大小在类加载后便可以确定。
3️⃣内存分配完成后,虚拟机需要将对象初始化为零值,保证对象的实例变量在代码中不赋初始值就能直接使用。类变量在类加载的准备阶段初始化为零值。
4️⃣对对象头进行必要信息的设置,比如如何找到类的元数据信息、对象的 HashCode、GC 分代年龄等。
5️⃣经过上述操作,一个新的对象已经产生,但是<init>方法还没有执行,所有的字段都是零值。这时候需要执行<init>方法(构造方法)把对象按照程序员的意愿进行初始化。类变量的初始化操作在类加载的初始化阶段<clinit>方法完成。
三、分配内存
1️⃣分配内存有两种方式
- Java 堆内存是规整的(使用标记整理或带压缩的垃圾收集器),使用一个指针指向空闲位置,分配内存既将指针移动与分配大小相等的距离。
- 内存不是规整的(使用标记清除的垃圾收集器),虚拟机维护一个可用内存块列表,分配内存时从列表中找到一个足够大的内存空间划分给对象并更新可用内存列表。
无法找到足够的内存时会触发一次GC。
2️⃣分配内存时并发问题解决方案:
- 对分配内存空间的动作进行同步操作---采用 CAS 失败重试的方式保证更新操作的原子性。
- 每个线程在堆中预先分配一块小内存,称为线程本地分配缓存(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存就在它的 TLAB 上分配,只有 TLAB 用完并分配新的 TLAB 时才需要同步锁定。通过
-XX:+/-UseTLAB
参数来设定。
四、创建对象指令重排序问题
A a = new A();
new 一个对象的简单分解动作:
1️⃣分配对象的内存空间
2️⃣初始化对象
3️⃣设置引用指向分配的内存地址
其中2️⃣3️⃣两步会发生指令重排序,导致多线程时如果在初始化之前访问对象则会出现问题,单例模式的双重检测锁模式会存在这个问题。可以使用volatile来禁止指令重排序解决问题。