Структура байт кода java

Структура байт-кода виртуальной машины Java

В последнее время на Хабре появились статьи которые затрагивают манипуляцию байт-кода. Что заставило меня опубликовать следую статью посвященную его структуре.

У платформы java имеется две особенности. Для обеспечения кроссплатформенности программа сначала компилируется в промежуточный язык низкого уровня — байт-код. Вторая особенность загрузка исполняемых классов происходит с помощью расширяемых classloader. Это механизм обеспечивает большую гибкость и позволяет модифицировать исполняемый код при загрузке, создавать и подгружать новые классы во время выполнения программы.

Такая техника широко применяется для реализации AOP, создания тестовых фреймворков, ORM. Особенно хочется отметить terracotta, продукт с красивой идеей кластеризации jvm и на всю катушку использующей модификации байт-кода. Эта заметка будет посвящена обзору структуры байт-кода, первой части этой сильной связки.

Каждому классу в java соответствует один откомпилированный файл. Это справедливо даже для подклассов или анонимным классов. Такой файл содержит информацию об имени класса, его родителе, список интерфейсов которые он реализует, перечисление его полей и методов. Важно отметить, что после компиляции информации, которая содержит директива import, теряется и все классы именуются теперь через полный путь. Например в место String будет записано java/lang/String.

Самое интересное как будут выглядеть методы класса в байт-коде. Будем наблюдать во что трансформируется следующий класс:

package org; class Test < private String name; public String getName() < return name; >public void setName(String name) < this.name = name; >>

Начнем с заголовка. В нем содержится информация о название метода, то, что метод вызывается без параметров, и тип возвращаемого аргумента.

Байт-код стеко-ориентированный язык, похожий по своей структуре на ассемблер. Что бы произвести операции с данными их сначала нужно положить на стек. Мы хотим взять поле у объекта. Что бы это сделять нужно его положить в стек. В байт-коде нет имен переменных, у них есть номера. Нулевой номер у ссылки на текущий объект или у переменой this. Потом идут параметры исполняемого метода. Затем остальные переменные.

Читайте также:  Шаблоны html на всю страну

Команда ALOAD 0 кладет переменную this на стек. Что бы на стек положить тип данных, отличный от ссылки, нужно воспользоваться другой командой. Для long будет LLOAD, а для doubles[] будет DALOAD.

Следующая команда GETFIELD, убирает со стека ссылку на объект и кладет примитивный тип или ссылку на поле данного объекта. У нее есть два параметра. Первый имя — класса, второй имя — переменной. Если же переменная статическая, то предварительно класть на стек ничего не нужно, а команду нужно заменить на GETSTATIC с теми же параметрами.

Последняя команда говорит, что метод завершен и возвращает значения типа ссылки со стека.

Сеттер имеет немного более сложную структуру.

public setName(Ljava/lang/String;)V ALOAD 0 ALOAD 1 PUTFIELD org/Test name RETURN

Данный метод ничего не возвращает. Первые две команды кладут на стек переменную this и параметр исполняемого метода. Затем вызывается команда PUTFIELD (PUTSTATIC для статического поля) которая установит значение поля объекта и уберет со стека последние два значения. Последняя команда — выход из метода.

Добавим к нашему объект еще пару методов и посмотрим, какой байт-код им соответствует.

public void forTest(Boolean b) < System.out.prinln(b); >public Long testMethods(Collection testInterface)

INVOKESTATIC java/lang/System currentTimeMillis ()J LSTORE 2 ALOAD 0 ALOAD 1 LLOAD 2 INVOKESTATIC java/lang/Long valueOf (J)Ljava/lang/Long; INVOKEINTERFACE java/util/Collection contains (Ljava/lang/Object;)Z INVOKESTATIC java/lang/Boolean valueOf (Z)Ljava/lang/Boolean; INVOKEVIRTUAL org/Test forTest (Ljava/lang/Boolean;)V LLOAD 2 INVOKESTATIC java/lang/Long valueOf (J)Ljava/lang/Long; ARETURN

