你写的Java对象究竟占多少内存?

memory

概述

Java 做为一个面向对象语言,给咱们带来了多态,继承,封装等特性,使得咱们能够利用这些特性很轻松的就能构建出易于扩展,易于维护的代码。做为一个Javaer,每天搞“对象”,那你写的对象究竟占用了多少内存呢?咱们来看看你的“对象”是如何“败家”的。html

本文环境:jdk1.8_64java

Java 对象头内存模型

想要了解Java对象究竟占用多少内存一定先要了解一个Java 对象的内存模型是怎么样的?因为咱们的虚拟机是分为32位和64位,那确定它们的模型也是有区别的,下面我列出列32位虚拟机和64位虚拟机下的Java对象头内存模型。git

32位虚拟机
32位虚拟机

64位虚拟机
64位虚拟机

64位带指针压缩
64位带指针压缩

由于笔者的本地环境是jdk1.8,64位虚拟机,这里我以64位虚拟机(开启指针压缩)来分析,由于默认状况下,jdk1.8 在64位虚拟机默认开启指针压缩。程序员

Java 对象头主要包括两部分,第一部分就是 Mark Word,这也是 Java 锁实现原理中重要的一环,另一部分是 Klass Wordgithub

Klass Word 这里实际上是虚拟机设计的一个oop-klass model模型,这里的OOP是指Ordinary Object Pointer(普通对象指针),看起来像个指针其实是藏在指针里的对象。而 klass 则包含 元数据和方法信息,用来描述 Java 类。它在64位虚拟机开启压缩指针的环境下占用 32bits 空间。bash

Mark Word 是咱们分析的重点,这里也会设计到锁的相关知识。Mark Word 在64位虚拟机环境下占用 64bits 空间。整个Mark Word的分配有几种状况:maven

  1. 未锁定(Normal): 哈希码(identity_hashcode)占用31bits,分代年龄(age)占用4 bits,偏向模式(biased_lock)占用1 bits,锁标记(lock)占用2 bits,剩余26bits 未使用(也就是全为0)
  2. 可偏向(Biased): 线程id 占54bits,epoch 占2 bits,分代年龄(age)占用4 bits,偏向模式(biased_lock)占用1 bits,锁标记(lock)占用2 bits,剩余 1bit 未使用。
  3. 轻量锁定(Lightweight Locked): 锁指针占用62bits,锁标记(lock)占用2 bits。
  4. 重量级锁定(Heavyweight Locked):锁指针占用62bits,锁标记(lock)占用2 bits。
  5. GC 标记:标记位占2bits,其他为空(也就是填充0)

以上就是咱们对Java对象头内存模型的解析,只要是Java对象,那么就确定会包括对象头,也就是说这部份内存占用是避免不了的。因此,在笔者64位虚拟机,Jdk1.8(开启了指针压缩)的环境下,任何一个对象,啥也不作,只要声明一个类,那么它的内存占用就至少是96bits,也就是至少12字节。ide

验证模型

咱们来写点代码来验证一下上述的内存模型,这里推荐openjdk的jol工具,它能够帮助你查看对象内存的占用状况。工具

首先添加maven依赖oop

<dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.10</version>
        </dependency>
复制代码

咱们先来看看,若是只是新建一个普通的类,什么属性也不添加,占用的空间是多少?

/**
 * @description:
 * @author: luozhou
 * @create: 2020-02-26 10:00
 **/
public class NullObject {

}
复制代码

按照咱们以前的Java对象内存模型分析,一个空对象,那就是只有一个对象头部,在指针压缩的条件下会占用 96 bits,也就是12bytes。

运行工具查看空间占用

public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new NullObject()).toPrintable());
    }
复制代码

上面这行代码会解析你新建一个NullObject对象,占用了多少内存。咱们执行看看结果如何:

内存占用
内存占用

这里咱们发现结果显示:Instance size:16 bytes,结果就是16字节,与咱们以前预测的12字节不同,为何会这样呢?咱们看到上图中有3行 object header,每一个占用4字节,因此头部就是12字节,这里和咱们的计算是一致的,最后一行是虚拟机填充的4字节,那为何虚拟机要填充4个字节呢?

什么是内存对齐

想要知道为何虚拟机要填充4个字节,咱们须要了解什么是内存对齐?

咱们程序员看内存是这样的:

上图表示一个坑一个萝卜的内存读取方式。但实际上 CPU 并不会以一个一个字节去读取和写入内存。相反 CPU 读取内存是一块一块读取的,块的大小能够为 二、四、六、八、16 字节等大小。块大小咱们称其为内存访问粒度。以下图:

