行百里er 行百里er
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)

行百里er

Java程序员一枚
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)
  • JVM

  • Java基础

    • 【优雅的避坑】从验证码生成代码的优化到 JVM 栈和堆
    • 【优雅的避坑】HashMap正确的避免扩容带来的额外开销
    • 【优雅的避坑】不要轻易使用==比较两个Integer的值
    • 【优雅的避坑】为什么0.1+0.2不等于0.3了!?
    • 【优雅的避坑】不安全!别再共享SimpleDateFormat了
    • new Object在内存中占多少字节?
      • Java Agent技术
      • 制作一个探测Object大小的Agent
      • 使用Agent探测Object大小
        • 对象在内存中的布局
        • 计算new Object()占用的字节数
    • Java 8之Lambda表达式的写法套路
    • Java 8 Stream API可以怎么玩?
    • Java最强大的技术之一:反射
    • 从一道面试题进入Java并发新机制---JUC
  • Java
  • Java基础
行百里er
2020-11-04
目录

new Object在内存中占多少字节?

作者:行百里er

博客:https://chendapeng.cn (opens new window)

提示

这里是 行百里er 的博客:行百里者半九十,凡事善始善终,吾将上下而求索!


如何知道一个对象在内存中的大小呢?C语言有个叫sizeOf的东西,很方便就能知道对象大小。但是Java没有这样的东西啊,不慌,Java本身有一个Agent技术。

# Java Agent技术

Agent是个是个什么机制呢?

jvm虚拟机,要load一个class到内存,在load内存的过程中可以加一个Agent代理,这个代理可以截获这些class文件(001 010等二进制码),并可以对他做任意修改,当然也就可以读取到整个Object的大小。

可以参考这篇文章了解更多 Java 动态调试技术原理及实践 (opens new window)

# 制作一个探测Object大小的Agent

新建一个项目ObjectSizeAgent,并制作成jar包。

  1. 写一个agent类,格式一般比较固定
public class ObjectSizeAgent {

    //Java内部字节码处理调试用的是Instrumentation
    //所以在使用代理装到我们jvm的时候可以截获这个Instrumentation
    private static Instrumentation inst;

    /**
     * 必须要有premain函数
     * 参数固定
     * 第二个参数就是Instrumentation,这个是虚拟机调用的,会自动帮我们初始化Instrumentation
     * 在这里通过给自己定义的成员变量赋值,赋完值就能拿到Instrumentation
     * @param agentArgs
     * @param _inst
     */
    public static void premain (String agentArgs, Instrumentation _inst) {
        inst = _inst;
    }

