引子
public class L {
}
package com.vip.poc.backend.vpao.lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
static L l = new L();
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args){
lockTest();
}
/**
* 假设现在lockTest()存在线程安全问题,需要对它进行同步操作,应该怎么做呢?
* (1)synchronized
* (2)reentrantLock
*/
public static void lockTest(){
//juc并发包中的锁
/**
* reentrantLock.lock()的本质就是改变reentrantLock对象中的值,把它从0改成1
* 如果改成功的话就是上锁成功
*/
reentrantLock.lock();
System.out.print("xxxx");
reentrantLock.unlock();
// java内置锁
/**
* 给l上锁,至少有个标识说明给l上锁成功了
* 那么在哪里体现给l加了锁呢?现在这个L对象里面什么都没有,没有变量体现线程给它加了锁
* 所以有个线程通过synchronized关键字给对象加锁了,我们到底改变了这个对象什么东西?
*/
synchronized (l){
System.out.print("xxxx");
}
}
}
上锁就是改变对象的对象头?
什么是对象头?----Java对象的布局----Java对象由什么组成----对象在堆上----把对象new出来之后,对象在堆上到底要分配多大内存?----分配的内存肯定是不固定的,要是固定就不会有内存溢出了
至少要考虑:
- Java对象的实例数据----不固定,定义了多少变量,就有多大。要是对象里面没有任何属性,那么就没有实例数据;如果定义了一个int对象,那么就有4 byte
- 对象头----固定
- 数据对齐----64bit jvm要求一个数据必须是8的整数位byte
public class L {
/**
* 实际分配内存时并不只为这个对象分配1 byte
* 因为64位的虚拟机规定对象大小一定要是8的整数倍
* 所以即使对象只有1 byte,但是在堆上也会为此对象分配8 byte来存放数据
* 其中的7 byte仅仅是为了让数据对齐
*/
boolean flag = false; // 1 byte
}
Java对象的布局以及对象头的布局
1. JOL开分析Java的对象布局
// 添加JOL的依赖
org.openjdk.jol:jol-core:0.9
L.java
public class L {
boolean flag = false;
}
JOLExample.java
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
public class JOLExample {
static L l = new L();
public static void main(String[] args){
// 打印对象布局
System.out.print(ClassLayout.parseInstance(l).toPrintable());
}
}
运行结果:l对象占用的内存是16 bytes
这个对象一共16B,其中对象头(Object header)12B,对象的实例数据为1B,还有3B是对齐的字节(因为在64位虚拟机上的对象大小必须是8的倍数)。
其中实例数据和对齐填充数据有可能有,也有可能没有。但是对象头一定有。那么问题来了?对象头里面的12B到底存的是什么呢?
2. 什么是JVM?什么是HotSpot?
- JVM可以理解为一种规范或者标准,规定了将来你要实现JVM你要怎么做。
- HotSpot可以理解为一个产品/JVM的一种实现。除了HotSpot之外,j9,taobaovm也是JVM的实现。
- openjdk是一个c++的项目/代码,HotSpot就是基于openjdk开发出来的。HotSpot里面80%都是openjdk的代码。
所以就由JVM就规定了对象头长什么样子,由什么组成。无论是HotSpot还是j9,对象头的组成都必须按照JVM的规范。
在openjdk的官方文档(http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html)中,可以看到对象头的定义:
也就是下面这12字节里面包含了很多的信息,比如对象的hashcode(相当于对象地址),同步状态(synchronized加锁)、GC状态(记录对象的年龄)、对象类型(对象属于哪个类,换言之,这12个字节里面保存了类的模板信息)。
【由两个词组成】,词,在计算机原理中就是【字长】的意思。Java中没有词这个概念,我们可以把它看作是属性,也就是【有两部分/两个属性组成】,从文档可以发现这两个部分就是 1. Mark Word 2. klass pointer。
3. Mark Word
HotSpot源代码中有一个markOop.hpp文件,里面就定义了Mark Word的信息。Mark Word占用的内存大小在文件中的注释写得非常清楚。
- 在32位JVM中,前25bit存储hashcode,跟着后面4bit存储GC年龄(age),然后1bit存储偏向锁(biased_lock)信息,再后面2bit存储同步状态(lock),总共占32bit(size:32)
-
在64位JVM中,Mark Word占用内存不一定固定是64bit的,根据对象状态不同,存的东西不一样。
Java的对象头在对象的不同状态下会有不同的表现形式,主要有三种状态:无锁状态(刚刚new出来) 、加锁状态、gc标记状态(方法已经执行完了,对象引用已经没有了,被gc标记 )
那么在不同状态情况下,存储的信息是什么样子的呢?下图进行说明。
其中lock存储的是锁的信息(锁的状态),用2 bit来存储。2 bit最多能存4种状态(00 01 10 11),那怎么表示上述5种状态呢?其实有锁和无锁状态是用biased_lock和lock联合表示的。比如无锁状态下,biased_lock表示为0,lock为10;偏向锁状态下,biased_lock就表示为1,lock还是为10。后面3种状态就直接用00 01 11 表示即可。
public class JOLExample {
static L l = new L();
public static void main(String[] args){
// 打印无状态对象布局
System.out.print(ClassLayout.parseInstance(l).toPrintable());
}
}
输出:
看到这里会不会很疑惑,不是说无状态对象前25bit是unused的吗(也就是应该全部为0),为什么还会有一个1出现呢?而且unused后面不是应该存储hashcode吗,为啥全都是0呢(全都是0,表示没有存储东西)?
hashcode是地址,需要计算。l.hashCode()
就是用来计算hashcode的。hashCode()是一个native方法(底层用c++写的),对象存在哪里只有c++知道。我们把代码改成:
public class JOLExample {
static L l = new L();
public static void main(String[] args){
System.out.print(l.hashCode());
System.out.print(ClassLayout.parseInstance(l).toPrintable());
}
}
输出:此时就有了hashcode
那么前面的8个bit明明是unused,为啥还会有数据呢?这是电脑原因导致的。mac电脑是大端存储(从内存的低地址开始,先存储数据的高序字节再存储数据的低序字节),而本台电脑是小端存储(从内存的低地址开始,先存储数据的低序字节再存高序字)。例如:
十进制数9877(二进制为10011010010101),
用小端存储表示则为:
高地址 <- - - - - - - - 低地址
10010101[高序字节] 00100110[低序字节]
用大端存储表示则为:
高地址 <- - - - - - - - 低地址
00100110[低序字节] 10010101[高序字节]
所以图中的字节是是倒着存放的,00000001表示的是图里面identity_hashcode后面【unused+age+biased_lock+lock】这部分加起来的8bit。
那么我可以理解Java当中的取锁其实可以理解是给对象上锁,也就是改变对象头的状态,如果上锁成功则进入同步代码块。
但是Java当中的锁有分为很多种,从输出结果可以看出大体分为偏向锁(只有一个线程持有这个对象)、轻量锁、重量锁三种锁状态。这三种锁的效率完全不同,关于效率的分析会在下文分析,我们只有合理地设计代码,才能合理地利用锁,那么这三种锁的原理是什么呢?所以我们需要先研究这个对象头。
网友评论