java对象的内存布局
当我们通过new创建了一个对象以后,会将这个对象实例化放入内存中,再给它一个内存地址。
Java对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
实例数据
实例数据部分是对象真正存储的有效信息,也既是我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的都需要记录下来。
对齐填充
该部分不是必要的,是为了实现字节对齐而存在的,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。对象头正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
为什么要如此规定?
看了很多,找到了个比较满意的回答:
- 牺牲一部分空间来换取更快地访问。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。不利于高效执行,所以这么做是为了计算机高效寻址。
- 最小分配单元的大小越大,垃圾回收就越简单(并加快)。
- 还有个没看明白,不写上了。
对象头
对象头由两部分组成:mark word 和klass pointer。(还有个可能有的数组长度length field)
Mark word:即标记字段。用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
klass pointer:类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
length field:如果对象是一个数组,那在对象头中还必须有一块数据用于记录数组长度。 因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。
互斥锁的实现
synchronize最开始的互斥锁的实现是基于Monitor来实现的。
Monitor是基于C++的ObjectMonitor类实现的,其主要成员包括:
- _owner:指向持有ObjectMonitor对象的线程
- _WaitSet:存放处于wait状态的线程队列,即调用wait()方法的线程
- _EntryList:存放处于等待锁block状态的线程队列
- _count:约为_WaitSet 和 _EntryList 的节点数之和
- _cxq: 多个线程争抢锁,会先存入这个单向链表
- _recursions: 记录重入次数
(1)当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列中。
(2)当某个线程获取到对象的Monitor后进入临界区域,并把Monitor中的 _owner 变量设置为当前线程,同时Monitor中的计数器 _count 加1。即获得对象锁。
(3)若持有Monitor的线程调用 wait() 方法,将释放当前持有的Monitor,_owner变量恢复为null,_count自减1,同时该线程进入 _WaitSet 集合中等待被唤醒。
(4)在_WaitSet 集合中的线程会被再次放到_EntryList 队列中,重新竞争获取锁。
(5)若当前线程执行完毕也将释放Monitor并复位变量的值,以便其他线程进入获取锁。
所以monitor对象存在于每个Java对象的对象头中(存储的是指针),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。
在早期java的synchronize属于重量级锁,因为需要调用操作系统的Mutex Lock来实现的,这种操作会导致从用户态到核心态的切换,所以早期的synchronized效率低的原因。
Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。这就使synchronize有了锁升级的过程。
锁升级具体的过程为:无锁——>偏向锁——>轻量级锁————>重量级锁。
重量级锁的实现就是上面所说的内容,下面来说一说偏向锁和轻量级锁。
偏向锁
偏向锁就是偏向于获得该锁对象线程的锁,研究发现大部分在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。所以,偏向锁的引入就是为了提升效率。
如何提升?
从前面我们知道synchronize的重量级锁需要进行内核态和用户态的切换,轻量级锁是用自旋锁来实现的,所以自旋锁的执行过程需要很多次cas操作才行。而偏向锁的实现中只有在切换持有的线程id时才会发生一次cas切换,比自旋锁还能节省资源。
从上面看到,偏向锁和无锁的对应的锁标志位都是01,是否偏向的位置处无锁是0,偏向锁是1。
执行的流程:
- 线程判断当前是否是无锁状态,如果是则将是否是偏向锁位置置为1,记录自己的线程id到对象头的对应位置。
- 如果不是,则查看偏向锁中记录的id是否是本线程,如果是则直接进入执行临界区。
- 如果不是,则尝试修改偏向锁的id,成功则执行临界区
- 如果失败,则说明当临界区正在被执行,需要进行锁升级。
轻量级锁
轻量级锁的实现基于LockRecord实现的,会在获取锁的线程的栈上显式或者隐式分配一个LockRecord空间,这个空间分为两部分:
- displaced mark word:用于存储锁对象目前的Mark Word的拷贝
- owner:指向当前的锁对象的指针
当尝试获取锁的时候会将锁对象的markword复制到自己的displaced mark word然后线程尝试用CAS将锁的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。当未取到锁的线程自旋一定次数没有获得锁就要升级成重量级锁。