让我们带着问题一探究竟
一个Object对象占用几个字节,里面内容都是什么(对象占用大小)?在哪里分布呢(内存模型)?什么时候会销毁(GC)?
klass是java中,类的元信息在jvm中的表现形式,就是在c++中,类的元信息是通过klass来表示的
对象占用大小
new Object();
会占用多少内存呢?答案是16字节=128比特位=128bits
为什么是16字节呢?而不是17或者15?
这是因为8个字节=1byte,所以只能是8的倍数,那么是8,要么是16、24、32
那为什么是16,而不是24、32?因为这得取决于里面的内容是什么
里面内容是什么呢?
对象头和数据体两大部分
1. 对象头
- 前面8个字节为markword,记录对象被上锁的各种状态或者垃圾回收和hashcode相关的信息
默认无锁的情况如下,有锁的情况参考java锁
- 64位系统中(64bit)
未使用:25位
hashcode: 31位存储对象的System.identityHashCode(),采用延迟加载技术
未使用:1位
gc分带年龄: 4位
偏向锁的标记:1位
当前对象的锁的状态:2位
25+31+1+4+1+2=64 - 32位系统中(也是有64bit,只不过java只用32bit)
hashcode: 25位存储对象的System.identityHashCode(),采用延迟加载技术
gc分带年龄: 4位
偏向锁的标记:1位
当前对象的锁的状态:2位
25+4+1+2 = 32
接下来4个字节(也有可能是8个字节)是Klass Point(类型指针)
为什么有可能是4有可能是8呢,取决于是否开启了classPointer指针压缩
虚拟机通过这个指针来确定这个对象是哪个类的实例 一个对象里面的数据都是紧挨着的,因为是紧挨着,无法区分数据断点在哪里,所以必须有class对象的指针,通过对象起始地址和字段的偏移地址(从class获取)读取字段的数据如果是数组类型的话对象头还得再加4个或8个字节(取决于是否开启压缩指针)表示数组的长度
只有当本对象是一个数组对象时才会有这个部分
2. 对象数据体
对象的里面的数据
如果是基本类型则是对应的值占用的空间,如果是引用类型那么大小有可能是8bit或者4bit取决于是否开启压缩指针
3. 对齐填充数据(可选)
根据对象对齐空间进行对齐,默认为8bit
因为必须是8的倍数,不是8的倍数剩下的将要补齐
所以一个new Object()
为16字节,8字节为对象头,后面8字节为class对象的指针和数据填充
1G内存大约可以存1024 * 1024 * 1024 / 16 = 67108864(约等于6千7百万多个对象)
java锁
java都有哪些锁呢?
是否阻塞
悲观(阻塞其他线程-synchronized)、乐观(不阻塞其他线程-ReentrantLock和ReentrantReadWriteLock)是否公平
非公平(synchronized)、公平(ReentrantLock和ReentrantReadWriteLock)二次是否能获取
不可重入锁(jdk没有自带的)、可重入锁(jdk自带的锁都可以重入)是否共享
共享锁(ReentrantReadWriteLock)、排他锁
大体分为synchronized、ReentrantLock和ReentrantReadWriteLock3个阵营
synchronized
- 特性:阻塞、非公平、可重入、排他锁
- 原理
jdk1.6引入了偏向锁和轻量级锁,1.6之前都是重量级锁,意思就是互斥等待都需要内核态完成(操作系统),开销非常的大,所以优化之后的synchronized可以进行锁升级
每一个Java对象就有一把看不见的锁,存放在对象头中叫markword,占用8个字节(动态的内容)
当创建一个对象时,会通过Klass的prototype_header来初始化该对象的markword,prototype_header包含了lock(锁标识-适用于所有的实例)、epoch(偏向时间戳-用来控制锁是否失效的版本号)
无锁的情况下里面存放默认的数据
偏向锁、轻量级锁、重量级锁都是根据markword里面的数据来标识当前锁的状态(所以markword里面的内容都是动态的),一个对象锁升级之后不可回退
在32位jvm中占用空间如下所示(4*8=32bit)
在64位中占用(8*8=64bit)如下所示
无锁、偏向锁、轻量级锁、重量级锁、gc标记,这5种状态都是通过锁标志位(lock)来判断的,但是2bit只能表示4种状态,如上图|------------------------------------------------------------------------------|--------------------| | Mark Word (64 bits) | State | |------------------------------------------------------------------------------|--------------------| | unused:25 | identity_hashcode:31 |unused:1| age:4 |biased_lock:1| lock:2 | Normal | |------------------------------------------------------------------------------|--------------------| | threadId:54 | epoch:2 |unused:1| age:4 |biased_lock:1| lock:2 | Biased | |------------------------------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | lock:2 | Lightweight Locked | |------------------------------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked | |------------------------------------------------------------------------------|--------------------| | | lock:2 | Marked for GC | |------------------------------------------------------------------------------|--------------------|
所以无锁和偏向锁的锁标志位都为01,然后在通过额外的1bit(biased_lock)用来区分当前对象是否为偏向锁
只有匿名偏向时(lock=01,biased_lock=1,threadId=null)才可以使用偏向锁,否则从轻量级锁开始无锁
锁标志位为lock=01,偏向锁标识为biased_lock=0
对于无锁状态的锁对象,如果有竞争,会直接进入到轻量级锁,所以如果没有关闭偏向锁那么JVM在启动4秒之后(-XX:BiasedLockingStartupDelay=4000),会将所有加载的Klass的prototype_header修改为匿名偏向锁为什么要延迟之后修改呢?
JVM启动时必不可免会有大量sync的操作,而偏向锁竞争时会STW并升级为轻量级锁,锁升级不可回退。如果开启了偏向锁,会发生大量锁撤销和锁升级操作,大大降低JVM启动效率偏向锁
因为偏向锁会占用hashcode空间,如果该对象已被产生hashcode则不能使用偏向锁,如果在偏向时产生hashcode,那么会升级为轻量级锁JVM默认的计算identity hash code的方式得到的是一个随机数,如果不想为随机数可参考, 因而我们必须要保证一个对象的identity hash code只能被底层JVM计算一次
Identity hash code是未被覆写的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值
匿名偏向:锁标志位为lock=01,偏向锁标识为biased_lock=1, 且threadId为空(未偏向任何线程),代表当前为匿名偏向
markword里面有54(64位jvm)或者23(32位jvm)bit表示偏向锁偏向的线程id,未偏向时此值为空,上锁时依赖一次CAS原子指令,设置对应的线程的id,默认情况下已上锁的threadId不会自动释放(减少CAS指令的调用),如果上锁失败或已偏向其他线程,则会进行重偏或竞争升级为轻量级锁
epoch的作用是记录偏向锁的版本号,用来判断偏向锁是否失效(实例的epoch != klass的epoch),epoch默认是有效的,如果失效可以重偏,什么时候会失效呢?参考-XX:BiasedLockingBulkRebiasThreshold=20偏向锁上锁大致过程为:
在匿名偏向状态才可以上锁,CAS设置threadId成功代表上锁成功,失败则会进行锁升级为轻量级锁
如果epoch失效,则会重偏(非重偏的逻辑只要有竞争就会升级为轻量级锁),CAS设置threadId和epoch最新值,失败则会进行锁升级
每次上锁或者重入时只需要检查epoch和threadId是否有效即可,并且会在当前的栈侦添加一条锁记录LockRecord(记录被锁对象的地址和被锁对象的markword),用来计算重入的次数(偏向锁LockRecord的MarkWord为空,称之为Displaced MarkWord),执行完同步代码块之后会销毁LockRecord偏向锁锁升级为轻量级锁的大致流程为:
其他线程请求锁,以被锁对象和新线程作为参数构造一个VM_Operation vo,新线程被挂起,在全局安全点(STW)时,VM_Thread去执行vo
vo内部逻辑是去检查持有偏向锁的线程状态,如果对应线程已经销毁则或者对应线程执行的代码在同步块之外(通过遍历持有锁的线程的栈,判断是否有指向被锁对象的lockRecord),则设置对象为无锁状态(无锁上锁会升级为轻量级锁) ,如果还在同步块之内则把当前偏向锁升级为轻量级锁,然后让新线程以轻量级锁的状态去竞争
至此偏向锁流程解析完毕轻量级锁
锁标志位为lock=00
偏向锁失效(超过-XX:BiasedLockingBulkRevokeThreshold=40默认的次数)或偏向锁竞争时,就会使用轻量级锁
ReentrantLock
特性:非阻塞和阻塞、非公平和公平、可重入、排他锁
ReentrantReadWriteLock
特性:非阻塞和阻塞、非公平和公平、可重入、共享锁