Java对象大小计算与MAT内存泄露分析

文章目录

  • JVM内存布局
    • 对象头
    • 实例数据
    • 对齐填充
  • 计算实例
    • 数组占用内存大小
    • String占用内存大小
    • 对象占用内存计算
  • 使用jmap的方式查看
  • Instrumentation计算对象内存方式
  • MAT内存分析示例

JVM内存布局

一个对象主要包含下面3个部分:

  1. 对象头(Header)
  2. 实例数据(Instance Data)
  3. 对齐填充(Padding)

对象头

32位jvm对象头:8字节

64位jvm对象头:

  1. 16字节(不开启指针压缩情况,-XX:-UseCompressedOops)
  2. 12字节(开启指针压缩,-XX:+UseCompressedOops)

数组对象对象头:

  1. 24字节(不开启指针压缩情况,-XX:-UseCompressedOops)
  2. 16字节(开启指针压缩,-XX:+UseCompressedOops)

对象头也包含2部分数据:

  1. MarkWord:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
  2. KlassPointer:指向它的类元数据的指针,JVM通过该指针来确定这个对象是哪个类的实例

实例数据

数据类型占用字节
boolean1
byte1
short2
char2
int4
float4
long8
double8
引用类型32位JVM4
引用类型64位JVM8
引用类型64位JVM开启指针压缩4

对齐填充

JVM按8字节对齐,不足8字节要填充(Padding)到8字节的整数倍。

填充字节数很容易计算:padding = 8 - ((对象头 + 实例数据) % 8)

计算实例

下面的计算实例都是在64位JVM开启指针压缩下的情况。

怎样计算对象占用内存大小呢?后面介绍2中方式:

  1. 使用Java8的jdk.nashorn.internal.ir.debug.ObjectSizeCalculator类的getObjectSize方法
  2. 使用ava.lang.instrument.Instrumentation类的getObjectSize方法(不能直接调用,得使用-javaagent方式,Instrumentation有JVM注入)

数组占用内存大小

数组计算:

MarkWord + KlassPointer + 数组长度 + 实例数据(数组长度*数组元数据大小) + 补齐填充

  • 开启指针压缩:MarkWord(8字节) + KlassPointer(4字节) + 数组长度(4字节) + 0*4 + 补齐 0 = 16
  • 关闭指针压缩:MarkWord(8字节) + KlassPointer(8字节) + 数组长度(4字节) + 0*8 + 补齐 0 = 24

默认是开启指针压缩,可以通过下面参数来关闭:

# 关闭指针压缩
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops

# 查看最终生效的参数
-XX:+PrintFlagsFinal

# 查看默认的初始参数
-XX:+PrintFlagsInitial

相信,有亲子动手试过的朋友会发现,Java8的ObjectSizeCalculator类的getObjectSize方法计算出来的值都是和开启指针压缩一致。

刚开始,我以为关闭指针压缩没有生效,找了半天原因,最后使用Instrumentation的getObjectSize能对上,参数生效了,应该是ObjectSizeCalculator的问题。

Instrumentation方式稍微麻烦一些,放在后面介绍。

@Test
public void memoryArray() {
    int[] base = new int[0];
    System.out.println("int[0]占用内存大小:" + ObjectSizeCalculator.getObjectSize(base));
    int[] a = new int[1];
    System.out.println("int[1]占用内存大小:" + ObjectSizeCalculator.getObjectSize(a));
    int[] b = new int[2];
    System.out.println("int[2]占用内存大小:" + ObjectSizeCalculator.getObjectSize(b));
    int[] c = new int[3];
    System.out.println("int[3]占用内存大小:" + ObjectSizeCalculator.getObjectSize(c));
    Integer[] d = new Integer[2];
    System.out.println("Integer[2]占用内存大小:" + ObjectSizeCalculator.getObjectSize(d));
    Integer[] e = new Integer[3];
    System.out.println("Integer[3]占用内存大小:" + ObjectSizeCalculator.getObjectSize(e));
}