    /**
     * 在premain里拿到Instrumentation后,可以调用getObjectSize获取对象大小
     * @param o
     * @return
     */
    public static long sizeOf (Object o) {
        return inst.getObjectSize(o);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  1. 制作jar包

不同的IDE工具打jar的方式不同。

我在IDEA 2020.2中打jar的过程如下:

File->Project Structure

选择Artifacts

输入jar名称,创建ManiFest,存放在src目录下

创建完成后,目录如下:

文件MANIFEST.MF的内容:

Manifest-Version: 1.0
Premain-Class: pers.xblzer.tryout.agent.ObjectSizeAgent
1
2

最后,打成jar包:

# 使用Agent探测Object大小

在实验项目中导入制作好的jar包,maven导入或是普通jar包导入都可以。

PS:我还是写一下制作成本地maven jar包的流程吧。

  1. 将制作的jar包拷贝到一个地方,D:\works\3rd_jar,没别的,就是为了方便
  2. 打包
mvn install:install-file -DgroupId=pers.xblzer.tryout -DartifactId=ObjectSizeAgent -Dversion=1.0 -Dpackaging=jar -Dfile=D:\works\3rd_jar\ObjectSizeAgent.jar -DgeneratePom=true -DcreateChecksum=true -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true
1
  1. 其他项目引用:
<dependency>
    <groupId>pers.xblzer.tryout</groupId>
    <artifactId>ObjectSizeAgent</artifactId>
    <version>1.0</version>
</dependency>
1
2
3
4
5

实验代码:

public class TestObjectSize {

    public static void main(String[] args) {
//        // 配置 XX:+UseCompressedClassPointer XX:+UseCompressedOops
        // 16字节 = markword 8 + classpointer 4 + padding 4
        System.out.println("new Object size:" + ObjectSizeAgent.sizeOf(new Object()));
        // 16字节 = markword 8 + classpointer 4 + 数组长度 4 + padding 0(前面已经是8的倍数了)
        System.out.println("new array size:" + ObjectSizeAgent.sizeOf(new int[]{}));
        // 32 (最终必须满足8的倍数)
        System.out.println("new a common class size:" + ObjectSizeAgent.sizeOf(new P()));

        // 配置 XX:-UseCompressedClassPointer XX:+UseCompressedOops
        // 16字节 = markword 8 + classpointer 8 + padding 0
//        System.out.println("new Object size:" + ObjectSizeAgent.sizeOf(new Object()));
//        // 24字节 = markword 8 + classpointer 8 + 数组长度 4 + padding 4(补齐至8的倍数)
//        System.out.println("new array size:" + ObjectSizeAgent.sizeOf(new int[]{}));
//        // 40 (最终必须满足8的倍数)
//        System.out.println("new a common class size:" + ObjectSizeAgent.sizeOf(new P()));
    }

    private static class P {
        // markword 8
        // ClassPinter 4 (-UseCompressedClassPointer时 为8;+UseCompressedClassPointer时 为4)

        // 4
        int id;
        // Oops 4
        String name;// 这是一个引用 +UseCompressedOops时 为8  -UseCompressedOops时 为4
        // 1
        byte b1;
        // 1
        byte b2;
        //Oops 4
        Object o;
        // 8
        long i;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

运行时添加我们制作的agent:运行时添加运行参数

-javaagent:D:\works\3rd_jar\ObjectSizeAgent.jar
1

先运行一下看结果,运行环境jdk 1.8,jvm参数是默认:

从运行结果看

new Object()占用16字节;

new int[]{}占用16字节;

new P()自己定义的里面含有各种数据类型属性的类占用40字节。

结果出来了,我们来分析一下,他们为什么占用那么多字节。

# 对象在内存中的布局

作为对象的内存布局来讲分为两种,一种是普通对象,一种是数组对象。

普通对象内存各部分字节占用分配情况:

  1. 对象头,在Hotspot里面称为markword,它的长度是8byte
  2. ClassPointer指针(比如Object.class)
  • -XX:+UseCompressedClassPointers的情况为4byte,默认
  • -XX:-UseCompressedClassPointers的情况为8byte
  1. 实例数据

其中引用类型字节占用情况:

  • -XX:+UseCompressedOops 4byte,默认
  • -XX:-UseCompressedOops 8byte

非引用类型字节占用情况:

类型 存储 取值范围
int 4byte -2^31 ~ 2^31 - 1
short 2byte -2^15 ~ 2^15 - 1
long 8byte (-2)^63 ~ 2^63 - 1
byte 1byte -128 ~ 127
float 4byte
double 8byte
boolean 1byte
char 2byte

Hotspot实现的JVM开启内存压缩的规则(64位机器):

  • 4G以下,直接砍掉高32位
  • 4G~32G,默认开启内存压缩
  • 32G以上,压缩无效,使用64位

所以,内存并不是越大越好。

  1. Padding对齐

这个对齐,对齐的是8的倍数。最为64位机器来说,它是按照块来读的,不是按照字节来读,每一块存的都是8的倍数个字节,因此它有一个对齐机制。

数组对象内存各部分字节占用分配情况:

  1. 对象头 markword 8字节
  2. ClassPointer指针,同普通对象,压缩4字节,不压缩8字节
  3. 数组长度 4字节
  4. 数组数据
  5. 对齐 8的倍数

# 计算new Object()占用的字节数

基于上面的分析,我们来验证一下前面写的程序计算结果:

对于new Object()

  • 首先,markword占8字节
  • ClassPointer:我的机器内存是8G,JVM默认是开启了内存压缩规则的,所以这里ClassPointer会占用4字节
  • 实例数据:我只是new了一个Object(),没有任何的引用类型和费用用类型,这部分没有占用字节
  • Padding对齐:前面8+4+0=12字节,因为要满足8的倍数,所以这里需要补齐至16字节

所以,new Object()占用16字节。

对于new int[]{}

  • markword: 8字节
  • ClassPointer:4字节
  • 数组长度:4字节
  • 数组数据:0字节
  • Padding对齐:前面8+4+4+0=16,已经是8的倍数了,这里不需要对齐

因此,new int[]{}占用16字节。

对于我自己定义的new P()

private static class P {
    // markword 8
    // ClassPinter 4 (-UseCompressedClassPointer时 为8;+UseCompressedClassPointer时 为4)

    // 4
    int id;
    // Oops 4
    String name;// 这是一个引用 +UseCompressedOops时 为8  -UseCompressedOops时 为4
    // 1
    byte b1;
    // 1
    byte b2;
    //Oops 4
    Object o;
    // 8
    long i;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • markword 8字节
  • ClassPointer 4字节
  • 实例数据:
    • int 4字节
    • String 引用类型 4字节
    • 两个byte 1*2=2字节
    • Object o 引用类型 4字节
    • long 8字节
  • Padding对齐:先算一下是否满足8的倍数 8+4+4+4+2+4+8=34,需要补齐至8的倍数,补至40

因此,本例中new P()占用40字节。

大家可以根据jvm参数来调试这个程序,会得到不同的结果。主要是下面两个参数:

-XX:(+/-)UseCompressedClassPointers
-XX:(+/-))UseCompressedOops
1
2

Over。以上。


首发公众号 行百里er ,欢迎老铁们关注阅读指正。

#Java
上次更新: 2022/10/04, 18:14:30
【优雅的避坑】不安全!别再共享SimpleDateFormat了
Java 8之Lambda表达式的写法套路

← 【优雅的避坑】不安全!别再共享SimpleDateFormat了 Java 8之Lambda表达式的写法套路→

最近更新
01
重要数据不能丢!MySQL数据库定期备份保驾护航!
05-22
02
分布式事务解决方案之 Seata(二):Seata AT 模式
09-09
03
Seata 番外篇:使用 docker-compose 部署 Seata Server(TC)及 K8S 部署 Seata 高可用
09-05
更多文章>
Theme by Vdoing | Copyright © 2020-2023 行百里er | MIT License | 豫ICP备2022020385号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式