java一个对象占用多少字节?

最近在读《深入理解Java虚拟机》,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存?

想弄清楚上面的问题,先补充一下基础知识。

1、JAVA 对象布局
在 HotSpot虚拟机中,对象在内存中的存储的布局可以分为三块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)

1.1对象头(Header):
Java中对象头由 Markword + 类指针kclass(该指针指向该类型在方法区的元类型) 组成。
普通对象头在32位系统上占用8bytes,64位系统上占用16bytes。64位机器上,数组对象的对象头占用24个字节,启用压缩之后占用16个字节。

用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。在64位的虚拟机(未开启压缩指针)为64bit。
Markword:

类指针kclass:
kclass存储的是该对象所属的类在方法区的地址,所以是一个指针,默认Jvm对指针进行了压缩,用4个字节存储,如果不压缩就是8个字节。 关于Compressed Oops的知识,大家可以自行查阅相关资料来加深理解。

如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,这块占用4个字节。因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。
(并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身,可参考对象的访问定位)

1.2实例数据(Instance Data)
实例数据部分就是成员变量的值,其中包含父类的成员变量和本类的成员变量。也就是说,除去静态变量和常量值放在方法区,非静态变量的值是随着对象存储在堆中的。
因为修改静态变量会反映到方法区中class的数据结构中,故而推测对象保存的是静态变量和常量的引用。

1.3对齐填充(Padding)
用于确保对象的总长度为8字节的整数倍。
HotSpot要求对象的总长度必须是8字节的整数倍。由于对象头一定是8字节的整数倍,但实例数据部分的长度是任意的。因此需要对齐补充字段确保整个对象的总长度为8的整数倍。

2、Java数据类型有哪些
基础数据类型(primitive type)
引用类型 (reference type)
2.1基础数据类型内存占用如下
2.2引用类型 内存占用如下:
引用类型跟基础数据类型不一样,除了对象本身之外,还存在一个指向它的引用(指针),指针占用的内存在64位虚拟机上8个字节,如果开启指针压缩是4个字节,默认是开启了的。

2.3字段重排序
为了更高效的使用内存,实例数据字段将会重排序。排序的优先级为: long = double > int = float > char = short > byte > boolean > object reference
如下所示的类

class FieldTest{
        byte a;
        int c;
        boolean d;
        long e;
        Object f;
    }
将会重排序为(开启CompressedOops选项):

   OFFSET  SIZE               TYPE DESCRIPTION            
         16     8               long FieldTest.e            
         24     4                int FieldTest.c            
         28     1               byte FieldTest.a            
         29     1            boolean FieldTest.d            
         30     2              (alignment/padding gap)
         32     8   java.lang.Object FieldTest.f
3、验证
讲完了上面的概念,我们可以去验证一下。
3.1有一个Fruit类继承了Object类,我们分别新建一个object和fruit,那他们分别占用多大的内存呢?

class Fruit extends Object {
     private int size;
}

Object object = new Object();
Fruit f = new Fruit();
先来看object对象,通过上面的知识,它的Markword是8个字节,kclass是4个字节, 加起来是12个字节,加上4个字节的对齐填充,所以它占用的空间是16个字节。
再来看fruit对象,同样的,它的Markword是8个字节,kclass是4个字节,但是它还有个size成员变量,int类型占4个字节,加起来刚好是16个字节,所以不需要对齐填充。

那该如何验证我们的结论呢?毕竟我们还是相信眼见为实!很幸运Jdk提供了一个工具jol-core可以让我们来分析对象头占用内存信息。具体使用参考
jol的使用也很简单:
打印头信息

    public static void main(String[] args) {
        System.out.print(ClassLayout.parseClass(Fruit.class).toPrintable());
        System.out.print(ClassLayout.parseClass(Object.class).toPrintable());
    }
输出结果

com.zzx.algorithm.tst.Fruit object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4    int Fruit.size                                N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到输出结果都是16 bytes,跟我们前面的分析结果一致。

3.2 除了类类型和接口类型的对象,Java中还有数组类型的对象,数组类型的对象除了上面表述的字段外,还有4个字节存储数组的长度(所以数组的最大长度是Integer.MAX)。所以一个数组对象占用的内存是 8 + 4 + 4 = 16个字节,当然这里不包括数组内成员的内存。
我们也运行验证一下。

    public static void main(String[] args) {
        String[] strArray = new String[0];
        System.out.println(ClassLayout.parseClass(strArray.getClass()).toPrintable());
    }
输出结果:

