Далвик еще больше голоден, чем HotSpot с точки зрения размеров объектов?
Мне интересно, сколько памяти занимает объект на Android.
Существует множество ресурсов (например, this), связанных с JVM HotSpot, сообщающих, что пустой объект занимает 8 байтов и
пустой массив 12 байтов и что все объекты выровнены с 8-байтовой границей.
Таким образом, объект без дополнительных полей должен принимать 8 байтов, наименьший объект с по крайней мере одним дополнительным полем - 16 байтов, пустой массив - 16 байтов, правый?
Я не нашел никакой конкретной информации о Dalvik по этому вопросу и решил разобраться в этом путем тестирования.
Запуск теста показал неожиданные результаты.
Несколько слов о методе вычисления. Android-реализация Object.hashCode() просто возвращает указатель на объект, выделенный для int. (казалось очевидным и общим, но [еще один сюрприз], как выяснилось, он НЕ на HotSpot JVM, например, запускает MemTest с помощью HotSpot и видит).
Итак, я использовал простоту hashCode() для Dalvik для вычисления размера объекта на 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 байтов для простого объекта (vs 8 на HotSpot)
-
по-видимому, пустой объект сам занимает 12 байт (vs 8 на HotSpot), и есть место для 4 дополнительных байтов, пока размер объекта не "перескочит" с 16 байт до следующей границы из 24 байтов.
-
минимум 24 байта для пустого массива (vs 12 на HotSpot)
-
аналогично, массив сам занимает 20 байт (vs 12 на HotSpot), и есть место для 4 дополнительных байтов данных массива, пока размер объекта не перескакивает с 24 байтов на следующую границу из 32 байтов.
ДОБАВЛЕНИЕ: (в ответ на предложение Луи)
Другой стресс-тест показывает, что даже выделение миллиона экземпляров объектов расстояние между любыми двумя никогда не меньше 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);
}
Я правильно понимаю, что Dalvik Объект занимает до 8 байт и массив еще 8-12 байтов по сравнению с HotSpot?
Ответы
Ответ 1
(Да, это старый вопрос, но результаты были интересными, поэтому я немного подтолкнул его.)
Метод Object.clone()
должен выполнить полную побитную копию объекта. Для этого ему нужно знать, насколько большой объект. Если вы посмотрите на dvmCloneObject()
, вы увидите, что он использует один метод для массивов и другой метод для объектов.
Для массивов он вызывает dvmArrayObjectSize()
, который умножает длину массива на ширину элемента (1, 2, 4 или 8), а затем добавляет смещение данных массива с начала объекта. Каждый объект имеет 8-байтовый заголовок; массивы имеют ширину в 4 байта и включают дополнительные 4 байта заполнения, чтобы обеспечить правильное выравнивание 64-битных значений. Итак, для 5-элементного массива short
это будет 16 + 5 * 2.
Для обычных объектов он просто использует поле objectSize
в объекте класса. Это задается довольно сложной функцией, называемой computeFieldOffsets()
. Эта функция гарантирует, что все ссылки на объекты будут первыми (так что GC может пропускать меньше при сканировании), а затем следует, что все 64-битные поля. Чтобы обеспечить правильное выравнивание 64-битных полей, он может переместить одно из 32-битных примитивных полей для выключения. (Если нет соответствующего 32-битного поля, вы просто получите 4 байта заполнения).
Я должен добавить: все поля 32-разрядные, кроме long
и double
, которые являются 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
Локаль имеет 4 опорных поля, дата имеет одно поле long
, поэтому эти значения соответствуют ожиданиям.
В идеале, именно то, сколько пространства потребуется. Однако объект выделяется mspace_calloc()
, который добавляет еще 4 или (иногда) 8 байтов служебных данных. Таким образом, фактическое пространство, необходимое для значений выше, будет 32 и 24, что соответствует вашим экспериментальным результатам.
Ответ 2
У меня нет ответов для вас, но я могу предложить пару мест, которые вы можете посмотреть в источнике, для получения дополнительной информации.
Вы можете взглянуть на структуры DataObject и ArrayObject в dalvik/vm/oo/Object.h. Исходя из этого, кажется, что пустой объект должен принимать только 8 байтов, а пустой массив должен принимать 12 байтов. Это, похоже, не соответствует вашим результатам, хотя я не уверен, почему.
Вы также можете посмотреть на использование полей objectSize в структуре ClassObject для более глубокого понимания. Быстрый поиск для использования этого поля показывает, что метод dvmAllocObject в dalvik/vm/alloc/Alloc.cpp, по-видимому, отвечает за выделение памяти для новых объектов.