【问题标题】:Is Dalvik even more memory hungry than HotSpot in terms of object sizes?就对象大小而言,Dalvik 是否比 HotSpot 更需要内存?
【发布时间】:2012-06-05 04:24:21
【问题描述】:

我一直想知道一个对象在 Android 上占用了多少内存。 有许多与 HotSpot JVM 相关的资源(如this)告诉我们一个空对象需要 8 个字节并且 一个 12 字节的空数组,并且所有对象都对齐到 8 字节边界。 因此,一个没有额外字段的对象应该占用 8 个字节,具有至少一个额外字段的最小对象 - 16 个字节,一个空数组 - 16 个字节,对吧?

我在这件事上没有找到有关 Dalvik 的具体信息,并决定通过测试弄清楚。 运行测试得到了令人惊讶的结果

关于计算方法的几句话。 Android 的 Object.hashCode() 实现只是简单地返回指向转换为 int 的对象的指针。 (看起来很明显也很笼统,但 [另一个惊喜] 结果证明,它不在 HotSpot JVM 上——例如,用 HotSpot 运行 MemTest 并查看)。 因此,我使用 Dalvik 上 hashCode() 的简单性,通过连续分配测试类的两个实例来计算 Android 上的对象大小,分配的空间量应该等于它们的 hashCode() 之差值(假设 Dalvik 将它们分配在完全随机的地址上毫无意义)。只是为了确保我总是在每个测试类中连续分配 4 个对象,这总是提供相同的 hashCode() 差异。所以,我相信这个方法的正确性是毫无疑问的。

这里是测试的源代码:

