使用synchronized关键字,是锁对象还是锁代码块呢?
在hotSpot虚拟机中,对象在内存中的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
而实际上,对象在堆上分配的内存为8的整数倍,若对象头
和实例数据
大小不是8的整数倍时,才需要对齐填充
。
如何查看对象布局
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
public static void main(String[] args) {
Account account=new Account();
System.out.println(ClassLayout.parseInstance(account).toPrintable());
}
2.1 对齐填充是否一定存在?
1. object对象:
private static void testObj() {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
打印出的对象布局:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
对象头占用12字节,对齐填充占用4字节(将其补充为8的整数倍);
2. 自定义对象:
public class Account {
//只有一个boolean的属性,即占1byte。
boolean flag=false;
}
打印出的对象布局:
com.tellme.lock.Account object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 21 f3 27 (01000011 00100001 11110011 00100111) (670245187)
12 1 boolean Account.flag false
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
对象头占用12字节,实例数据占用1字节,对齐填充占用3字节(将其补充为8的整数倍);
3. 对齐填充不一定存在
public class Account {
//若是int类型,那么实例数据占用的是4字节
int flag=0;
}
打印对象布局:
com.tellme.lock.Account object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 e1 e2 27 (01000011 11100001 11100010 00100111) (669180227)
12 4 int Account.flag 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
可以看到,此时对象的布局为对象头
和实例数据
。并不存在填充数据。
4. 数组对象
private static void testObjArr() {
Object[] arr = new Object[100];
arr[0] = "aaa";
System.out.println(ClassLayout.parseInstance(arr).toPrintable());
}
打印出的对象布局:
[Ljava.lang.Object; object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 88 3e 01 00 (10001000 00111110 00000001 00000000) (81544)
12 4 (object header) 64 00 00 00 (01100100 00000000 00000000 00000000) (100)
16 400 java.lang.Object Object;.<elements> N/A
Instance size: 416 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
其中对象头占用16字节(包含markword=8,c=klass pointer=4,数组length=4),数组中的实际数据占用400(4*100)字节。
2.2 Java对象头
有上面数据可以看出,对象头大小为12byte,也就是96bit。
那么对象头又是由什么组成的呢?
点击JDK官网文档了解详情...
可以看到,对象头由两部分组成:
1. mark word:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、同步状态等。
2. klass pointer:对象指向它的类元数据的指针,虚拟机可以通过这个指针来确定这个对象是哪个类的实例(数组、对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通JAVA对象的元数据信息确定JAVA对象的大小,但是从数组的元数据中无法确定数组的大小)。
引申出一道面试题:new Object[100]占用的内存大小:416字节。
2.3 markword的详细结构
markword占用8byte,换算为bit则是为32bit:
在mark word中分代年龄
占了4bit,即分代年龄
值为0-15
在JVM调优中:
-
-XX:MaxTenuringThreshold
默认15。即理论上每发生一次GC,对象的分代年龄+1; - GC标记的锁标志位为11,是GC的
markSweep
(标记清除算法)使用的。
在并发编程中:
synchronize
关键字对对象加锁,实际上是锁的对象,而并非对代码块进行加锁。而锁对象正是通过mark word
的同步标识来实现的。具体到上图中就是通过是否偏向锁+锁标识字段
来控制对象的五种标识的。
注:age:保存对象的分代年龄||biased_lock:偏向锁标识位||lock:锁状态标识位||epoch:保存偏向时间戳。
不管是32/64位的JVM,都是1bit偏向锁+2bit锁标识位,来标识对象状态的。
相关阅读
JAVA并发(1)—java对象布局
JAVA并发(2)—sync关键字和Monitor管程的关系
JAVA并发(3)—线程运行时发生GC,会回收ThreadLocal弱引用的key吗?
JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!
JAVA并发(5)— 多线程顺序的打印出A,B,C(线程间的协作)
JAVA并发(6)— AQS源码解析(独占锁-加锁过程)
JAVA并发(7)—AQS源码解析(独占锁-解锁过程)
JAVA并发(8)—AQS公平锁为什么会比非公平锁效率低(源码分析)
JAVA并发(9)— 共享锁的获取与释放
JAVA并发(10)—interrupt唤醒挂起线程
JAVA并发(11)—AQS源码Condition阻塞和唤醒
JAVA并发(12)— Lock实现生产者消费者