结果如下:

int[0]占用内存大小:16
int[1]占用内存大小:24
int[2]占用内存大小:24
int[3]占用内存大小:32
Integer[2]占用内存大小:24
Integer[3]占用内存大小:32

计算逻辑如下:

int[1] = 数组对象头16字节 + 1个int类型4字节 + 4个字节的padding = 24字节
int[2] = 数组对象头16字节 + 2个int类型4字节(8字节) = 24字节
int[3] = 数组对象头16字节 + 3个int类型4字节(12字节) + 4个字节的padding = 32字节

我们再看一个容易出错的char数组:

public static void memoryCharArray() {
    char[] base = new char[0];
    System.out.println("int[0]占用内存大小:" + ObjectSizeCalculator.getObjectSize(base));
    char[] a = new char[1];
    System.out.println("int[1]占用内存大小:" + ObjectSizeCalculator.getObjectSize(a));
    char[] b = new char[2];
    System.out.println("int[2]占用内存大小:" + ObjectSizeCalculator.getObjectSize(b));
    char[] c = new char[4];
    System.out.println("int[4]占用内存大小:" + ObjectSizeCalculator.getObjectSize(c));
    char[] d = new char[5];
    System.out.println("int[5]占用内存大小:" + ObjectSizeCalculator.getObjectSize(d));
}
int[0]占用内存大小:16
int[1]占用内存大小:24
int[2]占用内存大小:24
int[4]占用内存大小:24
int[5]占用内存大小:32

注意:在Java中char占用2个字节,而不是1个字节(从\u0000到\uffff)

计算逻辑如下:

int[1] = 数组对象头16字节 + 1个char类型2字节 + 6个字节的padding = 24字节
int[4] = 数组对象头16字节 + 4个char类型2字节(8字节) = 24字节
int[5] = 数组对象头16字节 + 5个char类型2字节(10字节) + 6个字节的padding = 32字节

String占用内存大小

public static void memoryString() {
    System.out.println("new String()占用字节:" + ObjectSizeCalculator.getObjectSize(new String()));
    System.out.println("new String(\"a\")占用字节:" + ObjectSizeCalculator.getObjectSize(new String("a")));
    System.out.println("a占用字节:" + ObjectSizeCalculator.getObjectSize("a"));
    System.out.println("ab占用字节:" + ObjectSizeCalculator.getObjectSize("ab"));
    System.out.println("abc占用字节:" + ObjectSizeCalculator.getObjectSize("abc"));
    System.out.println("abcd占用字节:" + ObjectSizeCalculator.getObjectSize("abcd"));
    System.out.println("abcde占用字节:" + ObjectSizeCalculator.getObjectSize("abcde"));
}

结果如下:

new String()占用字节:40
new String("a")占用字节:48
a占用字节:48
ab占用字节:48
abc占用字节:48
abcd占用字节:48
abcde占用字节:56

空字符串的时候,String占用内存大小为40字节,怎么算的呢?

String有2个非static的成员变量:

  1. private final char value[]

  2. private int hash

  3. value内存大小 = 对象头16字节 + 对齐0 = 16字节

  4. String内存大小 = value内存大小16字节 + 对象头12字节 + int 4字节(hash) + 数组value引用4字节 + 对齐4字节= 40字节

“abcd”:

  1. value内存大小 = 对象头16字节 + 数组长度4 * 2字节 = 24字节
  2. String内存大小 = value内存大小24字节 + 对象头16字节 + int 4字节 + 数组value引用4字节 = 48字节

“abcde”:

  1. value内存大小 = 对象头16字节 + 数组长度5 * 2字节 + 6字节padding= 32字节
  2. String内存大小 = value内存大小32字节 + 对象头16字节 + int 4字节 + 数组value引用4字节 = 56字节

对象占用内存计算

public static void memoryCustomObject(){
    System.out.println("MemoryCustomObject占用字节数:"
            + ObjectSizeCalculator.getObjectSize(new MemoryCustomObject()));
}