public class MemTest {
    public static void run() {
        Object o1 = new Object();
        Object o2 = new Object();
        Object o3 = new Object();
        Object o4 = new Object();

        EmptyObject eo1 = new EmptyObject();
        EmptyObject eo2 = new EmptyObject();
        EmptyObject eo3 = new EmptyObject();
        EmptyObject eo4 = new EmptyObject();

        ObjectWithBoolean ob1 = new ObjectWithBoolean();
        ObjectWithBoolean ob2 = new ObjectWithBoolean();
        ObjectWithBoolean ob3 = new ObjectWithBoolean();
        ObjectWithBoolean ob4 = new ObjectWithBoolean();

        ObjectWithBooleanAndInt obi1 = new ObjectWithBooleanAndInt();
        ObjectWithBooleanAndInt obi2 = new ObjectWithBooleanAndInt();
        ObjectWithBooleanAndInt obi3 = new ObjectWithBooleanAndInt();
        ObjectWithBooleanAndInt obi4 = new ObjectWithBooleanAndInt();

        ObjectWithLong ol1 = new ObjectWithLong();
        ObjectWithLong ol2 = new ObjectWithLong();
        ObjectWithLong ol3 = new ObjectWithLong();
        ObjectWithLong ol4 = new ObjectWithLong();

        ObjectWith4Ints o4i1 = new ObjectWith4Ints();
        ObjectWith4Ints o4i2 = new ObjectWith4Ints();
        ObjectWith4Ints o4i3 = new ObjectWith4Ints();
        ObjectWith4Ints o4i4 = new ObjectWith4Ints();

        ObjectWith4IntsAndByte o4ib1 = new ObjectWith4IntsAndByte();
        ObjectWith4IntsAndByte o4ib2 = new ObjectWith4IntsAndByte();
        ObjectWith4IntsAndByte o4ib3 = new ObjectWith4IntsAndByte();
        ObjectWith4IntsAndByte o4ib4 = new ObjectWith4IntsAndByte();

        ObjectWith5Ints o5i1 = new ObjectWith5Ints();
        ObjectWith5Ints o5i2 = new ObjectWith5Ints();
        ObjectWith5Ints o5i3 = new ObjectWith5Ints();
        ObjectWith5Ints o5i4 = new ObjectWith5Ints();

        ObjectWithArrayRef oar1 = new ObjectWithArrayRef();
        ObjectWithArrayRef oar2 = new ObjectWithArrayRef();
        ObjectWithArrayRef oar3 = new ObjectWithArrayRef();
        ObjectWithArrayRef oar4 = new ObjectWithArrayRef();

        byte[] a0b1 = new byte[0];
        byte[] a0b2 = new byte[0];
        byte[] a0b3 = new byte[0];
        byte[] a0b4 = new byte[0];

        byte[] a1b1 = new byte[1];
        byte[] a1b2 = new byte[1];
        byte[] a1b3 = new byte[1];
        byte[] a1b4 = new byte[1];

        byte[] a5b1 = new byte[5];
        byte[] a5b2 = new byte[5];
        byte[] a5b3 = new byte[5];
        byte[] a5b4 = new byte[5];

        byte[] a9b1 = new byte[9];
        byte[] a9b2 = new byte[9];
        byte[] a9b3 = new byte[9];
        byte[] a9b4 = new byte[9];

        byte[] a12b1 = new byte[12];
        byte[] a12b2 = new byte[12];
        byte[] a12b3 = new byte[12];
        byte[] a12b4 = new byte[12];

        byte[] a13b1 = new byte[13];
        byte[] a13b2 = new byte[13];
        byte[] a13b3 = new byte[13];
        byte[] a13b4 = new byte[13];

        print("java.lang.Object", o1, o2, o3, o4);
        print("Empty object", eo1, eo2, eo3, eo4);
        print("Object with boolean", ob1, ob2, ob3, ob4);
        print("Object with boolean and int", obi1, obi2, obi3, obi4);
        print("Object with long", ol1, ol2, ol3, ol4);
        print("Object with 4 ints", o4i1, o4i2, o4i3, o4i4);
        print("Object with 4 ints and byte", o4ib1, o4ib2, o4ib3, o4ib4);
        print("Object with 5 ints", o5i1, o5i2, o5i3, o5i4);

        print("Object with array ref", new Object[]{oar1, oar2, oar3, oar4});

        print("new byte[0]", a0b1, a0b2, a0b3, a0b4);
        print("new byte[1]", a1b1, a1b2, a1b3, a1b4);
        print("new byte[5]", a5b1, a5b2, a5b3, a5b4);
        print("new byte[9]", a9b1, a9b2, a9b3, a9b4);
        print("new byte[12]", a12b1, a12b2, a12b3, a12b4);
        print("new byte[13]", a13b1, a13b2, a13b3, a13b4);
    }

    static void print(String title, Object... objects) {
        StringBuilder buf = new StringBuilder(title).append(":");
        int prevHash = objects[0].hashCode();
        int prevDiff = -1;
        for (int i = 1; i < objects.length; i++) {
            int hash = objects[i].hashCode();
            int diff = Math.abs(hash - prevHash);
            if (prevDiff == -1 || prevDiff != diff) {
                buf.append(' ').append(diff);
            }
            prevDiff = diff;
            prevHash = hash;
        }
        System.out.println(buf.toString());
    }

    /******** Test classes ******/

    public static class EmptyObject {
    }

    public static class ObjectWith4Ints {
        int i1;
        int i2;
        int i3;
        int i4;
    }

    public static class ObjectWith4IntsAndByte {
        int i1;
        int i2;
        int i3;
        int i4;
        byte b;
    }

    public static class ObjectWith5Ints {
        int i1;
        int i2;
        int i3;
        int i4;
        int i5;
    }

    public static class ObjectWithArrayRef {
        byte[] b;
    }

    public static class ObjectWithBoolean {
        boolean b;
    }

    public static class ObjectWithBooleanAndInt {
        boolean b;
        int i;
    }

    public static class ObjectWithLong {
        long l;
    }
}

结果如下:

