Насколько я понимаю, при создании объекта класса через new выделяется область в Heap для хранения всех полей как самого класса, так и нестатичных публичных (и protected) полей всех его предков. А затем поочерёдно вызываются конструкторы всех предков, начиная со старшего, которые инициализируют, а возможно перезаписывают значения полей данного объекта. И в конце конструктор самого объекта-наследника инициализирует свои новые поля или, возможно, перезаписывает значения полей, которые уже инициализировали конструкторы предков.
Вопрос в следующем. Если у предка есть приватные поля и публичные геттеры к ним, а наследник не переопределяет эти поля и геттеры, то из объекта наследника можно, вызвав унаследованный геттер, получить значение поля из приватного поля предка. Как происходит в действительности? При создании объекта создаются объекты всех его предков в отдельных областях памяти, со всеми своими полями? Или же, что мне кажется более вероятным, JVM понимает, что при наличии публичных геттеров у предка наследник может получить доступ к его приватным полям и поэтому создаёт в области памяти объекта-наследника приватные поля его предка?
Ответ
Объектный модуль класса, помимо прочей информации, содержит информацию о иерархии наследования, порядке следования полей и их размерах. То есть после загрузки класса у виртуальной машины в метаспейсе всегда есть "карта" объектов этого класса, по которой можно вычислить по какому смещению от начала блока памяти находится то или иное поле. При создании нового объекта выделяется блок достаточного объёма, чтобы хранить заголовок объекта, его поля, а также поля всех его суперклассов. Причём поля располагаются в порядке от корня наследования - сначала поля суперкласса, потом подкласса. Благодаря такому расположению "карта" суперкласса подходит для ориентирования в объекте подкласса. Для наглядности определим примитивную иерархию классов
class A {
int x;
int y;
}
class B extends A {
int z;
}
Операция B obj = new B() выделит в куче такой блок
----------- --
| Заголовок | |
----------- |_ Класс A
| x | |
| y | |
----------- --
| z | |- Класс B
----------- --
Если мы теперь приведём тип объекта к базовому, то виртуальная машина будет выполнять операции доступа к полям объекта так, будто никакого хвостика, содержащего поле z, просто не существует.
Благодаря Алексею Шипилёву мы можем увидеть это вживую с помощью инструмента jol (Java Object Layout).
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
public class ShowLayout {
public static void main(String[] args) throws Exception {
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseClass(B.class).toPrintable());
}
}
Компилируем
javac -cp jol-cli-0.9-full.jar ShowLayout.java
Запускаем
java -javaagent:jol-cli-0.9-full.jar ShowLayout
Получаем
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int A.x N/A
16 4 int A.y N/A
20 4 int B.z N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
Для методов каждого класса у JVM тоже есть "карта", указывающая по какому смещению в памяти находится начало того или иного метода. Есть два способа использования этой "карты" - раннее и позднее связывание (на самом деле больше, но это несущественно в текущем контексте). Обычный вызов метода
obj.getZ()
будет скомпилирован в байткод
aload_1 // Загрузка в стек ссылки на obj
invokevirtual #4 // Method B.getZ:()I
Инструкция invokevirtual использует позднее связывание. То есть при вызове метода JVM анализирует контекст вызова (call site), определяет какой именно метод нужен и передаёт управление по требуемому смещению. Благодаря этому и возможен полиморфизм.
Вызов метода суперкласса (а также вызовы конструкторов и приватных методов)
super.getX();
будет скомпилирован в байткод
aload_0 // Ссылка this на объект класса B
invokespecial #2 // Method A.getX:()I
Инструкция invokespecial использует ранее связывание. То есть ещё на этапе загрузки класса понятно какой именно метод какого именно класса надо будет вызвать, и JVM "зашивает" смещение этого метода в байткод.
Получив управлением метод getX (с помощью JVM, конечно) отсчитает нужное смещение от начала блока памяти, на который указывает переданная ссылка, до места где должно располагаться поле x в классе A, прочитает его значение, положит на вершину стека и вернёт управление вызывающему коду. Даже не догадываясь, что ссылка this на самом деле указывает на объект большего размера, и есть ли в подклассе метод с таким же именем, как у него.