假设一个32位平台的 CPU,那它就会以4字节为粒度去读取内存块。那为何须要内存对齐呢?主要有两个缘由:

  • 平台(移植性)缘由:不是全部的硬件平台都可以访问任意地址上的任意数据。例如:特定的硬件平台只容许在特定地址获取特定类型的数据,不然会致使异常状况。
  • 性能缘由:若访问未对齐的内存,将会致使 CPU 进行两次内存访问,而且要花费额外的时钟周期来处理对齐及运算。而自己就对齐的内存仅须要一次访问就能够完成读取动做。

我用图例来讲明 CPU 访问非内存对齐的过程:

在上图中,假设CPU 是一次读取4字节,在这个连续的8字节的内存空间中,若是个人数据没有对齐,存储的内存块在地址1,2,3,4中,那CPU的读取就会须要进行两次读取,另外还有额外的计算操做:

  1. CPU 首次读取未对齐地址的第一个内存块,读取 0-3 字节。并移除不须要的字节 0。
  2. CPU 再次读取未对齐地址的第二个内存块,读取 4-7 字节。并移除不须要的字节 五、六、7 字节。
  3. 合并 1-4 字节的数据。
  4. 合并后放入寄存器。

因此,没有进行内存对齐就会致使CPU进行额外的读取操做,而且须要额外的计算。若是作了内存对齐,CPU能够直接从地址0开始读取,一次就读取到想要的数据,不须要进行额外读取操做和运算操做,节省了运行时间。咱们用了空间换时间,这就是为何咱们须要内存对齐。

回到Java空对象填充了4个字节的问题,由于原字节头是12字节,64位机器下,内存对齐的话就是128位,也就是16字节,因此咱们还须要填充4个字节。

非空对象占用内存计算

咱们知道了一个空对象是占用16字节,那么一个非空对象究竟占用多少字节呢?咱们仍是写一个普通类来验证下:

public class TestNotNull {
    private NullObject nullObject=new NullObject();
    private int a;
}
复制代码

这个演示类中引入了别的对象,咱们知道int类型是占用4个字节,NullObject对象占用16字节,对象头占12字节,还有一个很重要的状况 NullObject在当前这个类中是一个引用,因此不会存真正的对象,而只存引用地址,引用地址占4字节,因此总共就是12+4+4=20字节,内存对齐后就是24字节。咱们来验证下是否是这个结果:

public static void main(String[] args) {
        //打印对象内存占用
        System.out.println(ClassLayout.parseInstance(new TestNotNull()).toPrintable());
        System.out.println("=========================");
        //输出对象相关全部内存占用
        System.out.println(GraphLayout.parseInstance(new TestNotNull()).toPrintable());
        System.out.println("=========================");
        //输出内存占用统计
        System.out.println(GraphLayout.parseInstance(new TestNotNull()).toFootprint());
    }
复制代码

结果以下:

咱们能够看到TestNotNull的类占用空间是24字节,其中头部占用12字节,变量aint类型,占用4字节,变量nullObject是引用,占用了4字节,最后填充了4个字节,总共是24个字节,与咱们以前的预测一致。可是,由于咱们实例化了NullObject,这个对象一会存在于内存中,因此咱们还须要加上这个对象的内存占用16字节,那总共就是24bytes+16bytes=40bytes。咱们图中最后的统计打印结果也是40字节,因此咱们的分析正确。

这也是如何分析一个对象真正的占用多少内存的思路,根据这个思路加上openJDK的jol工具就能够基本的掌握本身写的“对象”究竟败家了你多少内存。

总结

本文我主要讲述了如何分析一个Java对象究竟占用多少内存空间,主要总结点以下:

  1. Java对象头部内存模型在32位虚拟机和64位虚拟机是不同的,64位虚拟机又分为开启指针压缩和不开启指针压缩两种对象头模型,因此总共有3种对象头模型。
  2. 内存对齐主要是由于平台的缘由和性能的缘由,本文主要解析的是性能方面的缘由。
  3. 空对象的内存占用计算注意要计算内存对齐,非空对象的内存计算注意加上引用内存占用和原实例对象的空间占用。

参考

1.cr.openjdk.java.net/~lfoltan/bu…

2.gist.github.com/arturmkrtch…

3.weekly-geekly.github.io/articles/44…

4.developer.ibm.com/articles/pa…

相关文章
相关标签/搜索