Введение
Содержит ли Java-объект:
- поля, объявленные в суперклассе?
- private поля, объявленные в суперклассе?
- методы?
- элементы массива?
- длину массива?
- другой объект (в себе)?
- hash-код?
- тип (свой)?
- имя (своё)?
Ответы на эти (и другие) вопросы можно получить с помощью библиотеки классов org.openjdk.jol которая, в частности, позволяет уяснить, что объект — это область памяти:
- содержащая:
- заголовок (до 16 байт), и в нём:
- hash-код
- ссылку на тип
- длину массива (для массива)
- все поля (включая private), объявленные во всех суперклассах
- или элементы массива (для массива)
- заголовок (до 16 байт), и в нём:
- не содержащая:
- статические переменные
- методы
- другие объекты в себе
- своё имя (то есть у объекта нет имени)
Подготовка
Здесь приведены результаты оценки памяти объектов разного типа по способу из описания пакета java.lang.instrument (смотри также здесь). Эти результаты позволяют ответить на большинство поставленных выше вопросов.
Необходимо выполнить следующие шаги:
- Создать класс-агент, содержащий метод premain:
public static void premain(String, Instrumentation) {...}
- Создать архив, содержащий класс-агент и манифест-файл с содержимым:
Premain-class: имя-класса-агента
- Создать исполняемый класс для оценки памяти.
- Указать архив параметром "-javaagent" при запуске виртуальной машины:
java -javaagent:имя-архива имя-исполняемого-класса
Начнём с пробного примера. Для простоты используем безымянный пакет.
Шаг 1. Создаём пробный класс-агент
import java.lang.instrument.Instrumentation;
public class A {
public static void premain(String notUsedHere, Instrumentation i) {
System.out.println("premain");
}
}
Компилируем:
javac A.java
Шаг 2. Создаём манифест-файл m.txt, содержащий:
Premain-class: A
пустая строка
ВНИМАНИЕ: вторая строка файла должна быть ПУСТОЙ, НЕ СОДЕРЖАЩЕЙ ПРОБЕЛОВ.
Создаём архив A.jar:
jar cmf m.txt A.jar A.class
Шаг 3. Создаём пробный исполняемый класс
public class M {
public static void main(String[] notUsedHere) {
// Пока без оценки памяти
System.out.println("main");
}
}
Компилируем:
javac M.java
Шаг 4. Выполняем
java -javaagent:A.jar M
Результат:
premain
main
указывает, что сначала был вызван метод premain класса-агента, а затем — метод main исполняемого класса.
Теперь создаём требуемый класс-агент:
import java.lang.instrument.Instrumentation;
public class A {
// статическую переменную инициализирует виртуальная машина
private static Instrumentation ins;
public static void premain(String notUsedHere, Instrumentation i) {
ins = i;
}
public static Instrumentation instrumentation() {return ins;}
}
и исполняемый класс:
class M {
public static void main(String[] notUsedHere) {
mem("Object", new Object());
}
private static void mem(Object o, Object ref) {
System.out.println(o + ": " + objectBytesEstimate(ref));
}
private static long objectBytesEstimate(Object ref) {
if (A.instrumentation() == null) {
throw new RuntimeException("Not initialized instrumentation.");
}
return A.instrumentation().getObjectSize(ref);
}
}
Метод
long getObjectSize(Object ссылка-на-объект)
возвращает ОЦЕНКУ размера (количества байт) памяти, занимаемой объектом по указанной ссылке. Необходимо иметь ввиду, что полученная оценка может быть иной для иной виртуальной машины. Здесь будут приведены значения для jdk-13.
Выполняем:
javac *.java
jar cmf m.txt A.jar A.class
java -javaagent:A.jar M
и получаем результат:
Object: 16
показывающий, что ПУСТОЙ объект типа Object занимает здесь (ПО ОЦЕНКЕ) 16 байт. Из них 12 байт занимает заголовок, а 4 байта в конце служат для выравнивания длины объекта на границу 8 байт.
Результаты
Дальнейшие примеры будут содержать лишь код, размещаемый в методе main класса M. Их следует выполнять для каждого примера командами:
javac M.java
java -javaagent:A.jar M
Пересоздавать A.jar нет необходимости.
Например, для получения оценки размера памяти объекта произвольного типа без полей, поместим в метод main код:
class C {}; mem("Empty", new C()); // Empty: 16
Результат, указанный в комментарии, показывает, что объект без полей занимает столько же байт, сколько объект типа Object.
Далее, результат программы:
{class C {int a; } mem(1, new C());} // 1: 16
{class C {int a,b; } mem(2, new C());} // 2: 24
{class C {int a,b,c; } mem(3, new C());} // 3: 24
{class C {int a,b,c,d;} mem(4, new C());} // 4: 32
показывает, что каждое int-поле занимает 4 байта. Замечу, что здесь каждая строка — отдельный блок, что позволяет использовать одно и то же имя для разных классов.
Каждое long-поле занимает 8 байт:
{class C {long a; } mem(1, new C());} // 1: 24
{class C {long a,b; } mem(2, new C());} // 2: 32
{class C {long a,b,c;} mem(3, new C());} // 3: 40
Каждое boolean-поле занимает 1 байт (для данной ВМ):
{class C {boolean a; } mem(1, new C());} // 1: 16
{class C {boolean a,b; } mem(2, new C());} // 2: 16
{class C {boolean a,b,c; } mem(3, new C());} // 3: 16
{class C {boolean a,b,c,d; } mem(4, new C());} // 4: 16
{class C {boolean a,b,c,d,e;} mem(5, new C());} // 5: 24
Каждое ссылочное поле занимает 4 байта (для данной ВМ):
{class C {Boolean a; } mem(1, new C());} // 1: 16
{class C {Integer a; } mem(1, new C());} // 1: 16
{class C {Long a; } mem(1, new C());} // 1: 16
{class C {C a; } mem(1, new C());} // 1: 16
{class C {Boolean a,b; } mem(2, new C());} // 2: 24
{class C {Integer a,b; } mem(2, new C());} // 2: 24
{class C {Long a,b; } mem(2, new C());} // 2: 24
{class C {C a,b; } mem(2, new C());} // 2: 24
{class C {Boolean a,b,c; } mem(3, new C());} // 3: 24
{class C {Integer a,b,c; } mem(3, new C());} // 3: 24
{class C {Long a,b,c; } mem(3, new C());} // 3: 24
{class C {C a,b,c; } mem(3, new C());} // 3: 24
{class C {Boolean a,b,c,d;} mem(4, new C());} // 4: 32
{class C {Integer a,b,c,d;} mem(4, new C());} // 4: 32
{class C {Long a,b,c,d;} mem(4, new C());} // 4: 32
{class C {C a,b,c,d;} mem(4, new C());} // 4: 32
Поле String-типа тоже занимает 4 байта, как и каждое ссылочное:
{class C {String a; } mem(" null", new C());} // null: 16
{class C {String a=""; } mem(" empty", new C());} // empty: 16
{class C {String a="A"; } mem("1-char", new C());} // 1-char: 16
{class C {String a="1234567";} mem("7-char", new C());} // 7-char: 16
Поле-ссылка на массив тоже занимаат 4 байта, как и каждое ссылочное:
{class C {int[] a; } mem("null", new C());} // null: 16
{class C {int[] a = {}; } mem(" 0", new C());} // 0: 16
{class C {int[] a = new int[1]; } mem(" 1", new C());} // 1: 16
{class C {int[] a = new int[7]; } mem(" 7", new C());} // 7: 16
{class C {int[][] a = {}; } mem(" 00", new C());} // 00: 16
{class C {int[][] a = new int[1][1];} mem(" 11", new C());} // 11: 16
{class C {int[][] a = new int[7][7];} mem(" 77", new C());} // 77: 16
Объект подтипа содержит каждое поле, объявленное в суперклассе, независимо от модификатора доступа:
{class S { } class C extends S {long a;} mem("0+1", new C());} // 0+1: 24
{class S {private long a;} class C extends S { } mem("1+0", new C());} // 1+0: 24
Объект подтипа содержит поле, объявленное в суперклассе с тем же именем, что и в подклассе (так называемое спрятанное — hidden):
{class S { } class C extends S {long a,b;} mem("0+2", new C());} // 0+2: 32
{class S {long a;} class C extends S {long a; } mem("1+1", new C());} // 1+1: 32
Объект подтипа содержит каждое поле, объявленное в каждом его суперклассе:
class U {private long a; }
class S extends U {private long a; }
class C extends S { long a; } mem("1+1+1", new C()); // 1+1+1: 40
class D { long a,b,c;} mem("0+0+3", new D()); // 0+0+3: 40
Обратися к массивам. Как известно, массив — это особый вид объекта, элементы которого находятся в самом объекте, так что размер памяти, занимаемый массивом, растёт с числом элементов:
{long[] a = new long[ 0]; mem(" 0", a);} // 0: 16
{long[] a = new long[ 1]; mem(" 1", a);} // 1: 24
{long[] a = new long[ 2]; mem(" 2", a);} // 2: 32
{long[] a = new long[ 3]; mem(" 3", a);} // 3: 40
{long[] a = new long[100]; mem("100", a);} // 100: 816
А для массива ссылок:
{Long[] a = new Long[ 0]; mem(" 0", a);} // 0: 16
{Long[] a = new Long[ 1]; mem(" 1", a);} // 1: 24
{Long[] a = new Long[ 2]; mem(" 2", a);} // 2: 24
{Long[] a = new Long[ 3]; mem(" 3", a);} // 3: 32
{Long[] a = new Long[100]; mem("100", a);} // 100: 416
Теперь из любопытства сравним размеры нескольких объектов разного типа:
mem(" Object", new Object()); // Object: 16
mem(" String", new String("ABC")); // String: 24
mem(" Exception", new Exception()); // Exception: 40
mem(" int.class", int.class); // int.class: 112
mem(" int[].class", int[].class); // int[].class: 112
mem("Object.class", Object.class); // Object.class: 112
mem("System.class", System.class); // System.class: 160
mem("String.class", String.class); // String.class: 136
То же для разных jdk на 64-битном процессоре:
jdk1.6.0_45 jdk1.7.0_80 jdk1.8.0_191 jdk-9 jdk-12 jdk-13 ----------- ----------- ------------ ------ ------ ------ Object: 16 16 16 16 16 16 String: 32 24 24 24 24 24 Exception: 32 32 32 40 40 40 int.class: 104 88 104 112 104 112 int[].class: 584 544 480 112 104 112 Object.class: 600 560 496 112 104 112 System.class: 624 560 496 144 152 160 String.class: 696 640 624 136 128 136
Оценка размера объекта типа String — 24 байта, хотя класс String содержит много статических переменных, статических и нестатических методов. Это несомненно указывает на отсутствие в объекте статических переменных и кода методов. То же верно для объекта любого типа.
В заключение — напоминание: все приведенные данные о размере объекта оценочные и они могут в какой-то мере изменяться от одного выполнения к другому и, конечно, для разных виртуальных машин.