【一文读懂】java对象占用多少内存

1,827 阅读6分钟

遇到问题

最近遇到一个批量处理的任务,处理过程中需要加载大量数据,其中有需要处理的数据,还有中间过程用到的一些缓存数据。虽然可以通过监控看到应用占用内存情况,但是还是需要知道因为这个任务中加载的数据和缓存占用了多少内存,根据实际情况做相应的调优。

那到底怎么才能知道总内存中有多少内存是由于这个批量任务导致的呢,当然我们可以根据基本数据类型占用字节数和总大小大概计算出来一个值,这个过程很麻烦,且不够精准。

解决问题的基础--基本类型及其占用空间大小

数据类型字节数位数封装类
byte18Byte
boolean18Boolean
char216Character
short216Short
int432Integer
float432Float
double864Double
long864Long

1M=1024字节 根据占用字节数就能计算占用内存大小。

如何查看运行中应用对象占用内存大小

jol简介

jol(JAVA OBJECT LAYOUT)是openjdk提供的查看内存布局的工具。

image.png

  1. markword 固定长度8byte,描述对象的identityhashcode,分代年龄,锁信息等
  2. classpoint 固定长度4byte, 指定该对象的class类对象;
  3. 基本变量:用于存放java八种基本类型成员变量,以4byte步长进行补齐,使用内存重排序优化空间;
  4. 引用变量:存放类的引用变量句柄,如String,Object;每个句柄大小4byte,java8开始默认开启UseCompressedOops指针压缩参数,不开启的话占8个字节;
  5. 补齐:对象大小必须是8byte的整数倍,用来补齐字节数;
  6. 数组长度:如果是数组,额外占用固定4byte存放数组长度;

jol官网

jol使用

pom.xml

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

基本类型 byte

byte b = 'a';
System.out.println(ClassLayout.parseInstance(b).toPrintable());

输出结果:

image.png

1.2行是markword占用8个字节
第3行 classpoint 占用4个字节
第4行byte类型占用一个字节
第5行,因为不是8的倍数,补齐3个字节。


基本类型 int

int i = 0;
System.out.println(ClassLayout.parseInstance(i).toPrintable());

输出结果:

image.png

1.2行是markword占用8个字节
第3行 classpoint 占用4个字节
第4行int类型占用4个字节
正好16个字节,8的倍数,不需要补位


基本类型 double

double d = 0;
System.out.println(ClassLayout.parseInstance(d).toPrintable());

输出结果:

image.png

1.2行是markword占用8个字节
第3行 classpoint 占用4个字节
第4行 补位行
第5行 double类型占用8个字节


数组 Double[]

Double[] ds = new Double[10];
System.out.println(ClassLayout.parseInstance(ds).toPrintable());

输出结果:

image.png

1.2行是markword占用8个字节
第3行 classpoint 占用4个字节
第4行 数组长度 4个字节
第5行 ???这里为啥不是8*10=80 个字节呢?
8的倍数,不需要补位

提出问题?

第5行 ???这里为啥是4*10=40不是8*10=80 个字节呢? 

我们把上面的Double 换成double基本类型看一下

数组 double[]

double[] dss = new double[10];
System.out.println(ClassLayout.parseInstance(dss).toPrintable());

输出结果: image.png 我们看到变成了8*10=80

回答问题?

当数组是基本类型时,是基本类型占用的字节数*数组大小
当数组是非基本类型时,是引用地址占用的字节数(4个字节)*数组大小

自己验证一下哦

引用对象如何计算呢

通过上面我们只能查看部分数据类型的对象占用的空间大小,真实生产环境大部分情况的对象引用不能计算到真实的占用

先直接上demo

直接上代码

package top.soft1010.java.base.inst;

import java.lang.instrument.Instrumentation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;

/**
 * Created by bjzhangjifu on 2021/9/23.
 */
public class SizeOfObject {

    static Instrumentation inst;

    public static void premain(String args, Instrumentation instP) {
        inst = instP;
    }

    /**
     * 直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、<br></br>
     * 引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小;<br></br>
     * 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小 <br></br>
     *
     * @param obj
     * @return
     */
    public static long sizeOf(Object obj) {
        return inst.getObjectSize(obj);
    }