[Ljava.lang.String; object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0    16                    (object header)                           N/A
     16     0   java.lang.String String;.<elements>                        N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
输出结果object header的长度也是16,跟我们分析的一致。
3.3 接下来看对象的实例数据部分:
为了方便说明,我们新建一个Apple类继承上面的Fruit类

public class Apple extends Fruit {
    private int size;
    private String name;
    private Apple brother;
    private long create_time;
    
}
// 打印Apple的对象分布信息

System.out.println(ClassLayout.parseClass(Apple.class).toPrintable());
1
// 输出结果

com.zzx.algorithm.tst.Apple object internals:
 OFFSET  SIZE                          TYPE DESCRIPTION                               VALUE
      0    12                               (object header)                           N/A
     12     4                           int Fruit.size                                N/A
     16     8                          long Apple.create_time                         N/A
     24     4                           int Apple.size                                N/A
     28     4              java.lang.String Apple.name                                N/A
     32     4   com.zzx.algorithm.tst.Apple Apple.brother                             N/A
     36     4                               (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到Apple的对象头12个字节,然后分别是从Fruit类继承来的size属性(虽然Fruit的size是private的,还是会被继承,与Apple自身的size共存),还有自己定义的4个属性,基础数据类型直接分配,对象类型都是存的指针占4个字节(默认都是开启了指针压缩),最终是40个字节,所以我们new一个Apple对象,直接就会占用堆栈中40个字节的内存,清楚对象的内存分配,让我们在写代码时心中有数,应当时刻有内存优化的意识!
这里又引出了一个小知识点,上面其实已经标注出来了。

父类的私有成员变量是否会被子类继承?
答案当然是肯定的,我们上面分析的Apple类,父类Fruit有一个private类型的size成员变量,Apple自身也有一个size成员变量,它们能够共存。注意划重点了,类的成员变量的私有访问控制符private,只是编译器层面的限制,在实际内存中不论是私有的,还是公开的,都按规则存放在一起,对虚拟机来说并没有什么分别!

4、方法内部new的对象是在堆上还是栈上?
我们常规的认识是对象的分配是在堆上,栈上会有个引用指向该对象(即存储它的地址),到底是不是呢,我们来做个试验!
我们在循环内创建一亿个Apple对象,并记录循环的执行时间,前面已经算过1个Apple对象占用40个字节,总共需要4GB的空间。

public static void main(String[] args) {
     long startTime = System.currentTimeMillis();
     for (int i = 0; i < 100000000; i++) {
         newApple();
     }
     System.out.println("take time:" + (System.currentTimeMillis() - startTime) + "ms");
}

public static void newApple() {
     new Apple();
}
我们给JVM添加上-XX:+PrintGC运行配置,让编译器执行过程中输出GC的log日志
// 运行结果,没有输出任何gc的日志

take time:6ms
1
1亿个对象,6ms就分配完成,而且没有任何GC,显然如果对象在堆上分配的话是不可能的,其实上面的实例代码,Apple对象全部都是在栈上分配的,这里要提出一个概念指针逃逸,newApple方法中新建的对象Apple并没有在外部被使用,所以它被优化为在栈上分配,我们知道方法执行完成后该栈帧就会被清空,所以也就不会有GC。
我们可以设置虚拟机的运行参数来测试一下。
// 虚拟机关闭指针逃逸分析

-XX:-DoEscapeAnalysis
1
// 虚拟机关闭标量替换

-XX:-EliminateAllocations
1
在VM options里面添加上面二个参数,再运行一次

[GC (Allocation Failure)  236984K->440K(459776K), 0.0003751 secs]
[GC (Allocation Failure)  284600K->440K(516608K), 0.0004272 secs]
[GC (Allocation Failure)  341432K->440K(585216K), 0.0004835 secs]
[GC (Allocation Failure)  410040K->440K(667136K), 0.0004655 secs]
[GC (Allocation Failure)  491960K->440K(645632K), 0.0003837 secs]
[GC (Allocation Failure)  470456K->440K(625152K), 0.0003598 secs]

take time:5347ms
可以看到有很多GC的日志,而且运行的时间也比之前长了很多,因为这时候Apple对象的分配在堆上,而堆是所有线程共享的,所以分配的时候肯定有同步机制,而且触发了大量的gc,所以效率低很多。
总结一下: 虚拟机指针逃逸分析是默认开启的,对象不会逃逸的时候优先在栈上分配,否则在堆上分配。
到这里,关于“一个对象占多少内存?”这个问题,已经能回答的相当全面了。
————————————————
版权声明:本文为CSDN博主「小左01」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zzx410527/article/details/93646925

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值