Java中对象是怎么回事?
1.对象在内存中的存储?
当我们new一个Java对象时,new出来的Java对象都包含了什么东西,每部分都占多少字节,作为Java工程师还是有必要了解这个东西的。
一个对象包含以下四部分:
- markword(8字节):用来存放对象hashcode、锁状态、锁标志位等信息
- 类型指针(4字节):对象是什么类型的,指向T.class
- 实例数据:类的成员变量所占的内存
- 对齐:一个对象所占字节数要被8整除,否则需要对齐
以下是使用JOL(Java Object Layout)分别将一个空对象和有成员变量的对象的内存结构:
空对象
可以在图片中看到,markword8个字节,类型指针4个字节,一共12个字节,不能被8整除,所以有对齐4个字节,即图中的“loss due to next object aligment”,当加上一个Integer类型的成员变量之后,刚好一共16个字节,就不需要对齐了,大家有兴趣可以试试。
public class test {
// Integer i;
// String s;
public static void main(String[] args) {
test t=new test();
System.out.println(ClassLayout.parseInstance(t).toPrintable());
}
}
有成员变量的对象
public class test {
Integer i;
String s;
public static void main(String[] args) {
test t=new test();
System.out.println(ClassLayout.parseInstance(t).toPrintable());
}
}
1.1 面试题
以下对象在内存中占用多少个字节?
object o=new Object();
上面讲过,对象中一共有四部分,分别是markword(8字节)、类型指针(4字节)、实例数据和对齐,Object中是没有属性的,所以没有实例数据,但由于对象头+类型指针一共只有12个字节,不能被8整除,所以需要对齐4个字节,一共占用16个字节,以下是代码和结果:
public class test {
public static void main(String[] args) {
Object o=new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
2.对象头是什么?
在上面截图当中有一个object header,翻译过来就是对象头,其实就是markword+类型指针,一共12个字节。
对象头中最重要的就是markword,其中包含了hashcode、锁状态、锁标识位等信息,主要分为三大块:
- 锁信息
- hashcode
- GC信息
下面以加synchronized锁为例,看看markword是怎么变化的,代码和结果如下:
public static void main(String[] args) {
//加锁之前
Object o=new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
//加锁之后
synchronized (o){}
System.out.println(ClassLayout.parseInstance(0).toPrintable());
//解锁之后
System.out.println(ClassLayout.parseInstance(0).toPrintable());
}
可以明显地看到,加锁前和解锁后的markword是相同的,加锁之后markword不一样。
除了加锁之后markword会变化,在调用对象的hashcode()方法之后,markword也会发生变化,因为markword中会记录对象的hashcode值,大家也可以试一试。
3.对象如何定位
-
句柄方式(间接方式)
对象引用指向堆中的句柄池,然后在句柄池中分别获取对象地址和类型地址,以此来找到对应的对象数据和类型数据。
这样的缺点就是多浪费了一次寻址开销,但当对象发生改变时,o的地址不需要改变,只需要改变句柄中的地址信息,会使GC更高效。
-
直接指针(直接方式)
对象引用指向堆中的对象,对象中存储着指向方法区的类型数据,可以通过找到对象获取类型指针,然后再获取类型数据。
使用直接方式的优点就是速度快,相比于句柄方式节省了一次寻址的开销,这种方式也是hotspot中所用的,但是当对象发生改变时,o的地址也需要改变。
4.对象如何分配?
目前使用的GC算法都是分代收集算法,那么也就可以知道整个堆是被分为了两部分,分别是新生代和老年代,不同类型的对象放在不同的地方。
一般来说,新创建的对象都会放在新生代中,有以下几种情况会直接放到老年代中或移动到老年代中:
- 当创建的对象所需连续内存太大时,则会放到老年代中
- 当新生代中的对象年龄超过15(默认)时,就会将对象移动到老年代中