1、java对象创建时机/方式
(1)使用new关键字来调用一个类的构造函数显式地创建对象
(2)反射机制(Class类的newInstance方法(调用无参构造器)、使用Constructor类的newInstance方法(更强大,可以调用有参数和私有的构造函数))
(3)使用Clone方法(实现Cloneable接口并实现其定义的clone方法)
(4)使用反序列化等方式创建对象(类实现Serializable接口,当我们反序列化一个对象时,JVM会给我们创建一个单独的对象)
2、对象的组成
(1)对象头:markword【哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID】
指向类的指针、
(数组长度)
(2)实例数据:对象的实例数据就是在Java代码中能看到的属性
和他们的属性值
。
(3)对其填充:JVM要求Java的对象占的内存大小应该是8bit的倍数
3、对象创建过程(以new为例)
(1)检查类是否已经被加载。类的加载过程:加载、链接(为静态变量赋默认值)、初始化(执行类的初始化方法clinit,为静态变量赋初始值,clinit方法包含静态变量、静态代码块、按照生命的顺序执行)
(2)为对象分配内存空间(堆中,私有)
有两种分配方式:指针碰撞,虚拟机的内存是地址连续的,带有内存压缩机制来形成连续地址空间。但这种方式在多线程时会出现指针划分不一致的问题。方式二是虚拟机为每个线程分配不同的空间,这个空间称为“本地线程分配缓冲”(TLAB)
(3) 为对象的字段赋默认值(0、null)
(4)设置对象头:对这个将要创建出来的对象,进行信息标记,包括是否为新生代/老年代,对象的哈希码,元数据信息,这些标记存放在对象头信息中。
(5)执行实例的初始化方法init: init方法包含成员变量、构造代码块的初始化,按照声明的顺序执行。
(6)执行构造方法:对象创建成功
上述为无父类的对象创建过程。对于有父类的对象创建过程,还需满足如下条件:
- 先加载父类;再加载本类;
- 先执行父类的实例的初始化方法init(成员变量、构造代码块),父类的构造方法;执行本类的实例的初始化方法init(成员变量、构造代码块),本类的构造方法。
总结对象创建过程
1、先加载类
父类链接:为静态变量赋默认值
父类初始化:为静态变量赋初始值
子类链接:为静态变量赋默认值
子类初始化:为静态变量赋初始值
2、为成员变量赋默认值
父类:为成员变量赋默认值
子类:为成员变量赋默认值
3、父类:对象初始化,为成员变量赋初始值
4、父类:执行构造方法
5、子类:对象初始化,为成员变量赋初始值
6、子类:执行构造方法
4、锁升级过程(记录在markword中)
1)当对象没有锁时,这就是一个普通的对象,MarkWord记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。锁状态为无锁。
2)当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
3)当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,MarkWord中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
4)当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是MarkWord中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把MarkWord里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
5)偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁MarkWord的指针,同时在对象锁MarkWord中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把MarkWord中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
6)轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
7)自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
5、redis实现分布式锁
(1)set key value nx ex seconds 将锁和过期时间编排到一起,它们是原子操作,可以避免死锁
(2)解锁可能出现解的不是自己的,所以在加锁时给锁设置一个标识,当进程解锁时需要进行比对,然后才能释放
(3)为了保证解锁两个步骤的原子性,使用lua脚本
(4)单个主节点没问题了,但如果存在主从情况,进程A在主节点上加锁,但是主宕掉了;将从节点晋升为主,此时进程B给新的主节点加锁。之后原主节点重启成为从节点,这时系统会出现两把锁,违背唯一性原则。建议使用redlock算法。