java.lang.Object: 16
Empty object: 16
Object with boolean: 16
Object with boolean and int: 24
Object with long: 24
Object with 4 ints: 32
Object with 4 ints and byte: 32
Object with 5 ints: 32
Object with array ref: 16
new byte[0]: 24
new byte[1]: 24
new byte[5]: 32
new byte[9]: 32
new byte[12]: 32
new byte[13]: 40

总结一下结果:

  • 8 字节边界对齐方式与 HotSpot 上的对齐方式相同,唯一相同的是。

  • 普通对象至少 16 个字节(HotSpot 上为 8 个字节)

  • 显然,一个空对象本身占用 12 个字节(HotSpot 上为 8 个),并且在对象大小从 16 个字节“跳跃”到下一个 24 个字节边界之前,还有 4 个额外字节的空间。

  • 空数组最少 24 个字节(HotSpot 上为 12 个字节)

  • 类似地,数组本身占用 20 个字节(HotSpot 上为 12 个字节),并且在对象大小从 24 个字节“跳跃”到下一个 32 个字节边界之前,还有 4 个额外字节的数组数据空间。

补充:(回应路易斯的建议) 另一个压力测试表明,即使分配一百万个 Object 实例,任何两个实例之间的距离也绝不会小于 16 字节。这证明了对象之间潜在的 8 字节空洞肯定是用于进一步分配的死空间,否则当为对象分配大约一半的内存时,dalvik 肯定也应该将其中一些放入“空洞”中,压力测试将返回 8,而不是 16。

public static void run2() {
    int count = 1024 * 1024;
    Object[] arr = new Object[count];
    for (int i = 0; i < count; i++) {
        arr[i] = new Object();
    }
    int[] hashes = new int[count];
    for (int i = 0; i < count; i++) {
        hashes[i] = arr[i].hashCode();
    }
    Arrays.sort(hashes);

    int minDist = Integer.MAX_VALUE;
    for (int i = 1; i < count; i++) {
        int dist = Math.abs(hashes[i] - hashes[i - 1]);
        if (dist < minDist) {
            minDist = dist;
        }
    }
    System.out.println("Allocated "+ count + " Objects, minimum distance is "+ minDist);
}

与 HotSpot 相比,Dalvik 的 Object 最多占用 8 个字节array 最多占用 8-12 个字节,我是否认为正确?

【问题讨论】:

  • 这里有几个不受支持的假设...
  • “不支持”是什么意思?
  • "假设 Dalvik 将它们分配到完全随机的地址没有什么意义" 随机的,也许不是,但是连续的?可能存在对齐限制或许多其他事情。我想知道有关此类事情的 Android 内存分配的详细信息是否在 Google 上。
  • 我相信,即使对象之间有一些“漏洞”——(可能是 4、8 个字节)——这绝对是死空间,很可能被认为是对象大小的一部分,因为最后唯一重要的是通过分配 N 个对象有效地消耗了多少内存(不能用于另一个对象分配)。它们的对齐方式等与每个对象的有效内存使用无关。

标签: java android memory-management dalvik


【解决方案1】:

我没有给你答案,但我可以建议你在源代码中查找更多信息的几个地方。

您可以查看 dalvik/vm/oo/Object.h 中的 DataObject 和 ArrayObject 结构。基于此,似乎一个空对象应该只占用 8 个字节,而一个空数组应该占用 12 个字节。这似乎与您的结果不符,虽然我不确定为什么。

您还可以查看 ClassObject 结构中 objectSize 字段的用法以获得更多信息。快速搜索该字段的用法表明 dalvik/vm/alloc/Alloc.cpp 中的 dvmAllocObject 方法似乎负责为新对象分配内存。

【讨论】:

  • Dalvik 在后台也使用 dlmalloc,它自己进行分配分桶。
【解决方案2】:

(是的,这是一个老问题,但结果有点有趣,所以我稍微戳了一下。)

Object.clone() 方法需要对对象进行完整的按位复制。为此,它需要知道一个对象有多大。如果您查看dvmCloneObject(),您会发现它对数组使用一种方法,对对象使用另一种方法。