Первая команда вызывает статический метод у класса System. Вторая запоминает результат вызова метода currentTimeMillis в переменной со вторым номером. Затем мы кладем переменную this, параметр метода и переменную с номером 2 на стек. Преобразую переменную к типу java/lang/Long. И проверяем, что она у нас содержится в коллекции, вызывая метод у параметра исполняемого. У нас параметр интерфейс, поэтому применяется команда INVOKEINTERFACE. Для метода класса необходимо использовать INVOKEVIRTUAL. Чтобы вызвать метод у объект или интерфейса необходимо, чтобы на стеке лежал объект, затем параметры вызываемого метода. В результате вызова метода они заменятся на результат или просто уберутся со стека, если метод ничего возвращает. Последняя три команды кладут перемену на стек, превращают ее в объект и возвращают ее как значение метода.

Чтобы завершить наш экскурс в байт-код, добавим последний метод и посмотрим на циклы и условные операторы.

public void testAriphmentics() < int i = -17; while(i < 10)< if(i < 0)< i = i + 7; >i = i*13; > >
ACC_FINAL -17 ISTORE 1 Label:L1466604866 ILOAD 1 ACC_FINAL 10 IF_ICMPGE L329949514 ILOAD 1 IFGE L658705244 ILOAD 1 ACC_FINAL 7 IADD ISTORE 1 Label:L658705244 ILOAD 1 ACC_FINAL 13 IMUL ISTORE 1 GOTO L1466604866 Label:L329949514 RETURN

Первые две команды инициализирую переменную i (с номером 1) значением -17.

Дальше у нас начинается тело цикла. Для реализации которого потребуются метки,
команда перехода и условный оператор. В теле нашего метода аж целых три меток. Первая метка означает начало цикла. Вторая нужна для условного оператора, а последняя знаменует конец цикла. Уловный оператор имеет один параметр метку перехода. Прежде чем его вызвать сравниваемы значения должны лежать на стеке. Для сравнения с нулем используется отдельная команда. Для каждого типа есть свой оператор сравнения. Для int он IF_ICMPGE. После сравнения сравниваемые значения убираются со стека. Для арифметических действий с двумя переменным их так же как и для условного оператора нужно предварительно положить на стек. После выполнения они снимаются со стека, а на их место кладется результат.

На это краткий экскурс в байт-код закончен, некоторые вопросы такие как исключения, синхронизация не были затронуты. Я надеюсь, что имея представление о байт коде читатель без труда справиться с ними. В следующей части мы рассмотрим инструменты которые применяются для модификации байт-кода.

Источник

Структура байт-кода виртуальной машины Java

В последнее время на Хабре появились статьи которые затрагивают манипуляцию байт-кода. Что заставило меня опубликовать следую статью посвященную его структуре.

У платформы java имеется две особенности. Для обеспечения кроссплатформенности программа сначала компилируется в промежуточный язык низкого уровня — байт-код. Вторая особенность загрузка исполняемых классов происходит с помощью расширяемых classloader. Это механизм обеспечивает большую гибкость и позволяет модифицировать исполняемый код при загрузке, создавать и подгружать новые классы во время выполнения программы.

Такая техника широко применяется для реализации AOP, создания тестовых фреймворков, ORM. Особенно хочется отметить terracotta, продукт с красивой идеей кластеризации jvm и на всю катушку использующей модификации байт-кода. Эта заметка будет посвящена обзору структуры байт-кода, первой части этой сильной связки.

Каждому классу в java соответствует один откомпилированный файл. Это справедливо даже для подклассов или анонимным классов. Такой файл содержит информацию об имени класса, его родителе, список интерфейсов которые он реализует, перечисление его полей и методов. Важно отметить, что после компиляции информации, которая содержит директива import, теряется и все классы именуются теперь через полный путь. Например в место String будет записано java/lang/String.

Самое интересное как будут выглядеть методы класса в байт-коде. Будем наблюдать во что трансформируется следующий класс:

package org; class Test < private String name; public String getName() < return name; >public void setName(String name) < this.name = name; >>

Начнем с заголовка. В нем содержится информация о название метода, то, что метод вызывается без параметров, и тип возвращаемого аргумента.

Байт-код стеко-ориентированный язык, похожий по своей структуре на ассемблер. Что бы произвести операции с данными их сначала нужно положить на стек. Мы хотим взять поле у объекта. Что бы это сделять нужно его положить в стек. В байт-коде нет имен переменных, у них есть номера. Нулевой номер у ссылки на текущий объект или у переменой this. Потом идут параметры исполняемого метода. Затем остальные переменные.

Команда ALOAD 0 кладет переменную this на стек. Что бы на стек положить тип данных, отличный от ссылки, нужно воспользоваться другой командой. Для long будет LLOAD, а для doubles[] будет DALOAD.

