一、一个Object对象在JVM内存中占用多大
想要知道一个java对象占用内存大小,一般用一个JOL工具来计算。
JOL(Java Object Layout)是OpenJDK官方提供的java对象内存查看工具。
只要加入依赖包即可
org.openjdk.jol jol-core0.16 provided
ObjectTest.java
package Object; import org.junit.Test; import org.openjdk.jol.datamodel.Model64; import org.openjdk.jol.datamodel.Model64_COOPS_CCPS; import org.openjdk.jol.info.ClassLayout; import org.openjdk.jol.layouters.HotSpotLayouter; import org.openjdk.jol.layouters.Layouter; public class ObjectTest { @Test public void test1() { Layouter layouter; layouter = new HotSpotLayouter(new Model64(), 8); System.out.println(ClassLayout.parseInstance(new Object(), layouter).toPrintable()); System.out.println("========================================"); layouter = new HotSpotLayouter(new Model64_COOPS_CCPS(), 8); System.out.println(ClassLayout.parseInstance(new Object(), layouter).toPrintable()); System.out.println("========================================"); } }
执行结果:
java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 8 (object header: class) 0x00000000f80001e5 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total ======================================== java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ========================================
(1)未开启指针压缩,有两个对象头。开启指针压缩,有两个对象头和一个对齐填充。总共都是16字节
(2)未开启指针压缩,16字节对象头(8字节的markword,8字节的klass指针)
(3)开启指针压缩,12个字节对象头(8字节的markword,4字节的klass指针)+ 4字节对齐填充
(4)说明
OFF:OFFSET,偏移地址,单位字节
SZ:SIZE,占用的内存大小,单位字节
TYPE DEscriptION:类型描述,其中object header为对象头
VALUE:对应内存中当前存储的值,二进制32位
二、Object对象以什么格式在内存中存储
1、Mark Word
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
Mark Word在32位JVM中的长度是32bit,在64位JVM中的长度是64bit。
2、klass指针
一个class文件被JVM加载之后,就会被解析成一个klass对象存储在方法区中。
对象和类的关系,就是这个指针指向方法区的内容(四大块内容)
3、什么是压缩指针呢?
(1)类型指针(Class Pointer)记录的是对象类型在metaSpace的地址引用,指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例。(就是klass指针)
(2)比如new Object()这个对象,类型指针记录的就是Object.class的地址引用。
(3)类型指针占用的内存大小分两种情况,当开启对象压缩时占用4字节(JVM默认开启),关闭时占用8字节,关闭压缩指针参数:-XX:UseCompressedOops
(4)压缩指针不仅可以作用于对象头的类型指针,还可以作用于引用类型的字段,以及引用类型的数组。
(5)在64位 *** 作系统中,对象头中的类型指针占用64位(8字节),开启压缩指针后占用32位(4字节),压缩指针的目的即节省内存空间。
4、对齐填充
对齐填充(Padding),这个部分存在的目的是为了保持对象大小与8字节的倍数对齐。
假如一个对象占用12字节,12不是8的倍数,则需要填充4字节,16刚好是8的倍数,那么这块区域就会用0进行填充。
如果对象大小刚好等于8的倍数,如16、32等,则该区域大小为0。
5、为什么要进行8字节内存对齐?
原因一:在默认情况下,JVM堆中的对象默认要对齐8字节倍数,可以通过参数-XX:ObjectAlignmentInBytes修改
原因二:是由于CPU进行内存访问时,一次寻址的指针大小是8字节,正好也是L1缓存行的大小
如果不进行内存对齐,则可能出现跨缓存行的情况,这叫做缓存行污染:
之所以叫做“污染”,是由于当obj1对象的字段被修改后,那么CPU在访问obj2对象时,必须将其重新加载到缓存行,因此影响了程序执行效率(修改Object1,会把Object2的3个字节也读进来)
三、对象中的属性是如何在内存中分配的?
1、对象
MyObjectData.java
package Object; public class MyObjectData { private int i = 66; private long l = 6L; private String str = new String("aaa"); }
ObjectTest.java
package Object; import org.junit.Test; import org.openjdk.jol.datamodel.Model64; import org.openjdk.jol.datamodel.Model64_COOPS_CCPS; import org.openjdk.jol.info.ClassLayout; import org.openjdk.jol.layouters.HotSpotLayouter; import org.openjdk.jol.layouters.Layouter; public class ObjectTest { @Test public void test1() { Layouter layouter; layouter = new HotSpotLayouter(new Model64(), 8); System.out.println(ClassLayout.parseInstance(new MyObjectData(), layouter).toPrintable()); System.out.println("========================================"); layouter = new HotSpotLayouter(new Model64_COOPS_CCPS(), 8); System.out.println(ClassLayout.parseInstance(new MyObjectData(), layouter).toPrintable()); System.out.println("========================================"); } }
运行结果:
Object.MyObjectData object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 8 (object header: class) 0x00000042f801234d 16 8 long MyObjectData.l 6 24 4 int MyObjectData.i 66 28 4 (alignment/padding gap) 32 8 java.lang.String MyObjectData.str (object) 40 0 (object alignment gap) Instance size: 32 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total ======================================== Object.MyObjectData object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 4 (object header: class) 0xf801234d 12 4 int MyObjectData.i 66 16 8 long MyObjectData.l 6 24 4 java.lang.String MyObjectData.str (object) 28 4 (object alignment gap) Instance size: 32 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ========================================
未开启压缩:
//16字节的header(8字节markword、8字节kclass指针)
//8字节的long字段,值为6
//4字节的int字段,值为66
//4字节的对齐填充(不够8的倍数)
//8字节的String字段
//0字节内存对齐
开启压缩:
//12字节的header(8字节markword、4字节kclass指针)
//4字节的int字段,值为66
//8字节的long字段,值为6
//4字节的String字段(启动指针压缩,把原本8字节压缩为4个字节)
//4字节的对齐填充(不够8的倍数)
2、数组
@Test public void test2() { int[] a = {1}; Layouter layouter; layouter = new HotSpotLayouter(new Model64_COOPS_CCPS(), 8); System.out.println("***** " + layouter); System.out.println(ClassLayout.parseInstance(a, layouter).toPrintable()); }
执行结果:
***** Hotspot Layout Simulation (JDK 8, 64-bit model, compressed references, compressed class pointers, 8-byte aligned) [I object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 4 (object header: class) 0xf800016d 12 4 (array length) 1 12 4 (alignment/padding gap) 16 4 int [I.N/A 20 4 (object alignment gap) Instance size: 24 bytes Space losses: 4 bytes internal + 4 bytes external = 8 bytes total
3、对象中属性总结
(1)对象数据
如果对象有属性字段,则这里会有数据信息。
如果对象无属性字段,则这里就不会有数据。
根据字段类型的不同占不同的字节,例如boolean类型占1个字节,int类型占4个字节等等。
(2)数组和属性
数组会多保存一个数组的长度。
(3)指针压缩
jdk8版本是默认开启指针压缩的,可以通过配置vm参数开启关闭指针压缩
-XX:+UseCompressedOops //开启指针压缩
-XX:-UseCompressedOops //关闭指针压缩
四、什么是对象头中的Mark Word
1、MarkWordTest.java
package Object; import org.junit.Test; import org.openjdk.jol.info.ClassLayout; public class MarkWordTest { @Test public void test1() { Object obj = new Object(); System.out.println(obj + "十六进制哈希:" + Integer.toHexString(obj.hashCode())); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); } }
执行结果:
java.lang.Object@61e717c2十六进制哈希:61e717c2 java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x00000061e717c201 (hash: 0x61e717c2; age: 0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
2、将mark word转换为二进制
0x00000061e717c201:
0000 0000 0000 0000 0000 0000 0110 0001 1110 0111 0001 0111 1100 0010 0000 0001
3、面试题
为什么晋升老年代的年龄设置(XX:MaxTenuringThreshold)不能超过15
因为就给了age四个bit空间,最大就是1111(二进制)也就是15,多了没地方存
五、为什么java对象头要存储锁信息
1、简单的代码
synchronized (obj) { //锁内容处理 System.out.println("锁住......"); }
2、谁来记录锁
高并发多线程抢obj,那如果是线程B抢到,线程B就锁住了obj,其他线程就不能抢。
问题:谁来记录线程B抢到obj,并告诉其他线程等待?
A方案:开辟一个空间来存储,obj=B,当B解锁时把obj=null,其他线程每次检查obj是否为null,不是null就继续抢obj。
B方案:在obj的对象头开辟一块锁空间把B设置进去,当B解锁时,obj的对象头锁空间清空,其他线程只要对象头锁空间为空,都可以继续抢。
解答:
这2种方案中,A方案有个致命性的缺陷,就是新开辟的空间有线程安全问题,还要继续加锁,麻烦。
而B方案就没有线程安全的问题了,obj本身就是被锁住,谁拿到锁谁在obj身上设置自己。
这个就是我们讲的对象头Mark Word空间。
3、Mark Word空间存储了4种锁
无锁 --> 001
偏向锁 --> 101
轻量级锁 --> 000
重量级锁 --> 010
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。
六、什么是无锁,什么是匿名偏向锁
1、例子MarkWordTest2.java
package Object; import org.junit.Test; import org.openjdk.jol.info.ClassLayout; public class MarkWordTest2 { @Test public void test1() { Object obj2 = new Object(); System.out.println(ClassLayout.parseInstance(obj2).toPrintable()); } @Test public void test2() throws InterruptedException { Thread.sleep(5000); Object obj2 = new Object(); System.out.println(ClassLayout.parseInstance(obj2).toPrintable()); } }
test1是无锁:
java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
test2是匿名偏向锁:
java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
2、无锁
当前我们的代码是没有加锁的,当前偏向锁为0,锁类型为01
故,无锁的对象头MarkWord格式:偏向锁为0,锁类型为01
3、匿名偏向锁
在对象,在支持偏向之前创建的对象都是无锁状态的,但是在支持偏向后(这个对象给谁),创建的对象就自动带有偏向标识。
但是此时是没有偏向任何线程的,属于一个匿名偏向(anonymously biased)状态,此时对象可以偏向任何一个线程。
就是说这个对象支持被锁住,目前没有谁来拿这个锁。
七、匿名偏向锁如何升级为偏向锁
1、例子MarkWordTest3.java
package Object; import org.junit.Test; import org.openjdk.jol.info.ClassLayout; public class MarkWordTest3 { @Test public void test1() throws InterruptedException { //JVM虚拟机启动的时候创建 Object obj1 = new Object(); //无锁 System.out.println(ClassLayout.parseInstance(obj1).toPrintable()); Thread.sleep(5000); //JVM启动5秒后创建对象 Object obj2 = new Object(); //匿名偏向锁 System.out.println(ClassLayout.parseInstance(obj2).toPrintable()); synchronized (obj2) { //偏向锁 System.out.println(ClassLayout.parseInstance(obj2).toPrintable()); } } }
执行结果:
java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x000002481578a005 (biased: 0x0000000092055e28; epoch: 0; age: 0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
2、说明
无锁是001,加睡眠从001变成101,加锁还是101
3、为什么要加睡眠
因为虚拟机在启动的时候做了优化,对于偏向锁有延迟,如果没有偏向锁的延迟的话,虚拟机在启动的时候,可能JVM某个线程调用你的线程,这样就有可能变成了轻量锁或者重量锁,所以要做偏向锁的延迟。
那我们怎么看到打印的对象头是偏向锁呢?
有两种方式:
第一种是加锁之前先让线程睡几秒。
第二种加上JVM的运行参数,关闭偏向锁的延迟,具体的命令如下:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
4、什么是偏向锁
如果在运行过程中,同步锁只有一个线程访问(有一个资源加了synchronized同步块),不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
匿名偏向锁和偏向锁的区别?
匿名偏向锁:无线程ID,0x0000000000000005 (biasable; age: 0)
偏向锁:有线程ID,0x000002481578a005 (biased: 0x0000000092055e28; epoch: 0; age: 0)
5、偏向锁线程ID的作用(此线程ID非JVM线程ID)
(1)一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁的偏向的线程ID。
(2)当下次该线程进入这个同步块时,会去检查锁的Mark Word里面是不是放的自己的线程ID。
(3)如果是,表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费CAS *** 作来加锁和解锁。
(4)如果不是,就代表有另一个线程来竞争这个偏向锁。
(5)这个时候会尝试使用CAS来替换Mark Word里面的线程ID为新线程ID,这个时候要分两种情况:
成功,表示之前的线程不存在了,Mark Word里面的线程ID为新线程ID,锁不会升级,仍然为偏向锁;
失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。
注意:这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
八、什么是轻量级锁?在什么情况下,偏向锁会升级为轻量级锁?
1、例子MarkWordTest4.java
package Object; import org.junit.Test; import org.openjdk.jol.info.ClassLayout; public class MarkWordTest4 { @Test public void test1() throws InterruptedException { System.out.println("无锁.........."); //JVM虚拟机启动的时候创建 Object obj1 = new Object(); //无锁 System.out.println(ClassLayout.parseInstance(obj1).toPrintable()); Thread.sleep(5000); //JVM启动5秒后创建对象 Object obj2 = new Object(); //匿名偏向锁 System.out.println("匿名偏向锁.........."); System.out.println(ClassLayout.parseInstance(obj2).toPrintable()); synchronized (obj2) { //偏向锁 System.out.println("偏向锁有线程ID.........."); System.out.println(ClassLayout.parseInstance(obj2).toPrintable()); } new Thread(()->{ synchronized (obj2) { //轻量级锁 System.out.println("轻量级锁.........."); System.out.println(ClassLayout.parseInstance(obj2).toPrintable()); } }).start(); } }
执行结果:
无锁.......... java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 匿名偏向锁.......... java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 偏向锁有线程ID.......... java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x0000020f9d15b005 (biased: 0x0000000083e7456c; epoch: 0; age: 0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 轻量级锁.......... java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x00000072c5fff3c0 (thin lock: 0x00000072c5fff3c0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
2、说明
当主线程进入的时候,锁没有升级,此时还是偏向锁,但是当其他线程进入的时候,偏向锁便升级为了轻量级锁。
3、什么是轻量级锁
(1)多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。
针对这种情况,JVM采用轻量级锁来避免线程的阻塞与唤醒。
(2)在偏向锁的时候,我们说过用CAS来替换Mark Word里面的线程ID为新线程ID,失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进行竞争。
(3)一旦失败,就说明在与其他线程竞争锁,当前线程就尝试使用自旋来获取锁。
自旋:不断尝试去获取锁,一般用循环来实现。
自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。
(4)解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。
(5)但JDK采用了更聪明的方式——适应性自旋,简单来说就是线程自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
(6)自旋也不是一直进行下去的,如果自旋到一定程度(和JVM、 *** 作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁(阻塞)。
九、在什么情况下,轻量级锁会升级为重量级锁?
1、例子MarkWordTest5.java
package Object; import org.junit.Test; import org.openjdk.jol.info.ClassLayout; public class MarkWordTest5 { @Test public void test1() throws InterruptedException { Thread.sleep(5000); Object obj = new Object(); Thread t0 = new Thread(()->{ synchronized (obj) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(ClassLayout.parseInstance(obj).toPrintable() + "---------- 线程1 重量级锁 结束"); } }); Thread t1 = new Thread(()->{ synchronized (obj) { System.out.println(ClassLayout.parseInstance(obj).toPrintable() + "---------- 线程2 重量级锁 结束"); } }); t0.start(); t1.start(); Thread.sleep(20*1000); } }
执行结果:
java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x000001befd4e710a (fat lock: 0x000001befd4e710a) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ---------- 线程1 重量级锁 结束 java.lang.Object object internals: OFF SZ TYPE DEscriptION VALUE 0 8 (object header: mark) 0x000001befd4e710a (fat lock: 0x000001befd4e710a) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ---------- 线程2 重量级锁 结束
2、什么是重量级锁
(1)自旋一直失败(达到一定程度),依然拿不到锁,就会阻塞,就会升级为重量级锁。
(2)重量级锁依赖于 *** 作系统的互斥量(mutex)实现的,而 *** 作系统中线程间状态的转换需要相对比较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。
(3)重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁。
十、小结
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)