对象内存布局
HotSpot
虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header
)、实例数据(Instance Data
)和对齐填充(Padding
)。
从上面的这张图里面可以看出,对象在内存中的结构主要包含以下几个部分:
Mark Word
(标记字段):对象的Mark Word
部分占4
个字节,其内容是一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。Klass Pointer
(Class
对象指针):Class
对象指针的大小也是4个字节,其指向的位置是对象对应的Class
对象(其对应的元数据对象)的内存地址- 对象实际数据:这里面包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:
byte
和boolean
是1个字节,short
和char
是2个字节,int
和float
是4个字节,long
和double
是8个字节,reference
是4个字节 - 对齐:最后一部分是对齐填充的字节,按
8
个字节填充。
对象头详情
对象头包括两部分:Mark Word 和 类型指针。
标记字段(Mark Word)
MarkWord
用于存储对象自身的运行时数据, 如哈希码(HashCode
)、GC
分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
这部分数据的长度在32位和64位的虚拟机(暂不考虑开启压缩指针的场景)中分别为32个和64个bits。
对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
例如在32位的HotSpot
虚拟机中对象未被锁定的状态下,Mark Word
的32
个bits空间中的25bits
用于存储对象哈希码(HashCode
),4bits
用于存储对象分代年龄,2bits
用于存储锁标志位,1bit
固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。
32位标记字段详情
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
-
lock
:2
位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock
标记。 -
biased_lock
:对象是否启用偏向锁标记,只占1
个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。 -
age
:4
位的Java对象年龄。在GC
中,如果对象在Survivor
区复制一次,年龄增加1
。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC
的年龄阈值为15
,并发GC
的年龄阈值为6
。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold
选项最大值为15
的原因。 -
identity_hashcode
:25
位的对象标识Hash
码,采用延迟加载技术。调用方法System.identityHashCode()
计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor
中。 -
thread
:持有偏向锁的线程ID
。 -
epoch
:偏向时间戳。 -
ptr_to_lock_record
:指向栈中锁记录的指针。 -
ptr_to_heavyweight_monitor
:指向管程Monitor
的指针。
64位标记字段详情
|------------------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal |
|------------------------------------------------------------------------------|--------------------|
| thread: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 |
|------------------------------------------------------------------------------|--------------------|
类型指针(Klass Word)
类型指针指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例。
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM
通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM
的一个字大小,即32
位的JVM
为32
位,64
位的JVM
为64
位。
如果应用的对象过多,使用64
位的指针将浪费大量内存,统计而言,64
位的JVM
将会比32位的JVM
多耗费50%
的内存。为了节约内存可以使用选项+UseCompressedOops
开启指针压缩,其中,oop
即ordinary object pointer
普通对象指针。开启该选项后,下列指针将压缩至32
位:
- 每个
Class
的属性指针(即静态变量) - 每个对象的属性指针(即对象变量)
- 普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM
不会优化,比如指向PermGen
的Class
对象指针(JDK8
中指向元空间的Class
对象指针)、本地变量、堆栈元素、入参、返回值和NULL
指针等。
数组长度(Array Length)
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度。
这部分数据的长度也随着JVM
架构的不同而不同:32
位的JVM上,长度为32
位;64
位JVM
则为64
位。
64
位JVM如果开启+UseCompressedOops
选项,该区域长度也将由64位压缩至32位。
使用JOL来分析java的对象布局
JOL简介
JOL
的全称是Java Object Layout
。是一个用来分析JVM
中Object
布局的小工具。包括Object
在内存中的占用情况,实例对象的引用情况等等。
JOL
可以在代码中使用,也可以独立的以命令行中运行。命令行的我这里就不具体介绍了,今天主要讲解怎么在代码中使用JOL
。
使用JOL需要添加maven
依赖:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>
查看分析vm信息
查看jdk
版本
λ java -version
java version "1.8.0_271"
Java(TM) SE Runtime Environment (build 1.8.0_271-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.271-b09, mixed mode)
通过JOL查看jvm信息
public class ObjectHeadTest {
public static void main(String[] args) {
//查看字节序
System.out.println(ByteOrder.nativeOrder());
//打印当前jvm信息
System.out.println("======================================");
System.out.println(VM.current().details());
}
}
输出:
LITTLE_ENDIAN
======================================
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
上面的输出中,我们可以看到:Objects are 8 bytes aligned
,这意味着所有的对象分配的字节都是8的整数倍。
可以从上面的LITTLE_ENDIAN
发现,内存中字节序使用的是小端模式。
- 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
- 小端字节序:低位字节在前,高位字节在后,即以
0x1122
形式储存。
计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。
人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
查看分析基本类型对象布局
分析String类型
System.out.println(ClassLayout.parseClass(String.class).toPrintable());
输出:
java.lang.String object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 char[] String.value N/A
16 4 int String.hash N/A
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
先解释下各个字段的含义
OFFSET
是偏移量,也就是到这个字段位置所占用的byte数,SIZE
是后面类型的大小,TYPE
是Class中定义的类型,DESCRIPTION
是类型的描述,VALUE
是TYPE
在内存中的值。
分析上面的输出,我们可以得出,String
类中占用空间的有5部分,第一部分是对象头,占12个字节,第二部分是char
数组,占用4个字节,第三部分是int
表示的hash
值,占4个字节 ,总共20个字节。但是JVM
中对象内存的分配必须是8字节的整数倍,所以要补全4字节,最后String
类的总大小是24字节。
分析Long类型
System.out.println(ClassLayout.parseClass(Long.class).toPrintable());
输出:
java.lang.Long object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap)
16 8 long Long.value N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
可以看到1个Long
对象是占24个字节的,但是其中真正存储long
的value
只占8个字节。
分析Long实例对象
System.out.println(ClassLayout.parseInstance(Long.MAX_VALUE).toPrintable());
输出:
java.lang.Long 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 23 00 f8 (00000101 00100011 00000000 11111000) (-134208763)
12 4 (alignment/padding gap)
16 8 long Long.value 9223372036854775807
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
可以看出,对象实例的布局跟类型差不多
分析数组实例对象
public static void main(String[] args) {
//查看字节序
List<String> arr = Lists.newArrayList();
arr.add("111");
arr.add("222");
System.out.println(ClassLayout.parseInstance(arr).toPrintable());
System.out.println("======================================");
String[] strArr = {"0","1","2","3","4","5","6","7","8","9","10"};
System.out.println(ClassLayout.parseInstance(strArr).toPrintable());
}
输出:
java.util.ArrayList 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) 7e 2f 00 f8 (01111110 00101111 00000000 11111000) (-134205570)
12 4 int AbstractList.modCount 2
16 4 int ArrayList.size 2
20 4 java.lang.Object[] ArrayList.elementData [(object), (object), null, null, null, null, null, null, null, null]
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
======================================
[Ljava.lang.String; 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 37 00 f8 (01000011 00110111 00000000 11111000) (-134203581)
12 4 (object header) 0b 00 00 00 (00001011 00000000 00000000 00000000) (11)
16 44 java.lang.String String;.<elements> N/A
60 4 (loss due to the next object alignment)
Instance size: 64 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以发现arr
是一个对象,对象头长度为12bits
,实例数据长度为12bits
,分别是3个属性,每个字符串为4bits
数组strArr
是一个列表,对象头长度为16bits
,可以看到最后一个object header的二进制数据为1011
,转换成十进制是11
,实例数据长度为44bits
,每个字符串为4bits
上面都是字符串String
,所有长度为4bits
,如果改成其他类型,长度也会跟着变动,比如改成Long,就是变成每个8bits
分析HashMap外部引用
HashMap hashMap= new HashMap();
hashMap.put("flydean","www.flydean.com");
System.out.println(GraphLayout.parseInstance(hashMap).toPrintable());
输出:
java.util.HashMap@7106e68ed object externals:
ADDRESS SIZE TYPE PATH VALUE
76bbcc048 48 java.util.HashMap (object)
76bbcc078 24 java.lang.String .table[14].key (object)
76bbcc090 32 [C .table[14].key.value [f, l, y, d, e, a, n]
76bbcc0b0 24 java.lang.String .table[14].value (object)
76bbcc0c8 48 [C .table[14].value.value [w, w, w, ., f, l, y, d, e, a, n, ., c, o, m]
76bbcc0f8 80 [Ljava.util.HashMap$Node; .table [null, null, null, null, null, null, null, null, null, null, null, null, null, null, (object), null]
76bbcc148 32 java.util.HashMap$Node .table[14] (object)
Addresses are stable after 1 tries.
从结果我们可以看到HashMap
本身是占用48
字节的,它里面又引用了占用24
字节的key
和value
。
使用JOL
可以分析java
类和对象,这个对于我们对JVM
和java
源代码的理解和实现都是非常有帮助的。
查看自定义类与实例的对象布局
public class ObjectHeadTest {
private int intValue = 0;
public Integer intValue2 = 999;
private short s1=256;
private Short s2=new Short("2222");
private long l1=222222222222222L;
private Long l2 = new Long(222222222222222L);
public boolean isT = false;
public Boolean isT2 = true;
public byte b1=-128;
public Byte b2=127;
public char c1='a';
public Character c2 = Character.MAX_VALUE;
private float f1=22.22f;
private Float f2=new Float("222.222");
private double d1=22.222d;
private Double d2 = new Double("2222.2222");
private BigDecimal bigDecimal = BigDecimal.ONE;
private String aa = "asdfasdfasdfasdfds";
public static void main(String[] args) {
ObjectHeadTest object = new ObjectHeadTest();
//打印hashcode
System.out.println(object.hashCode());
//打印hashcode二进制
System.out.println(Integer.toBinaryString(object.hashCode()));
//打印hashcode十六进制
System.out.println(Integer.toHexString(object.hashCode()));
//查看字节序
System.out.println("======================================");
System.out.println(ClassLayout.parseClass(ObjectHeadTest.class).toPrintable());
System.out.println("======================================");
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
输出:
396873410
10111101001111100111011000010
17a7cec2
======================================
com.qhong.basic.jol.ObjectHeadTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int ObjectHeadTest.intValue N/A
16 8 long ObjectHeadTest.l1 N/A
24 8 double ObjectHeadTest.d1 N/A
32 4 float ObjectHeadTest.f1 N/A
36 2 short ObjectHeadTest.s1 N/A
38 2 char ObjectHeadTest.c1 N/A
40 1 boolean ObjectHeadTest.isT N/A
41 1 byte ObjectHeadTest.b1 N/A
42 2 (alignment/padding gap)
44 4 java.lang.Integer ObjectHeadTest.intValue2 N/A
48 4 java.lang.Short ObjectHeadTest.s2 N/A
52 4 java.lang.Long ObjectHeadTest.l2 N/A
56 4 java.lang.Boolean ObjectHeadTest.isT2 N/A
60 4 java.lang.Byte ObjectHeadTest.b2 N/A
64 4 java.lang.Character ObjectHeadTest.c2 N/A
68 4 java.lang.Float ObjectHeadTest.f2 N/A
72 4 java.lang.Double ObjectHeadTest.d2 N/A
76 4 java.math.BigDecimal ObjectHeadTest.bigDecimal N/A
80 4 java.lang.String ObjectHeadTest.aa N/A
84 4 (loss due to the next object alignment)
Instance size: 88 bytes
Space losses: 2 bytes internal + 4 bytes external = 6 bytes total
======================================
com.qhong.basic.jol.ObjectHeadTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 c2 ce a7 (00000001 11000010 11001110 10100111) (-1479622143)
4 4 (object header) 17 00 00 00 (00010111 00000000 00000000 00000000) (23)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 int ObjectHeadTest.intValue 0
16 8 long ObjectHeadTest.l1 222222222222222
24 8 double ObjectHeadTest.d1 22.222
32 4 float ObjectHeadTest.f1 22.22
36 2 short ObjectHeadTest.s1 256
38 2 char ObjectHeadTest.c1 a
40 1 boolean ObjectHeadTest.isT false
41 1 byte ObjectHeadTest.b1 -128
42 2 (alignment/padding gap)
44 4 java.lang.Integer ObjectHeadTest.intValue2 999
48 4 java.lang.Short ObjectHeadTest.s2 2222
52 4 java.lang.Long ObjectHeadTest.l2 222222222222222
56 4 java.lang.Boolean ObjectHeadTest.isT2 true
60 4 java.lang.Byte ObjectHeadTest.b2 127
64 4 java.lang.Character ObjectHeadTest.c2 �
68 4 java.lang.Float ObjectHeadTest.f2 222.222
72 4 java.lang.Double ObjectHeadTest.d2 2222.2222
76 4 java.math.BigDecimal ObjectHeadTest.bigDecimal (object)
80 4 java.lang.String ObjectHeadTest.aa (object)
84 4 (loss due to the next object alignment)
Instance size: 88 bytes
Space losses: 2 bytes internal + 4 bytes external = 6 bytes total
分析对象头
从上面的vm
信息可以得出,该内存中的字节序为小端模式
hashcode
的二进制位10111101001111100111011000010
拆分开来应该是10111 10100111 11001110 11000010
转换成16
进制17 a7 ce c2
64
位的Mark Word
中的布局为
unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2
按上述的展示为:
00 00 00 | 17 a7 ce c2| 01
反过来就是
01 | c2 ce a7 17 | 00 00 00
三种锁
Java
对象的锁状态一共有四种,级别从低到高依次为: 无锁(01) -> 偏向锁(01) -> 轻量级锁(00) -> 重量级锁(10).
但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
JDK 1.6
中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking
来禁用偏向锁。
要注意锁的升级目的是为了提高锁的获取效率和释放效率。
偏向锁
HotSpot 研究员发现很多时候,锁不存在多线程之间的竞争,而是总是有同一个线程获取。如果在对象头加入上一次成功获取锁的线程Id,等待下次这个线程再次获取锁的时候(此时无其他线程竞争锁),那么线程不在需要通过自旋来获取锁,而是通过简单的测试对象记录的线程Id是否与当前线程的Id相等,如果相等直接加锁成功(线程Id相当于一个缓存的作用。),当然解锁也是如此。
当然以上只是偏向锁的一种使用场景,关于场景我们分三种情况来讨论:
- 1.只有一个线程T1 获取锁(上述场景)
- 2.线程T1与线程T2交替获取锁
- 3.线程T1与线程T2同时进入同步块,竞争锁。
当出现第二种情况: 两个线程交替获取锁,那么线程T1成功获取锁以后,线程T2尝试竞争锁的时候,他会检测线程T1是否还存活。如果下面层T1已经挂了,那么对象头会设置成无锁状态,并在T2成功获取锁后,重新偏向于线程T2.
当出现第三种情况: 两个线程同时竞争锁,当T2自旋获取锁失败时,表示存在锁竞争,当到达全局安全点的时候(会有资源消耗),偏向锁会被撤销,锁升级为轻量级锁。
轻量级锁
1 轻量级锁加锁
轻量级锁的加锁,在线程执行同步块之前,会现在当前线程的栈帧中创建存储Mark Word的锁记录里空间(官方称为Displace Mark Word
),然后利用CAS 将对象头的MarkWord
替换为指向锁记录的地址,如果成功,则获取锁成功,如果失败,则当前线程利用自旋来获取锁,若自旋到一定程度之后依然没有获取到锁,则锁会膨胀成重量级锁。
2 轻量级锁解锁
轻量级锁的解锁,在执行完同步块之后,会将Displace Mark Word
里面的锁记录替换回对象头中。如果替换成功,则解锁成功,如果替换失败,则解锁失败,锁就会膨胀成重量级锁。
重量级锁
例如我们最经常看见的synchronized就是非常典型的重量级锁,通过指令moniter enter 加锁,moniter exit解锁。重量级锁的同步成本非常高,包括内核态与用户态的切换造成的资源损耗等。
JVM
中对锁的优化
jdk1.6
对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
自旋锁
线程的阻塞和唤醒需要CPU
从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
何谓自旋锁?
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在JDK 1.4.2
中引入,默认关闭,但是可以使用-XX:+UseSpinning
开开启,在JDK1.6
中默认开启。同时自旋的默认次数为10
次,可以通过参数-XX:PreBlockSpin
来调整;
如果通过参数-XX:preBlockSpin
来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10
,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6
引入自适应的自旋锁,让虚拟机会变得越来越聪明。
自适应自旋锁
JDK 1.6
引入了更加聪明的自旋锁,即自适应自旋锁。
所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?
线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。
锁消除
为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM
检测到不可能存在共享数据竞争,这是JVM
会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK
的内置API
时,如StringBuffer
、Vector
、HashTable
等,这个时候会存在隐形的加锁操作。比如StringBuffer
的append()
方法,Vector
的add()
方法:
public void vectorTest(){
Vector<String> vector = new Vector<String>();
for(int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}复制代码
在运行这段代码时,JVM
可以明显检测到变量vector
没有逃逸出方法vectorTest()
之外,所以JVM
可以大胆地将vector
内部的加锁操作消除。
锁粗化
我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector
每次add
的时候都需要加锁操作,JVM
检测到对同一个对象(vector
)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for
循环之外。