Следующая команда GETFIELD, убирает со стека ссылку на объект и кладет примитивный тип или ссылку на поле данного объекта. У нее есть два параметра. Первый имя — класса, второй имя — переменной. Если же переменная статическая, то предварительно класть на стек ничего не нужно, а команду нужно заменить на GETSTATIC с теми же параметрами.

Последняя команда говорит, что метод завершен и возвращает значения типа ссылки со стека.

Сеттер имеет немного более сложную структуру.

public setName(Ljava/lang/String;)V ALOAD 0 ALOAD 1 PUTFIELD org/Test name RETURN

Данный метод ничего не возвращает. Первые две команды кладут на стек переменную this и параметр исполняемого метода. Затем вызывается команда PUTFIELD (PUTSTATIC для статического поля) которая установит значение поля объекта и уберет со стека последние два значения. Последняя команда — выход из метода.

Добавим к нашему объект еще пару методов и посмотрим, какой байт-код им соответствует.

public void forTest(Boolean b) < System.out.prinln(b); >public Long testMethods(Collection testInterface)

INVOKESTATIC java/lang/System currentTimeMillis ()J LSTORE 2 ALOAD 0 ALOAD 1 LLOAD 2 INVOKESTATIC java/lang/Long valueOf (J)Ljava/lang/Long; INVOKEINTERFACE java/util/Collection contains (Ljava/lang/Object;)Z INVOKESTATIC java/lang/Boolean valueOf (Z)Ljava/lang/Boolean; INVOKEVIRTUAL org/Test forTest (Ljava/lang/Boolean;)V LLOAD 2 INVOKESTATIC java/lang/Long valueOf (J)Ljava/lang/Long; ARETURN

Первая команда вызывает статический метод у класса System. Вторая запоминает результат вызова метода currentTimeMillis в переменной со вторым номером. Затем мы кладем переменную this, параметр метода и переменную с номером 2 на стек. Преобразую переменную к типу java/lang/Long. И проверяем, что она у нас содержится в коллекции, вызывая метод у параметра исполняемого. У нас параметр интерфейс, поэтому применяется команда INVOKEINTERFACE. Для метода класса необходимо использовать INVOKEVIRTUAL. Чтобы вызвать метод у объект или интерфейса необходимо, чтобы на стеке лежал объект, затем параметры вызываемого метода. В результате вызова метода они заменятся на результат или просто уберутся со стека, если метод ничего возвращает. Последняя три команды кладут перемену на стек, превращают ее в объект и возвращают ее как значение метода.

Чтобы завершить наш экскурс в байт-код, добавим последний метод и посмотрим на циклы и условные операторы.

public void testAriphmentics() < int i = -17; while(i < 10)< if(i < 0)< i = i + 7; >i = i*13; > >
ACC_FINAL -17 ISTORE 1 Label:L1466604866 ILOAD 1 ACC_FINAL 10 IF_ICMPGE L329949514 ILOAD 1 IFGE L658705244 ILOAD 1 ACC_FINAL 7 IADD ISTORE 1 Label:L658705244 ILOAD 1 ACC_FINAL 13 IMUL ISTORE 1 GOTO L1466604866 Label:L329949514 RETURN

Первые две команды инициализирую переменную i (с номером 1) значением -17.

Дальше у нас начинается тело цикла. Для реализации которого потребуются метки,
команда перехода и условный оператор. В теле нашего метода аж целых три меток. Первая метка означает начало цикла. Вторая нужна для условного оператора, а последняя знаменует конец цикла. Уловный оператор имеет один параметр метку перехода. Прежде чем его вызвать сравниваемы значения должны лежать на стеке. Для сравнения с нулем используется отдельная команда. Для каждого типа есть свой оператор сравнения. Для int он IF_ICMPGE. После сравнения сравниваемые значения убираются со стека. Для арифметических действий с двумя переменным их так же как и для условного оператора нужно предварительно положить на стек. После выполнения они снимаются со стека, а на их место кладется результат.

На это краткий экскурс в байт-код закончен, некоторые вопросы такие как исключения, синхронизация не были затронуты. Я надеюсь, что имея представление о байт коде читатель без труда справиться с ними. В следующей части мы рассмотрим инструменты которые применяются для модификации байт-кода.

Источник

Оцените статью