在java中,创建对象的方式有很多种。最常见的就是new关键字了。除此之外,还有反射,clone(),反序列化以及Unsafe.allocateInstance。其中,反序列化和clone()是直接复制已有的数据来初始化对象的字段。Unsafe.allocateInstance 没有初始化对象的字段。new和反射则是调用构造方法来初始化实例字段的。
下面是new关键字的字节码
test test = new test();
--------------------------------
0: new #13 // class com/lv/ddpay/test/test
3: dup
4: invokespecial #14 // Method "<init>":()V
7: astore_1
可以看到,编译来的字节码包含new指令,和用来调用构造方法的invokespecial指令。
当然,对于构造方法,java有很多约束
- 如果一个类没有定义构造方法,那java编译器就会自动添加一个构造方法
- 子类的构造方法需要调用父类的构造方法。如果父类的构造方法时无参构造方法,那么可以隐式调用,就是说java编译器会自动增加对父类构造方法调用的指令。如果父类没有无参构造方法,那子类就只能显示的调用。显示调用有两种
- 使用super关键字直接调用父类构造方法
- 使用this关键字调用同类里的别的构造方法
无论是super还是this,都需要作为构造方法里的第一条语句
总结来讲,就是说当调用构造方法的时候,会优先调用父类构造方法,一直到Object类。
从上面可以看出,使用new来创建对象,内存里其实拥有自己和父类的所有实例字段。即虽然子类不能访问父类的私有字段,但那些字段还是被分配了内存。下面就来看看java对象是怎么在内存里的。
压缩指针
在jvm里,每个对象都有一个由类型指针和标记字段组成的对象头。其中,标记字段时存储该对象运行时的数据,譬如hash,gc信息。在64位的jvm里,标记字段占64位,类型指针也占64位,就是说,每个对象在java内存里的额外开销有16个字节。所以,位了减少对象的额外内存,64位虚拟机就有了压缩指针这一东西。jvm参数是 -XX:-UseConpressedOops ,默认是开启的。开启之后,对象指针就能压缩到32位。压缩指针有个对应的东西就内存对齐。jvm参数是 -XX:ObjectAlignmentInBytes 默认值是8.就是说,在jvm堆中,对象的起始地址都要对齐到8的倍数。如果一个对象所需的内存用不到8的倍数,那空出来的部分就直接浪费了。默认情况下,jvm中的32位压缩指针可以寻址到2的35次方个字节,也就是32GB的空间。
内存对齐不仅仅在对象和对象之间,字段之间也是存在的。字段对齐的一个重要原因,是让字段只出现在一个CPU的缓存行里。如果字段不对齐,就很有可能出现跨缓存行的字段,这对于程序运行时非常不利的。
jvm为了让字段对齐,还有一个重要的东西就是字段重排序。就是jvm重新分配字段的先后顺序以达到内存对齐的效果。jvm里有三种方法,让字段重排序。jvm字段 -XX:FieldAllocationStyle 默认值为1.这三种方法都会遵循拉你两个规则
- 子类继承的字段的偏移量要和父类对于的字段偏移量保存一致
- 如果一个字段占据A个字节,那么该字段的偏移量需要对齐值AX。即A的整数倍数
当然,在java里面还有个东西叫虚共享。java8里有个注释叫@Contended,该注解也会影响字段的排序。譬如两个线程访问不同的volatile修饰的字段,从逻辑层面上看,并没有共享,所以不需要同步。但如果中这两个字段恰好在同一个缓存行里,那么写操作也就造成了实际内存上的共享。因此jvm就会让有@Contended注解的字段单独的处于缓存行里,因此内存也就会大量浪费。
总结
对象的构造有多种方法。常见的new关键字会被编译成new指令,然后调用对应的构造方法。构造方法的调用会先父类在子类依次调用。64位jvm为了节省空间引入了压缩指针的概念,将64位类型指针压缩成32位,使每个对象头都能节省4字节的空间。但,堆大小超过32GB的话,压缩指针就失去了效用。压缩指针要求jvm堆中的对象的起始地址要是-XX:ObjectAlignmentInBytes 设置的值的倍数。jvm还会对字段进行重排序,是字段也能内存对齐。
最后,想具体看看内存的偏移量的同学,可以使用工具JOL好吧。