您的当前位置:首页正文

Java对象在jvm堆内存中的存储布局

2024-11-30 来源:个人技术集锦

这次说的是64bit的jvm,暂不谈32bit的jvm,两者的对象大小是不一样的。

java对象的存储布局分为两种情况:普通对象 和 数据。

先说普通对象吧:如下图

那为啥对齐单元的大小是8个字节呢?

因为是64bit位的jvm,8个字节刚好是64个比特位,这样读取的效率高。

那这个东西我们在代码里面看得到吗? 可以看到的。

我们在pom.xml里面引入依赖:

 <dependency>

            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.14</version>
            <scope>provided</scope>

<dependency>

但是我的报错啊:java.lang.ClassNotFoundException: org.openjdk.jol.info.ClassLayout,我也不知道为啥,解决办法如下:

点开如下链接:

,选择一个版本,我选择的是0.10/这个版本,亲测可用:

然后选择格式符合: jol-cli-.-full.jar的包

 然后选中jar,将其添加为依赖,并且删除原来的maven依赖。

这样就可以用了。

 我们在代码里运行下:

1.0 情形:

public class Test {
   
}
public class RunTest {
    public static void main( String[] args ) {
        Test test = new Test();
        System.out.println(ClassLayout.parseInstance(test).toPrintable());
    }
}

运行的结果如下:

奇怪了,怎么只有header和padding呢,实例数据那一部分呢?

实例数据是指成员变量,而Test.java里面没有成员变量。所以总共16个字节。

那如果是Object类型的对象呢,Object里面没有成员变量。不管压缩与否都是16个字节,为啥??

如果是32位的jvm呢,32位的jvm不能压缩指针哦,Object对象是8个字节,如下图:

1.1情形:

public class Test {
    int a;
}
public class RunTest {
    public static void main( String[] args ) {
        Test test = new Test();
        System.out.println(ClassLayout.parseInstance(test).toPrintable());
    }
}

这种情况应该是多少个字节呢? int类型占4个字节。如下图:

header部分不变,依然是12个字节,成员变量a占4个字节,刚好16个字节。

那为什么这次没有填充数据呢?注意了,只有在对象大小不能被8整除的时候才需要填充数据。

那如果是这样呢:

public class Test {
    int a;
    int b;
}
多少个字节? header部分不变12 bytes,两个int类型成员变量8 bytes,这个时候需要填充4 bytes, 加起来共24个字节。 

那如果是这样呢:

public class Test {
    int a;
    int b;
    boolean c;
}
多少个字节? header部分不变12 bytes,两个int类型成员变量8 bytes,注意了boolean类型只占1个字节,所以需要填充3个字节,凑成24个字节。

那如果是这样呢:

public class Test {
    int a;
    int b;
    boolean c;
    String str = "hello";
}
多少个字节?header部分不变12 bytes,两个int类型成员变量8 bytes,注意了boolean类型只占1个字节。字符串类型的是怎么算呢? 我们要搞清楚,str才是成员变量,"hello"是一个字符串对象,是两回事。首先啊,字符串是一个特殊的对象,它是存放在字符串常量池中,那Test对象是多少字节呢?那str的大小为零吗 或者是5个字节吗??str是句柄,引用类型的,是存储对象的地址,也就是普通对象的指针,这个指针占用多少个字节呢? 

 12 + 4 + 4 + 1 + 3 + 4 + 4 = 32 bytes。有两个问题哦:

1.  为什么编译器会将boolean类型转为int类呢?

2.  64位的计算机,指针应该是8个字节啊。64位的jvm,对象的指针为什么是4个字节呢?

首先回答问题1:

因为虚拟机规范只有 4字节 和 8字节, 大部分指令都没有支持 byte、char 和 short 类型,甚至没有任何指令支持 boolean 类型。编译器会在编译器或运行期将 byte 和 short 类型带符号扩展为 int 类型, boolean 和 char 类型零位扩展为相应的 int 类型。与之类似,在处理 boolean、byte、char 和 short 类型的数组时,也会转为使用相应的 int 类型的字节码来处理指令。 因此,大多数对于 boolean、byte、char 和 short 类型数据的操作,实际都是使用 int 类型作为运算类型。另外还有第二点原因,在设计虚拟机时,主要考虑的是 32位体系,32位系统使用 4 字节是最节省,因为 CPU 只能 32位32位的寻址。

都说 byte、boolen 类型占 1字节,但上面又提到, byte 会被提升为 int 类型,那么就应该占了 4字节类型(long、float), boolean、char、short 都是占了 4字节

再来回答问题2:

如下图,执行命令:java -XX:+PrintCommandLineFlags -version

我们可以配置关闭这两个压缩:

将 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops                                            改为: -XX:-UseCompressedClassPointers -XX:-UseCompressedOops 

关闭压缩后,再运行,就成了 32(关闭前) + 4 + 4 = 40bytes,如下图:

最后再掰饬下一个问题啊,header里面包含了markword和class pointer这两部分,类型指针就是你属于哪个class对象就指向哪个class对象,上面已经说过了。现在重点这个header里面的markword里面到底装了些啥呢?markword装了三部分:1. 记录锁的状态,是无锁还是轻量级锁还是重量级锁,如果有锁,还得指向哪个线程持有这个锁等信息;2. GC标记信息(例如CMS用到三色标记法,需要记录三种颜色。还有有一个很重要的信息就是对象的年龄,在from和to去来回拷贝,每拷贝一次年龄就加1,这个年龄啊就记录在markword里面);3. 记录对象的hashcode信息,注意啊,只有调用了对象的hashcode()方法才会记录,记录之后,下次调用hashcode()方法,就不用重新计算了(对象的hashcode不会变),直接去头里面拿就可以了。

markword里面存hashcode有什么用呢? 会经常被用到吗?有用,HashMap和HashSet等等数据结构不都是用到了hashcode吗,你存储数据的时候,不经常用到这些吗? 所以说有用。

下面演示一下,只有调用了hashcode()方法,才会在markword里面记录hashclode信息。

public class RunTest {
    public static void main( String[] args ) {
        Test test = new Test();
        System.out.println(ClassLayout.parseInstance(test).toPrintable());
        test.hashCode();
        System.out.println(ClassLayout.parseInstance(test).toPrintable());
    }
}

如上图所示,对比调用hashcode()之前和之后的markword信息。

下面演示一下,无锁,持有锁,释放锁这三种状态,对比下在markword里面记录的信息。

public class RunTest {
    public static void main( String[] args ) {
        Test test = new Test();

        System.err.println("无锁状态:");
        System.err.println(ClassLayout.parseInstance(test).toPrintable());

        synchronized (test) {
            System.out.println("持有锁状态:");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
        }

        System.err.println("释放锁状态:");
        System.err.println(ClassLayout.parseInstance(test).toPrintable());
    }
}

 

 

对比就知道,对象作为锁之后,锁的状态信息会记录在markword里面,释放锁之后和无锁两种状态的记录信息是一样的。 

本次就分享这么多,欢迎大佬们指正!

显示全文