Loading... # java对象的内存布局 ## 对象的创建过程 1. 类加载 1. loading 双亲委派 2. linking 1. Verification - 验证文件是否符合jvm规范 2. Preparation - 静态成员变量赋默认值 3. Resolution - 将类、方法、属性等符号引用解析为直接引用 常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用 3. Initializing - 调用类初始化代码 <clinit>,给静态成员变量赋初始值 2. 对象创建 1. 申请对象内存 2. 成员变量赋默认值 3. 调用构造方法<init> 1. 成员变量顺序赋初始值 2. 执行构造方法语句 ## 对象在内存中的存储布局 ### 普通对象 1. 对象头: markword,8字节 2. ClassPointer指针: -XX:+UseCompressedClassPointers 为4字节 不开启为8字节 3. 实例数据 - 引用类型保存的是指针,开启-XX:+UseCompressedOops 为4字节 不开启为8字节 4. Padding对齐,将对象大小补全成8的倍数 ### 数组对象 1. 对象头: markword,8字节 2. ClassPointer指针: 同上 3. 数组长度: 4字节 4. 数组数据 5. Padding对齐 ## 对象头具体包括什么 ![markword内容.jpg][1] 如果没有重写hashcode方法,那么默认调用os::random产生hashcode,可以通过System.IdentityHashCode获取,一旦生成,jvm会将其记录在markword中。也就是如果一个对象计算过IdentityHashCode之后,前25位被hashcode填充,将无法进入偏向锁状态。(偏向锁需要线程ID和Epoch) ## 对象占用的字节 `Object o = new Object();`在内存中占多少字节。 首先,markword占用8字节,ClassPointer由于默认开启了指针压缩(`java -XX:+PrintCommandLineFlags -version`查看默认添加的选项),占用4字节,加起来是12字节,然后Padding对齐,是16字节。 ### 验证 1. 新建项目ObjectSize 2. 新建ObjectSizeAgent类 ``` public class ObjectSizeAgent { //调琴 private static Instrumentation inst; public static void premain(String agentArgs, Instrumentation _inst) { inst = _inst; } public static long sizeOf(Object o) { return inst.getObjectSize(o); } } ``` 3. src目录下创建META-INF/MANIFEST.MF ``` Premain-Class: tk.princeleiblog.agent.ObjectSizeAgent ``` 指向ObjectSizeAgent的全路径 4. 打包成jar文件 5. 在需要的项目中引入该jar包 6. 运行时指定Agent使用的jar包 ``` -javaagent:E:\ObjectSize\target\ObjectSize-1.0-SNAPSHOT.jar ``` 7. 写测试demo ``` public class T03_SizeOfAnObject { public static void main(String[] args) { System.out.println(ObjectSizeAgent.sizeOf(new Object())); System.out.println(ObjectSizeAgent.sizeOf(new int[] {})); System.out.println(ObjectSizeAgent.sizeOf(new P())); } private static class P { //8 _markword //4 _classpointer int id; //4 String name; //4 int age; //4 byte b1; //1 byte b2; //1 Object o; //4 byte b3; //1 } } ``` 输出 ``` 16 16 32 ``` 默认开启了-XX:+UseCompressedClassPointers与-XX:+UseCompressedOops,前者为ClassPointer指针压缩,后者为实例中引用型变量的指针压缩,比如代码中P类中的String类型,压缩之后为4字节,不压缩为8字节。可以手动指定-XX:-UseCompressedClassPointers与-XX:-UseCompressedOops来关闭指针压缩,测试结果。 **结果分析** `new Object()`之所以是16上边已经说过了。 `new int[] {}`,markword为8,ClassPointer为4,数组长度为4,总和是16,不用Padding对齐。 `new P()`,markword为8,ClassPointer为4,int为4,String为4(开启了UseCompressedOops),int为4,byte为1,byte为1,Object同String为4,byte为1,相加为31,Padding对齐为32。 ## Hotspot开启内存压缩的规则(64位机) - 4g以下直接砍掉高32位 - 4g-32g,默认开启内存压缩ClassPointers Oops - 32g及以上,压缩无效,使用64位 内存并不是越大越好 ## 内存分配的两种方式 **指针碰撞与空闲列表** 选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的 ![内存分配的两种方式.png][2] ### 内存分配的并发问题 在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全: - **CAS+失败重试**: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。 - **TLAB**: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配 ## 对象定位 - 句柄池 变量不直接指向对象,而是指向句柄池,句柄池包含该对象的引用和该类的Class对象的引用,通过句柄池间接指向对象。 - 直接指针 变量直接指向对象,对象中包含该类的Class对象的引用。Hotspot使用该种方式。 [1]: https://www.princelei.club/usr/uploads/2021/04/2620956220.jpg [2]: https://www.princelei.club/usr/uploads/2020/11/2540572554.png Last modification:April 21st, 2021 at 06:24 pm © 允许规范转载