一、对象创建的过程
我们先画一个流程图来看一下对象在创建的过程中,经历了哪些步骤:
类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那就会先执行相应的类加载过程。
分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间就是把一块确定大小的内存从Java堆中划分出来。 这个步骤存在两个问题需要思考:
- 1.如何划分内存。
- 2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
划分内存的方法:
“指针碰撞”(Bump the Pointer)(默认用指针碰撞)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点 的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
“空闲列表”(Free List)
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
解决并发问题的方法:
CAS(compare and swap)
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
本地线程分配缓冲(Thread Local Allocation Buffer即TLAB)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。
通过XX:+/ UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB),XX:TLABSize 指定TLAB大小。
初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值【参数类型对应的零值】(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
这些信息存放在对象的对象头Object Header之中。 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
HotSpot虚拟机的对象头包括两部分信息:
- 用于存储对象自身的运行时数据 :如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- 类型指针 :即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。:
这里我们以32位系统为例来分析一下对象头包含了什么内容:
Mark Word
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
PS:其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。
指向类的指针
该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
PS:Java对象的类数据保存在方法区。
数组长度
PS:只有数组对象保存了这部分数据。
该数据在32位和64位JVM中长度都是32bit。
PS:关于对象头的解析,这篇文章写得不错~
执行<init>方法
执行<init>方法,即对象按照程序员的意愿进行初始化。
对应到语言层面上讲,就是为属性赋值(PS:这与上面的赋零值不同,这是由程序员赋的值),执行构造方法。
二、对象大小
我们要分析一个对象结构的话,可以借助一个JOL的工具来帮助我们查看,首先先引入maven依赖:
然后编写一个测试类:
1 package com.happyfat.day3.test; 2 3 import org.openjdk.jol.info.ClassLayout; 4 5 /** 6 * JOL工具计算对象的大小 7 * @author 有梦想的肥宅 8 */ 9 public class JOLTest { 10 public static void main(String[] args) { 11 //打印一个Object对象的大小 12 ClassLayout layout = ClassLayout.parseInstance(new Object()); 13 System.out.println(layout.toPrintable()); 14 System.out.println(); 15 16 //打印一个int数组对象的大小 17 ClassLayout layout1 = ClassLayout.parseInstance(new int[]{}); 18 System.out.println(layout1.toPrintable()); 19 System.out.println(); 20 21 //打印一个类A的对象的大小 22 ClassLayout layout2 = ClassLayout.parseInstance(new A()); 23 System.out.println(layout2.toPrintable()); 24 } 25 26 // ‐XX:+UseCompressedOops 默认开启的压缩所有指针 27 // ‐XX:+UseCompressedClassPointers 默认开启的压缩对象头里的类型指针Klass Pointer 28 // Oops : Ordinary Object Pointers 29 public static class A { 30 /* 8B mark word */ 31 /* 4B Klass Pointer */ 32 //PS:如果关闭压缩‐XX:‐UseCompressedClassPointers或‐XX:‐UseCompressedOops,则占用8B 33 int id; //4B 34 String name; //4B 【如果关闭压缩‐XX:‐UseCompressedOops,则占用8B】 35 byte b; //1B 36 Object o; //4B 【如果关闭压缩‐XX:‐UseCompressedOops,则占用8B】 37 } 38 }