Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
212 views
in Technique[技术] by (71.8m points)

java - Is Dalvik even more memory hungry than HotSpot in terms of object sizes?

I've been wondering how much memory does an Object occupy on Android. There are numerous resources (like this) related to HotSpot JVM telling that an empty object takes 8 bytes and an empty array 12 bytes and that all objects are aligned to 8 byte boundary. Thus an object with no extra fields should take 8 bytes, the smallest object with at least one extra field – 16 bytes, an empty array – 16 bytes, right?

I've found no specific information about Dalvik on this matter and decided to figure it out by testing. Running the test had surprising results.

Few words about the method of calculation. Android's implementation of Object.hashCode() simply returns the pointer to the object casted to int. (seemed obvious and general, but [another surprise] as it turned out, it does NOT on HotSpot JVM for instance – run MemTest with HotSpot and see). So, I've used the simplicity of hashCode() on Dalvik to calculate the object size on Android by allocating two instances of the tested class in a row and the amount of the allocated space should be equal to the difference of their hashCode() values (assuming that it makes little sense for Dalvik to allocate those at completely random addresses). Just to be sure I've allocated always 4 objects in a row per test class, which delivered always the same difference of hashCode(). So, I believe there is little doubt about correctness of the method.

Here is the source code of the test:

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;
    }
}

and here are the results:

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

To sum up the results:

  • 8 byte boundary alignment is the same as on HotSpot, and that's the only thing that is the same.

  • minimum of 16 bytes for a plain Object (vs 8 on HotSpot)

  • apparently an empty object itself occupies 12 bytes (vs 8 on HotSpot) and there is room for 4 extra bytes until object size 'jumps' from 16 bytes to the next boundary of 24 bytes.

  • minimum of 24 bytes for an empty array (vs 12 on HotSpot)

  • similarly an array itself occupies 20 bytes (vs 12 on HotSpot) and there is room for 4 extra bytes of array data until object size 'jumps' from 24 bytes to the next boundary of 32 bytes.

ADDITION: (in response to Louis' suggestion) Another stress test shows that even allocating a million of Object instances the distance between any two is NEVER less than 16 bytes. That's the proof that potential 8-byte holes between the objects are definitely dead space for further allocations, otherwise by the time when about half the memory has been allocated for Objects dalvik should definitely have been putting some of them into 'holes' as well, and the stress test would return 8, not 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);
}

Do I see it right that Dalvik's Object takes up to 8 more bytes and array 8-12 more bytes compared to HotSpot?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

(Yes, this is an old question, but the results were sort of interesting so I poked at it a bit.)

The Object.clone() method needs to make a full bitwise copy of an object. To do so, it needs to know how big an object is. If you look at dvmCloneObject(), you see that it uses one method for arrays and a different method for objects.

For arrays, it calls dvmArrayObjectSize(), which multiplies the array length by the element width (1, 2, 4, or 8), and then adds the offset of the array data from the start of the object. Every object has an 8-byte header; arrays have a 4-byte width and include an additional 4 bytes of padding to ensure that 64-bit values are aligned properly. So for a 5-element array of short, it would be 16 + 5 * 2.

For ordinary objects, it just uses the objectSize field in the class object. This is set by a rather complicated function called computeFieldOffsets(). That function ensures that all object references come first (so the GC can skip around less when scanning), then follows that with all of the 64-bit fields. To ensure that the 64-bit fields are properly aligned, it may move one of the 32-bit primitive fields up to pad things out. (If there's no appropriate 32-bit field, you just get 4 bytes of padding.)

I should add: all fields are 32-bit, except long and double, which are 64-bit. Object references are 32-bit.

So it's tricky to say exactly how big a non-array object will be, but in general you take the 8-byte object header, sum up the widths of the additional fields, and round up to the next multiple of 8 bytes -- the last because all objects must be 64-bit aligned.

So that's the theory. To see it in practice, I added this to dvmCloneObject():

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

and saw logcat output like:

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

Locale has 4 reference fields, Date has one long field, so these values match expectations.

Ideally, that's exactly how much space would be required. However, the object is allocated with mspace_calloc(), which adds another 4 or (sometimes) 8 bytes of overhead. So the actual space required for the values above would be 32 and 24, which matches your experimental results.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...