文章目录
Object object = new Object() 谈谈你对这句话的理解?一般而言JDK8按照默认情况下,new一个对象占多少内存空间
在HotSpot虚拟机里,对象在堆内存
中的存储布局可以划分为三个部分:
-
对象头
-
实例数据
-
对齐填充
对象头
分为对象标记(markOop)
和 类元信息 (klassOop)
类元信息
存储的是指向该对象 类元数据(klass)的首地址。
引出问题
public class Demo01 {
public static void main(String[] args) {
Object o = new Object();//?new 一个对象,内存占多少,记录在哪里?
System.out.println(o.hashCode());//356573597,这个hashCode又是记录在哪里的
synchronized (o){//加锁信息又是记录在哪里的
}
System.gc();//手动垃圾收集中,15次可以从新生代到养老区,那这个次数又是记录在哪里的
}
}
刚刚几个问题都保存在对象标记里
贴一张对象内部结构图:
1、对象头
对象标记Mark Word
用于存储自身运行时的数据例如CG标志位、哈希码、锁状态等信息
在64位系统中,MarkWord占了8个字节,类型指针占了8个字节,一共是16个字节
类元信息(又叫类对象指针)Class Pointer
所谓的类元信息(类对象指针)其实就可以说是模板,用于存放方法区Class对象的地址,虚拟机通过这个指针来确定这个对象是哪个类的实例
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
数组长度(Array Length)(可选)
如果对象是一个Java数组,那么该字段必须存在,用于存放数组长度,占用32位字节
2、实例数据(对象体)
实例数据:存放类的属性(Field)信息,包括父类的属性信息,这部分内存按4字节对齐
3、对齐填充
用来保证Java对象所占内存字节数为8字节的倍数,
对齐填充:虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐。对象头本身是8的倍数,当对象的实例数据不是8 的倍数时,需要使用填充数据来保证8 字节的对齐
来个案例,对象头16+实例数据5+对齐填充3=24字节
4、指针压缩
对于对象指针来说,如果JVM中的对象数量过多,使用64位的指针将浪费大量内存(比32位多浪费50%),为了节约内存可以使用选项+UseCompressedOops 开启指针压缩
以下类型指针会从64位压缩到32位:
- Class对象的属性指针(静态变量)
- Object对象的属性值指针(成员变量)
- 普通对象数组的元素指针
5、再聊对象头的MarkWord
底层源码涉及到的一些字段:
- hash:保存对象的哈希码
- age: 保存对象的分代年龄
- biased_lock: 偏向锁标识位
- lock: 锁状态标识位
- JavaThread :保存持有偏向锁的线程ID
- epoch: 保存偏向时间戳
6、JOL分析对象在Java虚拟机中的大小和布局
导入依赖
Demo测试:
public class JolDemo {
public static void main(String[] args) {
//Vm的细节详细情况
System.out.println(VM.current().details());
//所有的对象分配的字节都是8的整数倍
System.out.println(VM.current().objectAlignment());
}
/**
* 运行情况:
* # Running 64-bit HotSpot VM.
* # Using compressed oop with 3-bit shift.
* # Using compressed klass with 3-bit shift.
* # Objects are 8 bytes aligned.
* # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
* # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
*
* 8
*
* 进程已结束,退出代码0
*/
}
看看Object 类:
Object obj = new Object()
public class JolDemo {
public static void main(String[] args) {
Object o = new Object();//----------新建一个Object对象就是 16bytes
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
为什么类型指针是4字节?之前不都是说是8字节的吗?(因为压缩指针默认开启了)
再来看看自定义的类:
public class JolDemo {
public static void main(String[] args) {
Customer c1 = new Customer();
System.out.println(ClassLayout.parseInstance(c1).toPrintable());
}
}
//只有对象头,没有实例数据,依然是16byte
class Customer{
}
/**
* 运行结果:
* com.hh.demo.Customer object internals:
* OFFSET SIZE TYPE DESCRIPTION VALUE
* 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
* 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
* 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
* 12 4 (loss due to the next object alignment)
* Instance size: 16 bytes
* Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
*
*
* 进程已结束,退出代码0
*/
public class JolDemo {
public static void main(String[] args) {
Customer c1 = new Customer();
System.out.println(ClassLayout.parseInstance(c1).toPrintable());
}
}
//有了对象头,且有实例数据(int+boolean),它进行了对齐填充,到了24byte
class Customer{
int id;
boolean flag = false;
}
/**
* 运行结果:
* com.hh.demo.Customer object internals:
* OFFSET SIZE TYPE DESCRIPTION VALUE
* 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
* 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
* 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
* 12 4 int Customer.id 0
* 16 1 boolean Customer.flag false
* 17 7 (loss due to the next object alignment)
* Instance size: 24 bytes
* Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
*
*
* 进程已结束,退出代码0
*/
GC年龄采用4位bit存储,最大位15,例如MaxTenuringThreshold参数默认值就是15
- 对象分代年龄最大就是15
- 我们假如想直接把分代最大年龄修改为16会直接报错。
-XX:MaxTenurningThreshold=16
尾巴参数说明(压缩指针相关)
压缩指针相关的命令(压缩指针是否开启对我们new一个对象是不是16字节的影响)
查看当前JVM运行参数的指令
java -XX:+PrintCommandLineFlags -version
压缩指针默认是开启的
(这也就解释了为什么前面的类型指针是4个字节,节约了内存空间)
假如不压缩的情况?我们手动关闭压缩指针看看?
+是开启,-就是关闭,所以指令是
-XX:-UseCompressedClassPointers
不管是否开启压缩指针,创建一个对象就是16字节的。(开启压缩指针后缺失的会由对齐填充补充)