前言
上下文提及到了类的加载过程,详细介绍了加载类的每个阶段:Loading、Linking、Initialize,在其中也说明了静态变量赋值顺序 > 先赋予默认值、在 Initialize 初始化阶段赋予初始值
从类加载到双亲委派:深入解析类加载机制与 ClassLoader
该篇文章会详细实例对象的创建过程、对象如何布局、对象头包括哪些内容以及对象如何定位、分配等
创建对象过程
创建对象的过程如下:
- 创建一个对象:new Obj(),第一步把 class 文件 loading 进内存中
- 第二步:Linking 链接阶段,分为三部分进行:Verification 校验文件是否符合 class 格式(CAFE BABY)、Preparation 将类的静态变量赋默认值、Resolution 将符号引用解析为直接引用
- 第三步:Initialize 初始化阶段将类的静态变量设置初始值,同时执行静态代码块语句
- 申请对象内存,成员变量赋默认值
- 调用构造方法:成员变量按顺序赋初始值、执行构造方法的代码块
创建对象需要预先申请好内存,成员变量再赋默认值,然后再调用构造方法时,先会将成员变量按顺序进行赋予初始值,再后来才调用构造方法 > 第一句先通过 super()
方法调用父类
对象布局
观察虚拟机默认配置:java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=23891840 -XX:MaxHeapSize=382269440 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
作为内存布局的对象分为两种:
1、普通对象
2、数组对象
普通对象
普通对象内部有几个重要的概念组成部分,如下:
- 对象头:包含了 Mark Word 标记字及 Klass Pointer 类型指针,长度为 8 字节
Mark Word 标记字,长度为 8 字节
ClassPointer 指针,开启:-XX:+UseCompressedClassPointers 参数后会压缩为 4 字节,不开启为 8 字节
- 实例数据,开启:-XX:+UseCompressedOops 参数后,开启时引用类型为 4 字节,不开启时为 8 字节
- 填充对齐,Padding 填充必须是 8 的整数倍
数组对象
数组对象内部有几个重要的概念组成部分,如下:
- 对象头:包含了 Mark Word 标记字及 Klass Pointer 类型指针,同普通对象一样
- 数组长度占用为 4 字节
- 数组数据
- 填充对齐,Padding 填充必须是 8 的整数倍
数组对象对比普通对象多了数组长度
如何观察 Object 大小
1、创建一个 Agent 代理类,获取对象大小
import java.lang.instrument.Instrumentation; /** * @author vnjohn * @since 2023/06/26 */ public class ObjectSizeAgent { /** * Java 内部字节码处理调试叫为 Instrumentation(调弦),所以我们在代理装到我们 JVM 时候可以截获这个 Instrumentation */ private static Instrumentation inst; /** * 必须要有 premain 函数参数也是固定的,第二个就是 Instrumentation * 这个是虚拟机调用的它会帮我们初始化 instrumentation,所以调用 getObjectSize 方法才不会空指针 */ public static void premain(String agentArgs,Instrumentation _inst){ inst = _inst; } public static long sizeOf(Object o){ return inst.getObjectSize(o); } }
2、在 META-INF 目录下,创建 MANIFEST.MF 文件,设置版本、主函数调用前要执行的方法,如下:
Manifest-Version: 1.0 Created-By: vnjohn Premain-Class: com.vnjohn.jvm.agent.ObjectSizeAgent
3、在 maven build 节点下引入自定义的 MANIFEST 配置,如下:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifestFile>src/META-INF/MANIFEST.MF</manifestFile> </archive> </configuration> </plugin> </plugins> </build>
4、将其打包成 jar 包,再其他存在主函数的项目中引入该依赖,如下:
<dependency> <groupId>org.vnjohn</groupId> <artifactId>object-size-agent</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency>
5、编写测试类代码(可自行调整你想知道某个类的大小)先调整启动参数,如下:
-javaagent:/Users/vnjohn/repository/org/vnjohn/object-size-agent/1.0.0-SNAPSHOT/object-size-agent-1.0.0-SNAPSHOT.jar
/** * @author vnjohn * @since 2023/6/26 */ public class 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 OrdinaryObject())); } // 一个 Object 占多少个字节 // Oops = ordinary object pointers 普通对象指针 private static class OrdinaryObject { // 8 _markword // 4 _class pointer int id; // 4 String name; // 4 int age; // 4 byte b1; // 1 byte b2; // 1 Object o; // 4 byte b3; // 1 } }
具体更多的源码及介绍,可以阅览 GitHub 仓库:object-size-agent.git
当我们要评估临时扩容方案时,都需要计算好对应实体所具体要占用的大小,以此来作为测试基准,做规划来定下最终要扩容的服务器大小及数量!!!
对象头组成部分
对象头组成部分,可以看 HotSpot 内核源码中 Mark Word 结构,在 markOop.hpp 文件中,截取一部分源码的注释部分,如下:
// 32 bits: // -------- // hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object) // size:32 ------------------------------------------>| (CMS free block) // PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object) // // 64 bits: // -------- // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) // PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object) // size:64 ----------------------------------------------------->| (CMS free block) // // unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object) // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object) // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object) // unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
以 64 bits 位为例,如下表:
为什么 GC 年龄默认设置为 15?
当我们使用 PS 垃圾回收器时,它默认的从年轻代升到老年代的年龄为 15,因为只有 4 bit 来表示分代年龄,最大二进制为:1111=8+4+2+1=15,所以说最大年龄也就只有 15
对象如何定位
当 new 出来一个对象,比如:new OrdinaryObject(),那么这个对象是如何被定位到的呢?
创建对象自然是为了后续使用该对象,在我们 Java 程序会通过栈上的 reference 数据来操作堆上的具体对象;由于 reference 类型在《Java 虚拟机规范》里面只是规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位;对象具体使用什么定位方式是由虚拟机实现而决定的,主流的对象定位方式主要有两种:句柄、直接指针
句柄:若使用句柄访问的话,Java 堆中将会划分一块内存来作为句柄池,reference 存储的就是对象的句柄地址,而句柄中包含了对象实例数据、类型数据各自的地址信息
直接指针:若使用直接指针访问的话,Java 堆中对象的内存布局就必须考虑如何放置类型数据的相关信息,reference 中直接存储的就是对象地址,若只是访问对象本身而不是对象类型数据的话,那么就不需要多一次的间接访问开销
这两种对象定位方式各有优劣,如下:
- 使用句柄来访问的最大好处就是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改
GC 回收时效率会比较高
- 使用直接指针来访问的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在 Java 中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,主要的虚拟机 HotSpot 就采用了这种方式进行对象访问
对象如何分配
图解过程,如下:
- 当 new 一个对象时,先往栈上分配,若栈上能分配就分配在栈上,当栈上弹出后对象就没了
作用于方法作用域下的对象,非本地缓存
- 若栈上无法分配,判断对象是否足够大,对象特别大时直接分配到堆中的老年代中
- 若对象不足够大,优先进行线程本地分配(TLAB:Thread Local Allocation Buffer)线程本地缓冲区能分配下就进行分配
- 线程本地缓冲区分配不下就分配到伊甸区,然后进行 GC 回收过程
- GC 分代年龄到了(最大为 15)直接进入到老年代,若年龄不到将一直在年轻代中 YGC 来 YGC 去,直接对象被回收或者年龄足够满足到达老年代
以上的过程就是所谓的对象分配
总结
该篇博文讲解了创建对象过程的几个核心步骤,剖析了对象内部是如何布局 > 普通对象、数组对象,通过了一个简单的案例来如何统计一个 Object 所占的字节大小,对象头组成部分:Mark Word、Class Pointer,介绍了对象定位的两种方式:句柄池、直接指针,最后,简要说明了对象如何分配的过程;希望你能喜欢,帮助到你是莫过于最开心的事了!
博文放在 Java 专栏里,欢迎订阅,会持续更新!
如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!
推荐专栏:Spring、MySQL,订阅一波不再迷路
大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!