一个 Java 对象到底占用多少内存


前言

这个问题我相信对于很多有经验的Java程序猿来说也是一个比较难回答的问题,说实话,我刚开始被人问到这个问题的时候也是一脸懵逼,首先脑子里很难一下想起或者猜测这个内存占用值到底是多少,因此着手去查之,果然有对应文章阐述这个问题。下面就这个问题展开探索,学习了也便于我们日后进行内存优化相关的工作不是?

环境基础

Java当中数据类型有哪些

从大的角度可以分为两类:

基础数据类型又分为哪些

基础数据类型所占用的内存空间大小是确定的。

基础数据类型在哪里存储分配

public class Pig {
    //堆上分配
    private String name;
    //方法区分配
    private static int age;
    private int getPigWeight() {
        //栈上分配
        int wieght = 100;
        return wieght;
    }
}

引用类型的存储大小和存储位置

引用类型除对象本身之外,还有一个指向这个对象的指针,在64位虚拟机环境下,这个指针通常占用4个字节,因为默认开启了指针压缩,如果不开启指针压缩的话就占用8个字节。存储位置还是看下面代码吧。

public class Pig {
    //'name'和它指向的对象都存储在堆上
    private String name = new String();
    //'color'和它所指向的对象都存储在方法区上
    private static String color = new String();
    private void show() {
        //temp则在栈上,它所指向的对象可能在堆上也可能在栈上
        String temp = new String();
        System.out.println(temp);
    }
}

Java中对象到底占多大内存

Java中对象分为三种类型:

对象由三个部分组成:

对象头

对象头同样由两部分组成:

Markword

按照官方文档对Markword的定义,Markword由64位组成,有下面四种情况:

其中:

我们主要关注 normal object, 这种类型的 Object 的 Markword 一共是 8 个字节,由以上定义,我们可以知道 normal object 的Markword组成是这样的:

小扩展

什么时候normal object会转变为biased object呢?

// 无其他线程竞争的情况下,由normal object变为biased object
synchronized(object)

类指针

类指针存储的是该对象所属的类在方法区的地址, Jvm 默认对指针进行了压缩,用 4 个字节存储,如果不压缩就是 8 个字节,主要是出于提高内存分配效率的考虑。

分析个实例

class Animal extends Object {
     private int size;
}

Object object = new Object();
Animal animal = new Aniaml();

分析检验

import org.openjdk.jol.info.ClassLayout;

public class Animal extends Object {
    private int size;
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(Object.class).toPrintable());
        System.out.println(ClassLayout.parseClass(Animal.class).toPrintable());
    }

}

这里用到了一个包:

<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.10</version>
</dependency>

输出结果

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

com.admin.hellotest.Animal object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4    int Animal.size                               N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可以看出和我们的分析结果是吻合的。类对象和接口对象和我们上面分析的方法一致,数组对象稍微不同,数组类型的对象除了上面表述的字段外,还有 4 个字节存储数组的长度(所以数组的最大长度是 Integer.MAX)。所以一个数组对象占用的内存是 8 + 4 + 4 = 16 个字节,当然这里不包括数组内成员的内存。

再看个例子

public class Dog extends Animal {
    private String name;
    private Dog brother;
    private long createTime;

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(Dog.class).toPrintable());

    }
}

输出结果:

com.admin.hellotest.Dog object internals:
 OFFSET  SIZE                      TYPE DESCRIPTION                               VALUE
      0    12                           (object header)                           N/A
     12     4                       int Animal.size                               N/A
     16     8                      long Dog.createTime                            N/A
     24     4          java.lang.String Dog.name                                  N/A
     28     4   com.admin.hellotest.Dog Dog.brother                               N/A
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

对象真的全部分配在堆里吗

来看这样一段代码

public class Dog extends Animal {
    private String name;
    private Dog brother;
    private long createTime;

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            newDog();
        }
        System.out.println("消耗时间:" + (System.currentTimeMillis() - start) + " ms");
    }

    private static void newDog() {
        new Dog();
    }
}

那么按照前面的分析,一个Dog对象将占用32个字节,这里创建1亿个,估算一下,大概就是3G内存占用,加上虚拟机选项配置:-XX:+PrintGC来看看输出结果。

//没有GC输出日志
消耗时间4 ms

Process finished with exit code 0

为什么会这样呢?主要是由于Dog对象并不是在堆上分配的,而是在栈上分配的,专业名词叫做指针逃逸,newDog()方法创建的对象并没有被外部使用,所以被优化为栈上分配,可以加上下面两个虚拟机参数禁止指针逃逸

// 虚拟机关闭指针逃逸分析
-XX:-DoEscapeAnalysis
// 虚拟机关闭标量替换
-XX:-EliminateAllocations

好了,以上就是今天的全部内容了,大家get了多少姿势呢?