Pull to refresh

Объекты Java

Reading time4 min
Views43K
Под впечатлениями от habrahabr.ru/blogs/java/134102.

Недавно мне приходилось немного поковыряться внутри JVM. Довольно интересный опыт. Текст в вышеупомянутом топике не совсем сходится с моим опытом, но я не считаю себя обладателем абсолютной истины. Ниже я поделюсь с читателями небольшой частью моих экспериментов, которые касаются непосредственно объектов Java.

Тестовая система

Windows XP SP3 32bit
D:\dev\puzzles>java -version
java version «1.6.0_29»
Java(TM) SE Runtime Environment (build 1.6.0_29-b11)
Java HotSpot(TM) Client VM (build 20.4-b02, mixed mode, sharing)

Подготовка


Все эксперименты проводились с использованием утилитарного класса Unsafe. Описание класса нагло украдено с wasm.ru.
Малоизвестный класс sun.misc.Unsafe входит в комплект Sun Java Runtime начиная с первых версий. Как и все остальные классы в package sun.*, Unsafe не документирован, но имена (в большинстве своем нативных) функций, видимые при декомпиляции, говорят сами за себя. Явно присутствуют функции работы с памятью (allocateMemory, freeMemory,...), чтения и записи значений по заданному адресу (putLong, getLong,...) и некоторые более специализированные (throwException, monitorEnter, ...).

Но, так просто инстанциировать Unsafe не удасться. Единственный конструктор — приватный, а в getUnsafe() проверяется загрузчик вызвавшего класса и объект возвращается только если класс загружен через Bootloader. В противном случае, получаем SecurityException.


public static Unsafe getUnsafe() {
    Class class1 = Reflection.getCallerClass(2);
    if (class1.getClassLoader() != null)
        throw new SecurityException("Unsafe");
    else
        return theUnsafe;
}


К счастью существует еще внутренняя переменная theUnsafe, до которой можно добраться с помощью Reflection. Я собрал небольшой вспомогательный класс.


import java.lang.reflect.Field;
import sun.misc.Unsafe;

public class T {

    public static Unsafe u;
    private static long fieldOffset;
    private static T instance = new T();
    private Object obj;

    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);

            u = (Unsafe) f.get(null);
            fieldOffset = u.objectFieldOffset(T.class.getDeclaredField("obj"));
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    };

    public synchronized static long o2a(Object o) {
        instance.obj = o;
        return u.getLong(instance, fieldOffset);
    }

    public synchronized static Object a2o(long address) {
        u.putLong(instance, fieldOffset, address);
        return instance.obj;
    }

    public static Unsafe get() {
        return u;
    }
}


Нам понадобятся операции взятия объекта по адресу и взятия адреса у объекта: o2a(Object o) и a2o(long address).
Теперь можно что-то смотреть.

Структура объекта


Рассмотрим работу следующего кода:

import sun.misc.Unsafe;

public class V {
    private Integer b = 3;
    private int a = 2;

    public static void main(String[] argv) throws Exception {
        Unsafe u = T.get();

        long obj = T.o2a(new V());
        for (int i = 0; i < 28; i++)
            System.out.print(u.getByte(obj + i) + " ");
    }
}


Данный код представляет собой класс, содержащий 2 приватных поля (int и Integer). Мы создаем экземпляр этого класса, узнаем его адрес в памяти, и выводим 28 байт.

Его результат в моем случае был следующим (я отформатировал вывод группами по 4 байта для большей ясности):

01 00 00 00
88 D7 79 32
02 00 00 00
D0 7C E4 22
01 00 00 00
E8 09 19 37
68 6F E4 22


Итак, что мы видим:
01 00 00 00 — это так называемый magic. Это маска объекта. Хранит различную информацию, например о локах. Подробнее в комментариях.
88 D7 79 32 — забегая вперед скажу, что это ссылка на объект класса.
02 00 00 00 — это значение нашего примитивного поля типа int.
D0 7C E4 22 — опять же, забегая вперед, скажу, что это ссылка на объект поля типа Integer.
А дальше… дальше это уже другой объект, лежащий рядом в памяти. Немного изменив наш код, можно это проверить.


