对象的内存布局
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
- Mark Word(标记字段):其内容是一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。
- Class Pointer(Class对象指针):Class对象指针的大小是4个字节,其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址
- 对象实际数据:这里面包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节。(如果有的话)
- 对齐填充:最后一部分是对齐填充的字节,使得总内存为8的倍数。
JVM(hotspot 64位)对象头内部组成
1. 图解
2. 偏向锁、轻量级锁和重量级锁在对象头中的标识:其中2bit的锁标志位表示锁的状态,1bit的偏向锁标志位表示是否偏向。
1.无锁状态
2.偏向锁状态
3.打印对象
typescript
public class ClassLayoutDemo {
public static void main(String[] args) {
//构建了一个对象实例
ClassLayoutDemo classLayoutDemo = new ClassLayoutDemo();
System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
}
}
引入依赖:
<!--classLayout:帮助打印对象的布局-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
打印结果:
python
com.gupao.gupaoedu.example.ClassLayoutDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
//Mark Word
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)
//Klass Pointer
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
一共是16个字节,其中对象头是12个字节,还有4个对齐字节(因为64位虚拟机规定:对象的大小必须是8的倍数),由于这个对象里没有任何属性和方法,所以对象的实例数据为0个字节,如果添加一个boolean字段
python
com.gupao.gupaoedu.example.ClassLayoutDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
//Mark Word
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)
//Klass Pointer
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 1 boolean ClassLayoutDemo.happy
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
从上面的输出来看,我们很容易发现,整个对象的大小没有改变,依然是16个字节,其中对象头12个字节,boolean字段 happy 占1个字节,剩下的3个Byte是对齐字节。由此我们可以发现一个对象的布局可以粗略的分为3个部分:对象头(Object Header),对象的实例数据(Instance Data)和 对齐字节(Padding),也叫对齐填充。
各种数据类型大小:
对象类型 | 字节 |
---|---|
boolean | 1 |
byte | 1 |
short | 2 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
引用类型 | 开启指针压缩为4,不开启为8 |
普通对象头 | 开启指针压缩为12,不开启为8 |
数据对象头 | 开启指针压缩为16,不开启为24 |
指针压缩
JVM最初是32位的,随着64位系统的兴起,JVM也迎来了从32位到64位的转换,32位的JVM对比64位的内存容量比较有限。但是使用64位虚拟机的同时,带来一个问题,64位下的JVM中的对象指针占用内存会比32位的多1.5倍(这是因为对象指针在64位架构下,长度会翻倍(更宽的寻址),对于那些将要从32位平台移植到64位的应用来说,平白无辜多了1/2的内存占用),这是我们不希望看到的。于是在JDK1.6时,引入了指针压缩。
JVM参数
ruby
*-XX:+UseCompressedClassPointers 参数:**启用类指针(类元数据的指针)压缩。
**-XX:+UseCompressedOops 参数:**启用普通对象指针压缩。Oops缩写于:ordinary object pointers
在Jdk1.8中默认开启,可用命令进行检测:
ruby
java -XX:+PrintCommandLineFlags -version
结果:
ruby
wangwang@localhost ~ % java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_261"
Java(TM) SE Runtime Environment (build 1.8.0_261-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.261-b12, mixed mode)
wangwang@localhost ~ %
参数中的+号代表开启参数,-号代表关闭参数。
测试
csharp
public class ClassLayoutDemo {
// -XX:-UseCompressedClassPointers -XX:-UseCompressedOops -XX:+PrintCommandLineFlags
char ch = 'c';
public static void main(String[] args) {
//构建了一个对象实例
ClassLayoutDemo classLayoutDemo = new ClassLayoutDemo();
System.out.println("-----------------");
System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
}
}
开启指针压缩(默认)
python
om.gupao.gupaoedu.example.ClassLayoutDemo 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 2 char ClassLayoutDemo.ch c
14 2 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total
未开启指针压缩
python
com.gupao.gupaoedu.example.ClassLayoutDemo 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) 80 c0 35 10 (10000000 11000000 00110101 00010000) (271958144)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
16 2 char ClassLayoutDemo.ch c
18 6 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 6 bytes external = 6 bytes total
锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程调用,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然无须再进行。
比如下面一段代码:
typescript
public static String concatString(String str1, String str2, String str3) {
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2).append(str3);
return sb.toString();
}
大家都熟知StringBuffer是一个线程安全的字符串拼接类,它的每个方法都加了synchronized关键字,每个方法都需要获取锁才能执行,锁对象就是StringBuffer的实例化对象。上述代码中,锁对象就是sb实例对象,经过虚拟机的逃逸分析后会发现sb对象的作用域仅仅被局限在concatString方法内部,根本不会被外部方法使用或调用。因此,其他线程完全没有机会访问到它,也不会产生资源竞争的同步问题。在解释执行时,这里仍然会加锁,在经过服务端编译器的即时编译后(因为逃逸分析是属于即时编译器的优化技术),这段代码就会忽略所有的同步措施而直接执行。
锁升级
上代码:
csharp
import org.openjdk.jol.info.ClassLayout;
public class JOLDemo3 {
static A a;
public static void main(String[] args) {
a = new A();
System.out.println("before lock");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
sync();
System.out.println("after lock");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
public static void sync(){
synchronized (a){
System.out.println("locking");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
打印:
vbnet
before lock
org.jlfang.concurrency.lock.A 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 1 boolean A.happy true
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
locking
org.jlfang.concurrency.lock.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 80 f5 6c 03 (10000000 11110101 01101100 00000011) (57472384)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 1 boolean A.happy true
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
after lock
org.jlfang.concurrency.lock.A 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 1 boolean A.happy true
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
Process finished with exit code 0
打印出来的结果表明这个对象再上锁过程中(locking)变成了轻量级锁(00)的状态,这显然不符合我们的预期。提到过在有且仅有1个线程获取synchronized关键字中的对象时,该对象的锁状态为偏向锁。那么为什么会这样呢?因为JVM的偏向锁有延迟启动机制,每个对象并不会在一开始就获得偏向锁,需要在程序运行后大概4-5秒之后才会获得。同样,让我们通过demo来验证一下
相同的代码,我们只是加了5秒的睡眠时间(避免偏向锁启动延迟的方法,还有更加直接的办法,我们可以在启动时添加下列参数取消偏向锁启动延迟:
ruby
-XX:+UseBiasedLocking
-XX:BiasedLockingStartupDelay=0
我们在代码中添加:
arduino
public static void main(String[] args) throws InterruptedException {
//睡眠5秒,保证偏向锁启动
TimeUnit.SECONDS.sleep(5);
a = new A();
....
打印:
vbnet
before lock
org.jlfang.concurrency.lock.A 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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 1 boolean A.happy true
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
locking
org.jlfang.concurrency.lock.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 38 97 02 (00000101 00111000 10010111 00000010) (43464709)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 1 boolean A.happy true
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
after lock
org.jlfang.concurrency.lock.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 38 97 02 (00000101 00111000 10010111 00000010) (43464709)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 1 boolean A.happy true
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
Process finished with exit code 0
终于,我们看到了偏向锁状态101,需要注意的是,和轻量级锁和重量级锁每次退出同步将变成无锁状态不同,当偏向锁退出同步状态后,该对象依然保持了偏向状态。
锁升级整体流程
- 当对象初始化后,还未有任何线程来竞争,此时为无锁状态。其中锁标志位为01,偏向锁标志位为0
- 当有一个线程来竞争锁,锁对象第一次被线程获取时,锁标志位依然为01,偏向锁标志位会被置为1,此时锁进入偏向模式。同时,使用CAS操作将此获取锁对象的线程ID设置到锁对象的Mark Word中,持有偏向锁,下次再可直接进入。
- 此时,线程B尝试获取锁,发现锁处于偏向模式,但Mark Word中存储的不是本线程ID。那么线程B使用CAS操作尝试获取锁,这时锁是有可能获取成功的,因为上一个持有偏向锁的线程不会主动释放偏向锁。如果线程B获取锁成功,则会将Mark Word中的线程ID设置为本线程的ID。但若线程B获取锁失败,则会执行下述操作。
- 偏向锁抢占失败,表明锁对象存在竞争,则会先撤销偏向模式,偏向锁标志位重新被置为0,准备升级轻量级锁。首先将在当前线程的帧栈中开辟一块锁记录空间(Lock Record),用于存储锁对象当前的Mark Word拷贝。然后,使用CAS操作尝试把锁对象的Mark Word更新为指向帧栈中Lock Record的指针,CAS操作成功,则代表获取到锁,同时将锁标志位设置为00,进入轻量级锁模式。若CAS操作失败,则进入下述操作。
- 刚一出现CAS竞争轻量级锁失败时,不会立即膨胀为重量级锁,而是采用自旋的方式,自旋锁在JDK1.4.2中引入。不断重试,尝试抢锁。默认自旋10次。如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。JDK1.6中,默认开启自旋,可通过下面参数更改自旋次数。JDK1.6对于只能指定固定次数的自旋进行了优化,采用了自适应的自旋,重试机制更加智能。
diff
-XX:PreBlockSpin
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,
进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,
直接阻塞线程,避免浪费处理器资源
- 只有通过自旋依然获取不到锁的情况,表明锁竞争较为激烈,不再适合额外的CAS操作消耗CPU资源,则直接膨胀为重量级锁,锁标志位设置为10。在此状态下,所有等待锁的线程都必须进入阻塞状态。
hashcode与锁
32位,Mark Word的存储结构如下:
64位下,Mark Word的存储结构如下:
由此可知,在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法(非用户自定义)第一次被调用时,JVM会生成对应的identity hash code值,并将该值存储到Mark Word中。后续如果该对象的hashCode()方法再次被调用则不会再通过JVM进行计算得到,而是直接从Mark Word中获取。只有这样才能保证多次获取到的identity hash code的值是相同的(以jdk8为例,JVM默认的计算identity hash code的方式得到的是一个随机数,因而我们必须要保证一个对象的identity hash code只能被底层JVM计算一次)。
我们还知道,对于轻量级锁,获取锁的线程栈帧中有锁记录(Lock Record)空间,用于存储Mark Word的拷贝,官方称之为Displaced Mark Word,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存;对于重量级锁,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中也可以存储identity hash code的值,所以重量级锁也可以和identity hash code共存。
对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法已经被调用过一次之后,这个对象还能被设置偏向锁么?答案是不能。因为如果可以的话,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。因此偏向锁不能与hashcode共存
HotSpot VM的锁实现机制是:
- 当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;
- 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;
- 轻量级锁的实现中,会通过线程栈帧的锁记录存储Displaced Mark Word;重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。
偏向锁、轻量级锁和重量级锁的性能对比
为什么要这么关注synchronized关键字维护的到底时哪种锁呢?各个锁之间的差异很大吗?用demo来测试一下。
我们调用同步方法10亿次,来计算10次++操作所耗的时间:
首先是偏向锁,特别注意启动时要加上参数,否则启动的将是轻量级锁
ruby
开启偏向锁:-XX:+UseBiasedLocking ‐XX:BiasedLockingStartupDelay=20000
关闭偏向锁:-XX:+UseBiasedLocking ‐XX:BiasedLockingStartupDelay=0
//为了测试,也可以设置睡眠时间,让偏向锁延时启动
代码:
arduino
public class JOLDemo5 {
public static void main(String[] args) throws Exception {
//睡眠5秒,保证偏向锁启动
//TimeUnit.SECONDS.sleep(5);
A a = new A();
System.out.println(ClassLayout.parseInstance(a).toPrintable());
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000L; i++) {
a.parse();
}
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
public class A {
int i;
public synchronized void parse(){
i++;
}
}
偏向锁结果:
python
com.gupao.gupaoedu.example.mine.entity.A 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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int A.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
218ms
com.gupao.gupaoedu.example.mine.entity.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 00 76 (00000101 10010000 00000000 01110110) (1979748357)
4 4 (object header) e5 7f 00 00 (11100101 01111111 00000000 00000000) (32741)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int A.i 100000000
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
可以看到锁标志为偏向锁:101
再看轻量级锁的结果,我们只需要把偏向锁延迟启动加上就能模拟出来轻量锁的效果。
轻量级锁结果:
python
com.gupao.gupaoedu.example.mine.entity.A 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int A.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
1802ms
com.gupao.gupaoedu.example.mine.entity.A 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int A.i 100000000
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
我们可以看到锁标志位001(虽然标志仍未偏向锁,是由于main线程有一些其他的线程)
重量级锁:
arduino
public class JOLDemo6 {
static CountDownLatch countDownLatch = new CountDownLatch(10000000);
public static void main(String[] args) throws InterruptedException {
final A a = new A();
long start = System.currentTimeMillis();
for(int i=0;i<2;i++){
new Thread(()->{
while(countDownLatch.getCount()>0){
a.parse();
}
}).start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
}
}
重量级锁结果:
python
com.gupao.gupaoedu.example.mine.entity.A 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int A.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
6653ms
com.gupao.gupaoedu.example.mine.entity.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) fa db 81 b9 (11111010 11011011 10000001 10111001) (-1182671878)
4 4 (object header) e5 7f 00 00 (11100101 01111111 00000000 00000000) (32741)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int A.i 100000001
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
比较结果:
锁 | 偏向锁 | 轻量级锁 | 重量级锁 |
---|---|---|---|
执行1亿次++操作耗时(单位:毫秒) | 218 | 1802 | 6653 |
比较可知:偏向锁,轻量级锁和重量级锁在性能上有着巨大的差异,如何合理的利用synchronized关键字是十分重要的。