对于数组,它调用dvmArrayObjectSize(),它将数组长度乘以元素宽度(1、2、4或8),然后加上数组数据从对象开始的偏移量。每个对象都有一个 8 字节的标头;数组的宽度为 4 字节,并包含额外的 4 字节填充,以确保 64 位值正确对齐。因此,对于short 的 5 元素数组,它将是 16 + 5 * 2。

对于普通对象,它只是使用类对象中的objectSize 字段。这是由一个名为computeFieldOffsets() 的相当复杂的函数设置的。该函数确保所有对象引用首先出现(因此 GC 在扫描时可以少跳过),然后是所有 64 位字段。为确保 64 位字段正确对齐,它可能会向上移动 32 位原始字段之一以填充内容。 (如果没有合适的 32 位字段,您只会得到 4 个字节的填充。)

我应该补充一点:所有字段都是 32 位的,除了 longdouble,它们是 64 位的。对象引用是 32 位的。

所以很难准确地说出非数组对象有多大,但通常你取 8 字节的对象头,将附加字段的宽度相加,然后四舍五入到下一个 8 字节的倍数-- 最后一个,因为所有对象都必须是 64 位对齐的。

这就是理论。为了在实践中看到它,我将此添加到dvmCloneObject()

ALOGD("class=%s size=%d", clazz->descriptor, clazz->objectSize);

并看到 logcat 输出如下:

D dalvikvm: class=Ljava/util/Locale; size=24
D dalvikvm: class=Ljava/util/Date; size=16

Locale 有 4 个参考字段,Date 有一个 long 字段,因此这些值符合预期。

理想情况下,这正是所需的空间。但是,该对象分配有mspace_calloc(),这又增加了 4 或(有时)8 字节的开销。因此,上述值所需的 实际 空间将是 32 和 24,这与您的实验结果相符。

【讨论】:

  • 我想知道为什么一个没有字段的对象一个有一个int字段的对象占用相同的空间(都是16字节)。 mspace_calloc 的开销是 4 字节还是 8 字节?
  • dlmalloc 文档说“每个分配的块的最小开销:4 或 8 字节(如果是 4 字节大小)”。我相信这取决于分配的执行方式(本地与 mmap),这可能取决于分配请求的大小。所有对象都是 8 字节对齐的,因此 12 字节对象和 16 字节对象都将占用 16 字节。
  • 没有字段的对象的大小为&lt;dlmalloc_overhead&gt; + sizeof(struct Object) = &lt;dlmalloc_overhead + 8&gt;。具有一个 int 字段的对象的大小为 &lt;dlmalloc_overhead + sizeof(struct Object) + sizeof(int) = &lt;dlmalloc_overhead&gt; + 12。由于最终结果都是 16 字节,我猜 &lt;dlmalloc_overhead&gt; 是 4。但这意味着,在第二种情况下,dlmalloc 簿记元数据和对象指针 不能 都是 8 字节对齐的(如果dlmalloc 簿记数据放在分配的对象之前。这是真的吗?) dlmalloc 簿记元数据是否可能不是 8 字节对齐的?
  • dlmalloc 分配的开始(malloc() 返回的内容)必须是 8 字节对齐的。 4 字节开销(块的大小)紧接在此之前。 dlmalloc 代码包含一些不错的 ASCII 艺术——从 android.googlesource.com/platform/bionic/+/jb-mr2-release/libc/… 的第 2047 行开始。为了增加一些额外的风味,还有“最小分配大小:4 字节 ptrs:16 字节(包括开销)”。
猜你喜欢
  • 1970-01-01
  • 2013-08-30
  • 1970-01-01
  • 1970-01-01
  • 2011-03-05
  • 2016-10-06
  • 1970-01-01
  • 1970-01-01
  • 2017-06-12
相关资源
最近更新 更多