import sun.misc.Unsafe;

public class V {
    private int a = 2;
    private Integer b = 777;

    public static void main(String[] argv) throws Exception {
        Unsafe u = T.get();

        long obj = T.o2a(new V());
        for (int i = 0; i < 28; i++)
            System.out.print(u.getByte(obj + i) + " ");

        long field = u.getAddress(obj + 3 * 4);
        Object i = T.a2o(field);
        System.out.print("\nInteger: " + i);
    }
}

Результат:

01 00 00 00
98 D6 79 32
02 00 00 00
C8 FB E4 22
...
Integer: 777

В этом примере мы взяли адрес поля типа Integer, а затем взяли объект, лежащий по этому адресу. Результат подтвердил наши предположения.

Следующий пример подтвердит наши ожидания, касательно второго блока (адрес класса).

import sun.misc.Unsafe;

public class V {
    private int a = 2;
    private int b = 3;

    public static void main(String[] argv) throws Exception {
        Unsafe u = T.get();

        long obj = T.o2a(new V());
        V v = new V();
        v.a = 5;
        v.b = 6;
        for (int i = 0; i < 32; i++)
            System.out.print(u.getByte(obj + i) + " ");
    }
}

Результат:

01 00 00 00
08 D6 79 32
02 00 00 00
03 00 00 00
01 00 00 00
08 D6 79 32
05 00 00 00
06 00 00 00

В этом примере я убрал ссылочные поля из класса. Создал 2 объекта, в одном из которых поменял значения полей. И, как результат, объекты попали в память рядом, и их можно отличить по значению примитивных полей.

Еще один пример с массивом:

import sun.misc.Unsafe;

public class V {
    public static void main(String[] argv) throws Exception {
        Unsafe u = T.get();

        long obj = T.o2a(new Integer[] {1, 2, 3});
        for (int i = 0; i < 28; i++)
            System.out.print(u.getByte(obj + i) + " ");
    }
}


01 00 00 00
B8 C9 79 32
03 00 00 00
40 78 E4 22
50 78 E4 22
60 78 E4 22
...

Массивы действительно хранят количество элементов в нем.

Пример со смещением:

import sun.misc.Unsafe;

public class V {
    private Integer a = 1;

    public static void main(String[] argv) throws Exception {
        Unsafe u = T.get();

        long obj = T.o2a(new V());
        for (int i = 0; i < 28; i++)
            System.out.print(u.getByte(obj + i) + " ");
    }
}

Здесь я оставил у класса только одно поле.
Результат:

01 00 00 00
B0 D7 79 32
60 78 E4 22
00 00 00 00
01 00 00 00
E8 09 19 37
...

Здесь видно, что за ссылкой на значение поля типа Integer идет пустой блок из 4 байт. Все объекты дополняются до размера, кратного 8 байтам.

Пример со смещением:

import sun.misc.Unsafe;

public class V {
    private int a = 2;
    private int b = 3;

    public static void main(String[] argv) throws Exception {
        Unsafe u = T.get();
        V v = new V();
        synchronized (v) {
            long obj = T.o2a(v);
            for (int i = 0; i < 32; i++)
                System.out.print(T.hex(u.getByte(obj + i)) + " ");
        }
    }
}

Здесь рассматриваемый объект находится в блоке синхронизации, в итоге значение первых 4 байт изменилось.
Результат:

8C 84 F0 00
58 D6 79 32
02 00 00 00
03 00 00 00
...


Выводы


Java объект состоит из:
4 байта — magic маска.
4 байта — адрес класса.
4 байта — размер массива (только в случае массивов разумеется).
n * 4 байта — на каждое поле объекта (значение примитивного типа или ссылка на объект).
Все это дополняется до кратности 8 байтам.

Писал в спешке. Исправления, вопросы, пожелания и лучи ненависти приветствуются.
Tags:
Hubs:
Total votes 68: ↑51 and ↓17+34
Comments24

Articles