Java基础之《JVM性能调优(5)—Java对象内存布局》

Java基础之《JVM性能调优(5)—Java对象内存布局》,第1张

Java基础之《JVM性能调优(5)—Java对象内存布局》

一、一个Object对象在JVM内存中占用多大
想要知道一个java对象占用内存大小,一般用一个JOL工具来计算。
JOL(Java Object Layout)是OpenJDK官方提供的java对象内存查看工具。
只要加入依赖包即可


    org.openjdk.jol
    jol-core
    0.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)重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁。

十、小结

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5717097.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存