JVM中对象布局
通过引入JOL工具,查看对象在JVM中的布局。
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.14</version></dependency>
对象的在JVM中的基本信息
普通对象:对象头,实例数据,对其填充。
数组对象:对象头,实例数据,数组长度,对其填充
JOL使用
在代码中使用JOL提供的方法查看JVM信息:
System.out.println(VM.current().details());
查看对象信息
创建一个普通类。
public class TestOne {private String name = "对象布局";private Boolean edit;private List<String> dd;String defaultName;protected String protectedName;public String publicName;private int sex;private long likes;private static String staticName;private final String finalName = "fire";public String getName() {return name;}public void setName(String name) {this.name = name;}public Boolean getEdit() {return edit;}public void setEdit(Boolean edit) {this.edit = edit;}public List<String> getDd() {return dd;}public void setDd(List<String> dd) {this.dd = dd;}public String getDefaultName() {return defaultName;}public void setDefaultName(String defaultName) {this.defaultName = defaultName;}public String getProtectedName() {return protectedName;}public void setProtectedName(String protectedName) {this.protectedName = protectedName;}public String getPublicName() {return publicName;}public void setPublicName(String publicName) {this.publicName = publicName;}public int getSex() {return sex;}public void setSex(int sex) {this.sex = sex;}public long getLikes() {return likes;}public void setLikes(long likes) {this.likes = likes;}
}
打印这个类的对象的布局。
public static void main(String[] args) {/* TestOne testOne = new TestOne();String publicName = testOne.publicName;TestPowerChildren testPowerChildren = new TestPowerChildren();String publicName1 = testPowerChildren.publicName;*//* Integer a = 2256;Integer b = 2256;if (a == b) {System.out.println("正常");} else {System.out.println("卧槽");}*///System.out.println(VM.current().details());TestOne testOne = new TestOne();System.out.println(ClassLayout.parseInstance(testOne).toPrintable());}
结果如下(如果子类继承父类,父类的属性也会保存在子类的实例数据中):
Connected to the target VM, address: '127.0.0.1:54320', transport: 'socket'
com.song.owrntest.TestOne object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 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) 92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)12 4 int TestOne.sex 016 8 long TestOne.likes 024 4 java.lang.String TestOne.name (object)28 4 java.lang.Boolean TestOne.edit null32 4 java.util.List TestOne.dd null36 4 java.lang.String TestOne.defaultName null40 4 java.lang.String TestOne.protectedName null44 4 java.lang.String TestOne.publicName null48 4 java.lang.String TestOne.finalName (object)52 4 (loss due to the next object alignment)
Instance size: 56 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes totalDisconnected from the target VM, address: '127.0.0.1:54320', transport: 'socket'Process finished with exit code 0
1. OFFSET:偏移地址,单位为字节。
2. SIZE:占用内存大小,单位为字节。
3. TYPE:Class(类)中定义的类型
4. DESCRIPTION:类型描述,object header 标识对象头,alignment标识对齐填充。
5. VALUE:对应内容中存储的值。
通过结果,我们可以看的很清楚。有一个点就是我们定义的静态变量staticName并没有在对象头里面,它的大小是不计算在对象中的。因为静态变量属于类而不是属于某一个对象的
。
对象的头信息
对象的头信息包含mark word,klass pointer。
mark word:对象运行时的状态数据。
klass pointer:对象所属类的引用指针。
扩展。synchronized锁升级
对象头中的运行时数据mark word
锁的状态有后三位标识。
001:无锁
101:偏向锁
00:轻量级锁
10:重量级锁
在jdk1.6之前,通过synchronized关键字枷锁时使用无差别的中重量级锁。所有线程串行执行,并且cpu在用户态和核心态之间频繁切换。
后面随着对synchronized的不断优化,提出了锁升级的概念,并引入了偏向锁,轻量级锁,重量级锁。在mark word中,锁(lock)标志位占用2个bit,结合1个bit偏向锁(biased_lock)标志位,这样通过倒数的3位,就能用来标识当前对象持有的锁的状态。
无锁状态,偏向锁
锁对象刚创建的时候,没有任务锁竞争,对象处于无锁状态。可以通过jol工具查看对象的内存布局。根据大小端(高位在低地址,地位在高地址)。看到锁的状态。001
因为jdk中偏向锁存在延迟4秒启动。就是说JVM启动后,4秒后创建的对象才会开启偏向锁。可以先通过参数:
-XX:BiasedLockingStartupDelay=0
,关闭这个设置。再打印对象的内存布局。101。表示当前对象
的锁没有被持有,并且处于可被偏向的状态。
当不存在锁竞争的条件下,第一个获取对象锁的线程通过CAS将自己的threadId写入到锁对象的mark word中。若后续这个线程再次获取锁,需要比较当前线程的threadId和锁对象mark word中的threadId是否一致。如果一致,直接获取到锁。
总结
偏向锁通过消除资源无竞争情况下的同步原语,提高了单线程下访问同步资源的运行性能。当出现多个线程竞争的时候,就会撤销偏向锁,升级为轻量级锁。研究表明,大部分情况下,都是同一个线程获取锁对象。
锁过程就是,锁对象中通过CAS设置当前线程的threadId,设置锁对象中的锁状态为101,当前线程释放锁,锁对象mark word不会改变。直到出现不同的线程获取锁对象,比较锁对象的threadId不是当前线程。升级为轻量级锁。
轻量级锁
当存在两个及以上线程获取锁对象的锁的时候,偏向锁升级为轻量级锁。
如果锁对象处于无锁不可偏向状态,JVM首先将当前线程的栈帧中创建一条锁记录,用于存放锁对象的mark word的拷贝(displaced mark word) + 指向当前的锁对象的指针(owner)。在拷贝mark word完成后
,首先会挂起线程
。JVM使用CAS
操作尝试将对象的mark word中的lock record
指针指向栈帧中的锁记录
,并将栈帧中的锁记录中的owner指针
指向锁对象的mark word
。
如果CAS替换成功,表示竞争锁对象成功。锁对象中的锁标识设置为00,标识锁对象处于轻量级锁状态。然后线程执行同步代码块中的逻辑。
如果CAS替换失败,则判断当前锁对象的mark word是否指向当前线程的栈帧。
(1)指向当前线程的栈帧,表示当前线程已经持有对象的锁,执行的是synchronized的锁重入过程,可以直接执行同步代码块逻辑。
(2)指向的不是当前线程的栈帧,说明其他线程已经持有了该对象的锁。当前线程就会自旋一定次数,尝试获取锁。如果自旋一定次数后,还未获取到锁,轻量级锁需要升级为重量级锁,锁对象的标识为10
。后面等待的线程将会进入阻塞状态
。
轻量级锁释放同样使用了CAS操作
,尝试将displaced mark word
替换回锁对象的mark word
。这时需要检查锁对象的mark word中lock record指针是否指向当前线程的栈帧所记录。
(1)替换成功,表示没有锁竞争,整个同步过程完成。
(2)替换失败,表示当前锁资源存在竞争,有可能其他线程在这段时间里尝试过获取锁失败,导致自身被挂起,并修改了锁对象的mark word升级为重量级锁
,最后在执行重量级锁的解锁流程后唤醒被挂起的线程
。
public static void main(String[] args) throws InterruptedException {TestOne testOne=new TestOne();synchronized (testOne){System.out.println(ClassLayout.parseInstance(testOne).toPrintable());}Thread thread = new Thread(() -> {synchronized (testOne) {System.out.println("--THREAD--:"+ClassLayout.parseInstance(testOne).toPrintable());}});thread.start();thread.join();System.out.println("--END--:"+ClassLayout.parseInstance(testOne).toPrintable());
}
偏向锁升级为轻量级锁,在执行完成同步代码后释放锁,变为无锁状态。当再次被线程获取到锁的时候,直接变为轻量级锁(就不会先偏向锁,再轻量级锁)。
总结
轻量级锁是通过CAS来避免开销较大的互斥操作。轻量级锁的轻量是相对与重量级锁而言的。轻量级锁尝试利用CAS,在升级为重量级锁的之前进行补救,目的就是为了减少多线程进入互斥
。jvm使用轻量级锁来保证同步,避免线程切换的开销,不会造成用户态与内核态的切换
。但是如果过度自旋,会引起cpu资源的浪费,这种情况下轻量级锁消耗的资源可能反而会更多。
加锁过程:当前线程在栈空间的栈帧中保存锁对象的mark word。当前线程挂起,JVM使用CAS
操作尝试将对象的mark word中的lock record
指针指向栈帧中的锁记录
,并将栈帧中锁记录中的owner指针
指向锁对象的mark word
。
释放锁过程:JVM尝试将displaced mark word
替换回锁对象的mark word
,并校验锁对象中的lock record是否指向当前线程的锁记录。如果是,流程结束。如果不是,说明存在锁竞争,其他线程在自旋一定次数后还未获取到锁,升级为重量级锁,其他线程阻塞挂起。需要唤醒其他线程。
重量级锁
当获取锁后,调用锁对象的wait()方法后,直接从偏向锁升级为重量级锁,释放锁后,锁状态变为无锁状态。wait()方法调用过程中依赖与重量级锁中与对象关联的monitor。在调用wait()方法后monitor
会把线程
变为WAITING
状态,所以才会强制升级为重量级锁。调用hashCode方法时也会使偏向锁直接升级为重量级锁。
重量级锁是依赖锁对象内部的monitor来实现的,而monitor又依赖与操作系统底层的Mutex lock(互斥锁)实现。这也就是为什么重量级锁比较重的原因。操作系统在实现线程之间的切换时,需要从用户态切换到内核态,成本非常高。
monitor
ower:拥有该monitor的线程,初始时和锁被释放后都为null。
cxq:竞争队列,所有竞争锁失败的线程都会首先被放入这个队列中。
EntryList:候选者列表,当ower解锁时会将cxq队列中的线程移动到该队列中。
OnDeck:将线程从cxq移动到EntryList时,回指定某个线程为Ready状态(即OnDeck),表明它可以竞争锁,如果竞争成功那么成为Owner线程,如果失败则放回EnTryList中。
WaitSet:获取到锁后调用wait() 或 wait(time)方法而被阻塞的线程会被放入到该队列中。
count:monitor的计数器,数值加1表示当前对象的锁被一个线程获取,线程释放monitor对象时减1
recursions:线程重复次数
原理
当升级为重量级锁,锁对象的mark word中指针不在指向线程栈中的lock record。而是指向堆中与锁对象关联的monitor对象。
当多个线程尝试获取锁对象时,其实就是获取monitor对象的所有权。获取成功,执行同步代码块逻辑。获取失败,当前线程会被阻塞,等待其他线程释放锁后唤醒,再次竞争获取锁对象。
在重量级锁的情况下,加解锁过程都涉及到操作系统Mutex Lock进行互斥操作,线程间的调度和线程的状态变更过程需要在用户态和内核态之间进行切换,会导致消耗大量的cpu资源,导致性能降低。