private static class MemoryCustomObject {
    String s = new String();
    int i = 0;
}

计算逻辑如下:

前面我们知道空字符串占用40字节 + s引用4字节 + i变量4字节 + MemoryCustomObject对象都12字节 = 60字节

60字节不是8的倍数,再加4字节padding,最终64字节。

如果我们把MemoryCustomObject换成下面这样,结果是多少呢?

private static class MemoryCustomObject {
    String s;
    int i = 0;
}

答案是24字节。

计算逻辑如下:

MemoryCustomObject对象头12字节 + s引用4字节 + i变量4字节 = 20字节 + 4字节padding = 24字节

使用jmap的方式查看

jps
jps -l

jps

jmap -histo 6380
jmap -histo 6380 | findstr "MemoryCustomObject"

jmap

jmap -histo输出说明:

  1. #instances表示实例数量
  2. #bytes实例总的字节数
  3. class name实例的类名
数据类型class name
byteB
charC
intI
longJ
booleanZ
doubleD
floatF
shortS
类接口全限定名,例如java.lang.Class
类接口数组Lclassname;例如[Ljava.lang.Class;

jmap的class name中[表示数组,[[表示二维数组,以此类推。

例如[B就表示一维byte数组

jmap-find

我们可以看到100个MemoryCustomObject对象总大小是3200字节,每个32字节,为什么和我们前面计算的对不上呢?

还是因为我们添加了-XX:-UseCompressedClassPointers -XX:-UseCompressedOops参数,关闭了指针压缩,ObjectSizeCalculator计算得还是开启指针压缩的值。

还是要用我们后面介绍的Instrumentation方式MemoryPreMain.sizeOf(new MemoryCustomObject())才能计算关闭指针压缩的值。

Instrumentation计算对象内存方式

size agent

首先,不知道怎么打javaagent包的可以参考:javaagent与attach

用后面的MemoryPreMain类创建一个maven工程,打一个jar包,在测试Main工程中引用这个包,并且指定运行vm参数:

-XX:-UseCompressedClassPointers -XX:-UseCompressedOops -XX:+PrintFlagsFinal -javaagent:E:\app\me\learn\agent-learn\target\agent-learn-1.0.0-jar-with-dependencies.jar

设置vm参数

上面参数是关闭指针压缩,和设置-javaagent。

import java.lang.instrument.Instrumentation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Stack;

public class MemoryPreMain {

    private static Instrumentation instrumentation;

    /**
     * 
     * @param agentArgs 由–javaagent传入
     * @param instrumentationParam 由JVM传入
     */
    public static void premain(String agentArgs, Instrumentation instrumentationParam) {
        instrumentation = instrumentationParam;
    }

    /**
     * 计算Shallow Size(只计算引用大小,不计算引用对象实际大小)
     * @param toCalcObject 要计算内存占用大小的对象
     * @return 对象Shallow Size
     */
    public static long sizeOf(Object toCalcObject) {
        if (instrumentation == null) {
            throw new IllegalStateException("请指定-javaagent");
        }
        return instrumentation.getObjectSize(toCalcObject);
    }

    /**
     *
     * 计算Retained Size,计算对象及其引用对象大小
     * @param toCalcObject 要计算内存占用大小的对象
     * @return 对象Retained Size
     */
    public static long fullSizeOf(Object toCalcObject) {
        Map<Object, Object> visited = new IdentityHashMap<>();
        Stack<Object> stack = new Stack<>();

        long result = internalSizeOf(toCalcObject, stack, visited);
        while (!stack.isEmpty()) {
            result += internalSizeOf(stack.pop(), stack, visited);
        }
        visited.clear();
        return result;
    }

    // 这个算法使每一个对象仅被计算一次。 避免循环引用,即死循环计算
    private static boolean skipObject(Object obj, Map<Object, Object> visited) {
        if (obj instanceof String) {
            // String 池里已有的不再计算
            if (obj == ((String) obj).intern()) {
                return true;
            }
        }
        return (obj == null) // 已有对象不再计算
                || visited.containsKey(obj);
    }

    private static long internalSizeOf(Object obj, Stack<Object> stack,  Map<Object, Object> visited) {
        if (skipObject(obj, visited)){
            return 0;
        }
        visited.put(obj, null);

        long result = 0;
        result += sizeOf(obj);

        // 处理全部数组内容
        Class clazz = obj.getClass();
        if (clazz.isArray()) {
            // [I , [F 基本类型名字长度是2,跳过基本类型数组
            if(clazz.getName().length() != 2) {
                int length =  Array.getLength(obj);
                for (int i = 0; i < length; i++) {
                    stack.add(Array.get(obj, i));
                }
            }
            return result;
        }

        // 处理对象的全部字段
        while (clazz != null) {
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                // 不反复计算静态类型字段
                if (!Modifier.isStatic(field.getModifiers())) {
                    // 不反复计算原始类型字段
                    if (field.getType().isPrimitive()) {
                        continue;
                    } else {
                        // 使 private 属性可訪问
                        field.setAccessible(true);
                        try {
                            Object objectToAdd = field.get(obj);
                            if (objectToAdd != null) {
                                stack.add(objectToAdd);
                            }
                        } catch (IllegalAccessException ex) {
                            assert false;
                        }
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
        return result;
    }
}

MAT内存分析示例

有你前面的基础,我们就可以使用MAT(Memory Analyze Tool)分析内存泄露了。

MAT下载

先创建一个简单对象:

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

再创建一个复杂一点的对象:

public class Employ {
    private User user;
    private String employeeId;
    private int year;

    private String position;

    public Employ(User user, String employeeId, int year, String position) {
        this.user = user;
        this.employeeId = employeeId;
        this.year = year;
        this.position = position;
    }
}

构造一个内存溢出的场景:

import java.util.LinkedList;
import java.util.List;

/**
 * -Xms10M -Xmx10M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=F:/tmp/mat
 */
public class MemoryLeakMain {

    public static void main(String[] args) {
        List<Employ> employs = new LinkedList<>();
        User user = new User("tim", 18);
        Employ employ = new Employ(user,"001", 18,"dev");
        System.out.println("user size:" + MemoryPreMain.sizeOf(user) + " user full size:" + MemoryPreMain.fullSizeOf(user));
        System.out.println("employ size:" + MemoryPreMain.sizeOf(employ) + " employ full size:" + MemoryPreMain.fullSizeOf(employ));

        while (true){
            user = new User("tim", 18);
            employ = new Employ(user,"001", 18,"dev");
            employs.add(employ);
        }
    }
}

参数说明:

  1. -Xms10M:初始内存
  2. -Xmx10M:最大内存
  3. -XX:+HeapDumpOnOutOfMemoryError:内存溢出时dump内存快照
  4. -XX:HeapDumpPath=F:/tmp/mat 指定dump快照路径,如果F:/tmp/mat目录存在即在改目录下创建java_pidxxxx.hprof文件,目录不存在mat就是文件名

输出:

user size:24 user full size:24
employ size:32 employ full size:56

我们看到:

  1. user的Shallow Heap和Retained Heap一样
  2. employ的Shallow Heap比Retained Heap小

我们看一下employ,Shallow Heap的大小32计算好说,就是每个引用4字节,原生类型按字节加再加本身对象头,加padding。

对象头12字节 + 3个引用 * 4字节 + 1个int * 4字节 = 28字节 + 4字节padding = 32字节

Retained Heap的56字节怎么算的呢?

很简单: Employ对象的Shallow Heap 32字节 + User对象的Shallow Heap 24字节 = 56字节

实际的逻辑比较复杂,可以参考MemoryPreMain的fullSizeOf方法。

概览

探测内存泄露代码位置:
内存泄露代码位置探测
查看,可能内存造成内存泄露对象信息:
mat list objects
具体对象内存:
对象内存情况

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/638987.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

WAF绕过(下)

过流量检测 这里的流量检测就是在网络层的waf拦截到我们向webshell传输的数据包&#xff0c;以及webshell返回的数据 包&#xff0c;检测其中是否包含敏感信息的一种检测方式。如果是大马的情况下&#xff0c;可以在大马中添加多处判断代码&#xff0c;因此在执行大马提供的功…

最新FinalShell专业版激活

支持的版本 可以激活任意版本的FinalShell为专业版&#xff0c;包括最新版4.3.10 激活方式 打开FinalShell&#xff0c;点击左下角 激活/升级。 账号密码任意输入几个字符&#xff0c;点离线激活。 复制机器码&#xff0c;将机器码发送给微信公众号【小白学算法】见文章末…

如何解决Nginx反向代理不生效?

目录 背景 过程 日志 检查配置文件 重启服务 检查容器内的配置文件 容器和宿主机 其他 背景 用了两年的nginx新加的反向代理不生效 Docker挂载的配置文件启动的Nginx&#xff0c;配置一切正常&#xff0c;但是反向代理不生效&#xff0c;???先自查一波 过程 日志 …

Function Calling 介绍与实战

functions 是 Chat Completion API 中的可选参数&#xff0c;用于提供函数定义。其目的是使 GPT 模型能够生成符合所提供定义的函数参数。请注意&#xff0c;API不会实际执行任何函数调用。开发人员需要使用GPT 模型输出来执行函数调用。 如果提供了functions参数&#xff0c;…

创新指南|利用电商产品视频进行渠道营销的最佳策略,不断提升销售额

无论企业的利基市场如何&#xff0c;电商产品视频都已被证明是非常可靠的资产&#xff0c;可以让目标受众了解您所提供的产品——关键功能、展示重要的差异化优势甚至改变大多数营销活动的游戏规则。阅读本文&#xff0c;全面了解电商产品视频如何融入营销推广&#xff0c;以最…

【保姆级教程】基于OpenCV+Python的人脸识别上课签到系统

【保姆级教程】基于OpenCVPython的人脸识别上课签到系统 一、软件安装及环境配置1. 安装IDE&#xff1a;PyCharm2. 搭建Python的环境3. 新建项目、安装插件、库 二、源文件编写1. 采集人脸.py2. 训练模型.py3. 生成表格.py4. 识别签到.py5. 创建图形界面.py 三、相关函数分析1.…

浅谈分布式系统

目录 一、单机架构二、分布式架构1、应用服务与数据库分离2、负载均衡3、数据库读写分离4、引入缓存5、数据库分库分表6、引入微服务 一、单机架构 单机架构&#xff0c;只有一台服务器&#xff0c;这个服务器负责所有工作。 绝大多数公司的产品&#xff0c;都是这种单机架构。…

思科模拟器--06.单臂路由升级版--多端路由互连实验--24.5.20

实验图纸如下: 第0步: 先放置六台个人电脑,一台交换机和一台2911路由器(千兆路由器(G0开头的)) 接着,用直通线将 PC0的F0,PC1的F0分别和交换机的F0/0, F0/1连接 交换机的F0/3和路由器的G0/0连接 PC2的F0,PC3的F0分别和交换机的F0/4, F0/5连接 交换机的F0/6和路由器的G0/1…

【施磊】C++语言基础提高:深入学习C++语言先要练好的内功

课程总目录 文章目录 一、进程的虚拟地址空间内存划分和布局二、函数的调用堆栈详细过程三、程序编译链接原理1. 编译过程2. 链接过程 一、进程的虚拟地址空间内存划分和布局 任何的编程语言 → \to → 产生两种东西&#xff1a;指令和数据 编译链接完成之后会产生一个可执行…

【传知代码】Modnet 人像抠图-论文复现

文章目录 概述原理介绍核心逻辑ModNet 的结构 环境配置WebUI 小结 论文地址 论文GitHub 本文涉及的源码可从Modnet 人像抠图该文章下方附件获取 概述 人像抠图技术在多个领域有着广泛的应用场景&#xff0c;包括但不限于&#xff1a; 展馆互动拍照&#xff1a;展馆中使用的抠…

K8S认证|CKA题库+答案| 11. 创建PVC

11、创建PVC 您必须在以下Cluster/Node上完成此考题&#xff1a; Cluster Master node Worker node ok8s master …

玩转OpenHarmony智能家居:如何实现开发版“碰一碰”设备控制

一、简介 “碰一碰”设备控制&#xff0c;依托NFC短距通信协议&#xff0c;通过碰一碰的交互方式&#xff0c;将OpenAtom OpenHarmony&#xff08;简称“OpenHarmony”&#xff09;标准系统设备和全场景设备连接起来&#xff0c;解决了应用与设备之间接续慢、传输难的问题&…

Java web应用性能分析之【高并发之缓存-多级缓存】

说到缓存&#xff0c;作为java开发第一时间想到的是不是上图所示的Redis&#xff0c;又或者是Guava Cache、Caffeine、EhCache这些&#xff1b;Redis作为分布式缓存、其他的可以作为本地缓存。但是作为一名资深开发人员&#xff0c;着眼的层面应该再提升一个级别&#xff0c;从…

LLM多模态——GPT-4o改变人机交互的多模式 AI 模型应用

1. 概述 OpenAI 发布了迄今为止最新、最先进的语言模型 – GPT-4o也称为“全“ 模型。这一革命性的人工智能系统代表了一次巨大的飞跃&#xff0c;其能力模糊了人类和人工智能之间的界限。 GPT-4o 的核心在于其原生的多模式特性&#xff0c;使其能够无缝处理和生成文本、音频…

微信小程序开发环境的搭建

一、注册微信小程序账号 二、安装微信开发者工具 1.下载微信开发者工具。 官网下载地址&#xff1a;https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/downloads.html 2、选择稳定版Window64下载安装 3、下载完毕后&#xff0c;点击下一步安装 三、使用微信开发者工具…

slam14讲(第8讲、前端里程计)LK光流、直接法

直接法的引出 因为第7讲大部分都是讲特征点法&#xff0c;通过提取orb特征点和点的描述子&#xff0c;来构建两帧图像之间的特征点对应关系。这种方法会有缺点&#xff1a; 关键点和描述子提取计算耗时&#xff0c;如果相机的频率高&#xff0c;则slam算法大部分耗时被占。特…

2种方法将集合数据List构建成树形结构

文章目录 递归循环构建树结构hutool.TreeUtil.build构建树结构 递归循环构建树结构 先查最外层树节点数据&#xff0c;再递归遍历每一层子节点数据 public ApiResultDto<List<LocationDto>> getTreeByParams(LocationSearchDto searchDto, SecurityUser user) {// …

代码随想录算法训练营第三十六天|860.柠檬水找零、406.根据身高重建队列、452. 用最少数量的箭引爆气球

860.柠檬水找零 文档讲解&#xff1a;代码随想录 题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 注意看提示&#xff1a; bills[i] 不是 5 就是 10 或是 20 场景较为固定 遇到了20&#xff0c;优先消耗10 class Solution:def lemonadeChange(self, bills: …

秋招突击——算法——模板题——区间DP——合并石子

文章目录 题目内容思路分析实现代码分析与总结 题目内容 思路分析 基本思路&#xff0c;先是遍历区间长度&#xff0c;然后再是遍历左端点&#xff0c;最后是遍历中间的划分点&#xff0c;将阶乘问题变成n三次方的问题 实现代码 // 组合数问题 #include <iostream> #in…

宝塔Linux下安装EMQX服务并设置匿名访问

简述 之前有在Windows和Linux下搭建过EMQX服务并且使用方面都没问题,但那都是使用的用户和密码方式访问,且前提都是通过浏览器进入EMQX的配置页面设置的属性; 但这次使用的是腾讯云租用的宝塔Liniux,由于没有浏览器只能通过命令行方式修改EMQX配置以达到目的;由于事先没看…