Java对象大小计算与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
具体对象内存:
对象内存情况

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值