Мой новый блог

середа, 16 вересня 2009 р.

Структура байт-коду віртуальної машини 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) (
Long a = System.curretM ();
forTest (testInterface.contains (a));
return a;
)


testMethod має такі уявлення.

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. Після порівняння сравніваеми значення забираються зі стека. Для арифметичних дій з двома змінним їх так само як і для умовного оператора потрібно попередньо покласти на стек. Після виконання вони знімаються з стека, а на їх місце кладеться результат.

На це короткий екскурс в байт-код закінчено, деякі питання такі як виключення, синхронізація не були порушені. Я сподіваюся, що маючи уявлення про байт коді читач легко впоратися з ними. У наступній частині ми розглянемо інструменти які застосовуються для модифікації байт-коду.

http://math-and-prog.blogspot.com

Немає коментарів:

Мой блог