    /**
     * 递归计算当前对象占用空间总大小,包括当前类和超类的实例字段大小以及实例字段引用对象大小
     *
     * @param objP
     * @return
     * @throws IllegalAccessException
     */
    public static long fullSizeOf(Object objP) throws IllegalAccessException {
        Set<Object> visited = new HashSet<Object>();
        Deque<Object> toBeQueue = new ArrayDeque<>();
        toBeQueue.add(objP);
        long size = 0L;
        while (toBeQueue.size() > 0) {
            Object obj = toBeQueue.poll();
            //sizeOf的时候已经计基本类型和引用的长度,包括数组
            size += skipObject(visited, obj) ? 0L : sizeOf(obj);
            Class<?> tmpObjClass = obj.getClass();
            if (tmpObjClass.isArray()) {
                //[I , [F 基本类型名字长度是2
                if (tmpObjClass.getName().length() > 2) {
                    for (int i = 0, len = Array.getLength(obj); i < len; i++) {
                        Object tmp = Array.get(obj, i);
                        if (tmp != null) {
                            //非基本类型需要深度遍历其对象
                            toBeQueue.add(Array.get(obj, i));
                        }
                    }
                }
            } else {
                while (tmpObjClass != null) {
                    Field[] fields = tmpObjClass.getDeclaredFields();
                    for (Field field : fields) {
                        if (Modifier.isStatic(field.getModifiers())   //静态不计
                                || field.getType().isPrimitive()) {    //基本类型不重复计
                            continue;
                        }

                        field.setAccessible(true);
                        Object fieldValue = field.get(obj);
                        if (fieldValue == null) {
                            continue;
                        }
                        toBeQueue.add(fieldValue);
                    }
                    tmpObjClass = tmpObjClass.getSuperclass();
                }
            }
        }
        return size;
    }

    /**
     * String.intern的对象不计;计算过的不计,也避免死循环
     *
     * @param visited
     * @param obj
     * @return
     */
    static boolean skipObject(Set<Object> visited, Object obj) {
        if (obj instanceof String && obj == ((String) obj).intern()) {
            return true;
        }
        return visited.contains(obj);
    }
}

打包jar

<plugin>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.4</version>
    <configuration>
        <finalName>inst</finalName>
        <archive>
            <manifestEntries>
                <Premain-class>top.soft1010.java.base.inst.SizeOfObject</Premain-class>
                <Boot-Class-Path></Boot-Class-Path>
                <Can-Redefine-Classes>false</Can-Redefine-Classes>
            </manifestEntries>
            <addMavenDescriptor>false</addMavenDescriptor>
        </archive>
    </configuration>
</plugin>

执行命令

java -javaagent:../../target/inst.jar top.soft1010.java.base.inst.Test

基本类型 int

int i = 0;
System.out.println(SizeOfObject.fullSizeOf(i));

输出结果:16


基本类型 double

double j = 0;
System.out.println(SizeOfObject.fullSizeOf(j));

输出结果:24


数组

 double[] ds = new double[10];
 System.out.println(SizeOfObject.fullSizeOf(ds));

结果:8个字节markword+4个字节classpoint+4个字节数组长度+double占用8个自己*数组长度10=96

 Double[] dds = new Double[10];
 System.out.println(SizeOfObject.fullSizeOf(dds));

结果:8个字节markword+4个字节classpoint+4个字节数组长度+Double引用占用4个字节*数组长度10=56

如果我对其中的一个Double赋值 ,结果是多少呢?我们通过实际执行发现是80

80怎么来的呢?
8个字节markword+4个字节classpoint+4个字节数组长度+Double引用占用4个字节*数组长度10=56 还有24是怎么来的呢,其实就是一个Double对象占用的空间
8个字节markword+4个字节classpoint+8个字节double占用 还有4个补位 = 24


Instrumentation简介

  • 利用java.lang.instrument(容器类) 做动态 Instrumentation(执行容器) 是 Java SE 5 的新特性。
  • 使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。
  • 这个功能为虚拟机监控提供了支撑

Instrumentation原理&步骤

开发者可以让 Instrumentation 代理在 main 函数运行前执行

1、编写 premain 函数

public static void premain(String agentArgs,Instrumentation inst)public static void premain(String agentArgs);

2、jar 文件打包

3、java -javaagent:jar文件的位置 [= 传入 premain 的参数, ]

Instrumentation介绍