Объекты Java

    Под впечатлениями от 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 байтам.

    Писал в спешке. Исправления, вопросы, пожелания и лучи ненависти приветствуются.
    Поделиться публикацией

    Комментарии 24

      +3
      Гораздо более внятное исследование, а не гадание на кофейной гуще. Интересно.

      P.S.: байтики привычнее смотреть в HEX.
        +1
        Как Вам угодно.
          +17
          По-моему, это и есть гадание. Один «magic» чего стоит. Правдивую информацию можно почерпнуть в исходниках, благо, что открытые.

          Заголовок Java-объекта, действительно, состоит из 2х слов.
          Первое — markOop — многофункциональное хранилище разнообразной информации об объекте.
          В зависимости от ситуации может содержать
          — хеш-код объекта;
          — возраст (количество пережитых GC);
          — lock (обычный, рекурсивный или biased).
          Подробное описание с побитовыми схемами — в комментариях к markOop.hpp.

          Второе слово — указатель на класс. Но не тот, который java.lang.Class, а klassOop — нативное описание типа объекта. Что из себя представляет Klass, можно вычитать, опять же, в комментариях к klass.hpp. В частности, Klass содержит и java_mirror — ссылку на java.lang.Class.

          У массивов сразу за этими двумя словами заголовка идет 32-битная длина массива — см. arrayOop.hpp.

          Что касается полей объекта, они переупорядочиваются для экономии занимаемого места с учетом выравнивания. long и double поля должны быть выровнены по 64-битной границе, int и float — по 32-битной, short и char — по 16-битной, а затем уже и byte с boolean. Для некоторых системных классов (например, String, Throwable, Reference), с которыми тесно взаимодействует VM, уплотнение полей не применяется.
          +11
          Разве хорошо воровать чужой текст?

          wasm.ru/article.php?article=unsjav1

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

          "
            –6
            Капитанский кусок про то, что такое Unsafe.
            В свое время руководствовался этим текстом.
            Не хорошо.
            Мне не стыдно.
              0
              Не только этот кусок…
            +2
            P.s. дальше по тексту еще очень много плагиата
              –2
              Да и хостинг с картинкой для привлечения внимания не выдержал и показывает соответствующую заглушку
              0
              На char тоже 4 байта?
              И где хранятся поля монитора объекта?
                0
                >И где хранятся поля монитора объекта?

                В «magic». Попробуй добавить synchronized(v) {… }.
                  0
                  Вернее, там не сам монитор, а сам факт блокировки + поддержка biased locks.
                –3
                It is nuts! Спасибо. Приятно читать!
                  0
                  Меня интересует вопрос: с помощью unsafe можно ли из AccessController.doPrivileged секции запланировать или сразу послать на исполнение любой код просто записывая байтики по адресу в памяти, тем самым обходя всевозможные ограничения безопасности jvm?
                    0
                    Включенный security manager не даст вам выстрелить себе в ногу.
                      0
                      Я боюсь пользовательских скриптов, которые выполняются в jvm основного приложения. Если security manager запрещает прямую запись в память, то это успокаивает.
                        0
                        В любом случае никак невозможно защититься от таких вещей как new byte[1000000000] и for(;;) { }
                          0
                          Пользовательские скрипты можно загружать своим загрузчиком и просто отсекать «небезопасные» классы.
                        0
                        Выше описанным путем мне удавалось поменять байткод уже загруженных методов классов, меняя значения байт инструкций в памяти. Если будет время, дополню статью.

                        Насчет SM — я ничего не поверял, но я не думаю, что он вообще позволить лезть в память.
                          +1
                          Надеюсь, изменение байткода методов класса в памяти делалась только «фану ради».
                          Потому что из-за тех, кто использует подобные трюки в реальных приложениях бог не просто убивает котенка, а выжигает кошачьи деревни напалмом.
                      +2
                      Hotspot != Java
                      OpenJDK != Java
                      ...
                      != Java

                      Отличайте уже наконец язык от его реализации, в топике вы говорите исключительно об реализации.
                        +4
                        <any JVM implementation> != Java

                        p.s. вот и меня хабрапарсер покарал
                        +2
                        Я бы сократил всю статью до «С помощью sun.misc.Unsafe можно читать байты из памяти и получать адреса объектов».

                        Структура Java-объектов это даже не просто JVM-implementation-specific, а, вообще говоря, JVM-implementation-version-and-target-platform-specific, т.к. ничто не мешает тому же Oracle, например, полностью переделать структуру объектов в HotSpot'е к следующей мажорной версии Java.

                        Кстати, magic — это уникальная константа, для идентификации каких-либо структур данных, объектов (например, для проверок перед кастами или для отладки).
                        То, что вы назвали magic'ом — просто поле с набором флагов, но никак не magic.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое