Java中创建对象的六个步骤 细分后(new关键字)对象头详细介绍
要看的懂对象的创建过程,首先你得有对Java虚拟机和Java基础以及JUC很是熟悉,比如类的加载过程,CAS、多线程、JVM的GC等等
首先好看一个图,我大概会根据图中的内容来分享这六个步骤(其实几个步骤不重要,只要包括的内容都在就行):
一、以下是创建对象的六个步骤:
1、判断是否能在常量池中能找到类符号引用,并检查是否已被加载、解析、初始化(即判断类元信息是否存在)
如果没有则在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 + 类名为Key进行查找对应的.class文件以此来完成类的加载进内存。在这个过程中,要是找不到要加载的类JVM就会抛出ClassNotFoundException异常,意思就是找不到该类进行加载。
2、为对象分配内存空间
首先计算对象占用空间大小,接着在堆中划分一块内存给新对象,如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节。文章结束附有Java八种基本数据类型的所占字节数,分配内存时根据堆中内存是否规则分为:
① 内存规整—指针碰撞:如果是规整的,那么虚拟机将采用指针碰撞法来为对象分配内存。
意思就是所有用过的内存在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那一边挪动一段与对象大小相等的距离,以此来分配内存给对象。如果垃圾收集器选择的是Serial、ParNew这种基于标记压缩算法的,也就是一般使用带有compact(整理)过程的收集器时,就会使用指针碰撞分配方法。
② 内存不规整—空闲列表:如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用这种空闲列表法给对象分配内存。
意思就是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表找到一块足够大的空间划分给对象实例,这种分配方式就是空闲列表(Free List)。
3、处理并发问题
为什么会有这个阶段呢?就是我们都知道堆空间是多个线程共享的,那么多个线程同时去操作堆空间数据时就会存在并发线程安全问题。
两种方式去解决这种并发安全问题:
① 采用CAS失败重试、区域加锁更新的原子性。
CAS算法是这样的:CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包(Java.Util.Concurrent)中的很多类都使用了CAS技术。
CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
大白话:假如A线程要把共享变量sum的值从1修改为2,那么这时候A线程会先通过地址去获取一次sum的初始值1作为预期值,在修改之前会把之前获取的预期值与当前sum的值进行一次比较,假如两个值相同,那就将1修改为2,如果在A线程修改之前,B线程进入把sum的值提前修改为0了,那么A线程在用预期值去与当前的sum值0比较,显然两个值不相等,此时的A线程的操作就会停止。
② 每个线程预先分配一块TLAB—通过-XX:+ /-UseTLAB参数来设定
TLAB这一块区域是在堆当中Eden(伊甸园)区中的,大概占整个Eden区的1%左右,很小的一块区域,堆为每一个线程单独分配一块TLAB,以此来保证多线程共享数据时是线程安全的。
4、初始化分配到的空间(对属性初始化,零值初始化)
这一过程说白了就是:所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用。基本数据类型默认初始化的值在文章下方表格里。
5、设置对象的对象头
将对象的所属类(即类的元数据信息)、对象的HashCode、对象的GC信息、锁信息等数据存储在对象头中。这个过程的具体设置方式取决于JVM实现。
6、执行init方法进行初始化
可以简单理解成调用构造函数进行显示的赋值,代码中和构造器中初始化。在Java程序的视角看来,初始化才正式开始,初始化成员变量,显示赋值,执行实例化代码块,调用类的构造器,并把堆内对象的首地址赋值给引用变量。
一般来说(由字节码中是否跟随有invokspecial 指令所决定), new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。
扩展:还有一个clinit方法,这个方法是加载时对类中static静态修饰的属性和静态代码块进行初始化(此过程是在链接阶段的准备阶段)。这是JVM相关的知识了。
二、对象头的内部结构
1、创建了Customer()实例 和 Account()实例
2、对象头里包括:运行时元数据、类型指针、实例数据、对齐填充
① 运行时元数据里又包括:哈希值(HashCode)、GC分代年龄、锁状态标志
哈希值:它是一个地址,用于栈对堆空间中对象的引用指向,不然栈是无法找到堆中对象的
GC分代年龄:记录幸存者区对象被GC之后的年龄age,,一般age为15之后下一次GC就会直接进入老年代
锁状态标志:记录一些加锁的信息
② 类型指针:是对方法区中类元信息的引用
③ 实例数据:真实记录一个对象包含的数据,比如说一个person对象,里面可能包含年龄、性别、身高等等
其中数据为字符串的,要引用到字符串常量池。
想知道jdk8字符串常量池到底在那个地方存储的可以看这里。
④ 对其填充:这一区域就相当于一个快递在邮寄过程中,访问物品损坏,塞的一层填充物,起到一个缓冲的作用。
另外这个过程还会涉及到逃逸分析。我写了一篇逃逸分析的具体作用,感兴趣可以看看。
基本数据类型 | 所占字节数 | 默认值 | 包装类名称 |
---|---|---|---|
byte | 1(1字节=8位) | 0 | Byte |
short | 2 | 0 | Short |
int | 4 | 0 | Integer |
long | 8 | 0L | Long |
double | 8 | 0.0d | Double |
float | 4 | 0.0f | Float |
char | 2 | ‘\u0000’ | Character |
boolean | 1 | false | Boolean |
怎样快速记住这些类型的方法,请看这里。
有用点个关注,手留余香!