一个java对象到底有多大

摩森特沃 2021年03月17日 860次浏览

转载声明:本文引用自【面试官,欺负人:new Object()到底占用几个字节?】,如有侵权,请联系删除

前言

问题:new Object()到底占用几个字节?

这个问题,需要我们来分析一下堆内布局以及Java对象在内存中的布局

对象的指向

先来看一段代码:

package com.zwx.jvm;

public class HeapMemory {
    private Object obj1 = new Object();

    public static void main(String[] args) {
        Object obj2 = new Object();
    }
}

上面的代码中,obj1 和obj2在内存中有什么区别?

我们先来回忆一下JVM系列的文章中有提到,方法区存储每个类的结构,比如:运行时常量池、属性和方法数据,以及方法和构造函数等数据。所以我们这个obj1是存在方法区的,而new会创建一个对象实例,对象实例是存储在堆内的,于是就有了下面这幅图(方法区指向堆):

A522RT

而obj2 是属于方法内的局部变量,存储在Java虚拟机栈内的栈帧中的局部变量表内,这就是经典的栈指向堆:

Ucu72Z

这里我们再来思考一下,我们一个变量指向了堆,而堆内只是存储了一个实例对象,那么堆内的实例对象是如何知道自己属于哪个Class,也就是说这个实例是如何知道自己所对应的类元信息的呢?这就涉及到了一个Java对象在内存中是如何布局的。

Java内存模型

对象内存中可以分为三块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding),以64位操作系统为例(未开启指针压缩的情况)Java对象布局如下图所示:

UvEQin

其中对象头中的Mark Word中的详细信息在文章synchronized锁升级原理中有详细介绍。上图中的对齐填充不是一定有的,如果对象头和实例数据加起来刚好是8字节的倍数,那么就不需要对齐填充。

知道了Java内存布局,那么我们来看一个面试问题

Object obj=new Object()占用字节

这是网上很多人都会提到的一个问题,那么结合上面的Java内存布局,我们来分析下,以64位操作系统为例,new Object()占用大小分为两种情况:

  • 未开启指针压缩
    占用大小为:8(Mark Word)+8(Class Pointer)=16字节
  • 开启了指针压缩(默认是开启的)
    开启指针压缩后,Class Pointer会被压缩为4字节,最终大小为:8(Mark Word)+4(Class Pointer)+4(对齐填充)=16字节
    结果到底是不是这个呢?我们来验证一下。首先引入一个pom依赖:
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

然后新建一个简单的demo:

package com.zwx.jvm;

import org.openjdk.jol.info.ClassLayout;

public class HeapMemory {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

输出结果如下:

开启指针压缩的结果

最后的结果是16字节,没有问题,这是因为默认开启了指针压缩,那我们现在把指针压缩关闭之后再去试试。

-XX:+UseCompressedOops  开启指针压缩
-XX:-UseCompressedOops  关闭指针压缩

关闭指针压缩

再次运行,得到如下结果:

关闭指针压缩的结果

可以看到,这时候已经没有了对齐填充部分了,但是占用大小还是16位。

下面我们再来演示一下如果一个对象中带有属性之后的大小。

新建一个类,内部只有一个byte属性:

package com.zwx.jvm;

public class MyItem {
    byte i = 0;
}

然后分别在开启指针压缩和关闭指针压缩的场景下分别输出这个类的大小。

package com.zwx.jvm;

import org.openjdk.jol.info.ClassLayout;

public class HeapMemory {
    public static void main(String[] args) {
        MyItem myItem = new MyItem();
        System.out.println(ClassLayout.parseInstance(myItem).toPrintable());
    }
}

开启指针压缩,占用16字节:

开启指针压缩的结果

关闭指针压缩,占用24字节:

关闭指针压缩的结果

这个时候就能看出来开启了指针压缩的优势了,如果不断创建大量对象,指针压缩对性能还是有一定优化的。

对象的访问

创建好一个对象之后,当然需要去访问它,那么当我们需要访问一个对象的时候,是如何定位到对象的呢?目前最主流的访问对象方式有两种:句柄访问直接指针访问

句柄访问

使用句柄访问的话,Java虚拟机会在堆内划分出一块内存来存储句柄池,那么对象当中存储的就是句柄地址,然后句柄池中才会存储对象实例数据和对象类型数据地址。

句柄访问示意图

直接指针访问(Hot Spot虚拟机采用的方式)

接指针访问的话对象中就会直接存储对象类型数据。

指针访问示意图

句柄访问和直接指针访问对比

上面图形中我们很容易对比,就是如果使用句柄访问的时候,会多了一次指针定位,但是他也有一个好处就是,假如一个对象被移动(地址改变了),那么只需要改变句柄池的指向就可以了,不需要修改reference对象内的指向,而如果使用直接指针访问,就还需要到局部变量表内修改reference指向。

一个对象的人生轨迹图

从前面博文的介绍我们知道一个对象会在Eden区,Survivor区,Old区不断流转(当然,一开始就会被回收的短命对象除外),我们可以得到下面的一个流程图:

对象生命周期

其他参考内容