http://habrahabr.ru/post/245333/
Обычно при компиляции Java-файла получаются .class-файлы примерно того же размера, что и исходник. Меня заинтересовало, можно ли по небольшому исходнику сделать .class-файл, который больше, сильно больше исходника.
Можно поискать какие-то короткие конструкции языка, которые компилируются в длинные цепочки байткода, но линейный прирост меня не устраивал. Я сразу подумал про компиляцию finally-блоков: про неё уже
писали на Хабре. Если вкратце, то для каждого finally-блока при непустом try-блоке создаётся минимум два варианта в байткоде: для случая нормального завершения try-блока и для случая завершения с исключением. В последнем случае исключение сохраняется в новую локальную переменную, выполняется код finally, затем исключение достаётся из локальной переменной и перебрасывается. А что если внутри finally снова разместить try-finally и так далее? Результат превзошёл все ожидания.
Я компилировал с помощью Oracle javac 1.7.0.60 и 1.8.0.25, результаты практически не отличались. Путь для исключения формируется даже в том случае, если в блоке try совсем ничего предосудительного. Например, присваивание целочисленной константы в локальную переменную — это две инструкции
iconst и
istore, ни про одну из них в спецификации не сказано, что они могут сгенерировать исключение. Так и будем писать:
class A {{
int a;
try {a=0;} finally {
try {a=0;} finally {
try {a=0;} finally {
try {a=0;} finally {
try {a=0;} finally {
try {a=0;} finally {
try {a=0;} finally {
try {a=0;} finally {
try {a=0;} finally {
try {a=0;} finally {
try {a=0;} finally {
try {a=0;} finally {
a=0;
}}}}}}}}}}}}
}}
Добавление нового нетривиального кода в самый внутренний finally вызывает ошибку компиляции code too large, поэтому ограничимся таким. Если кто-то подзабыл, это у нас
блок инициализации, который подклеивается к каждому конструктору. Для нашей задачи метод объявлять смысла нет.
Такой исходник занимает
336 байт, а получившийся class-файл растаращило до
6 571 429 байт, то есть в
19 557 раз (назовём это коэффициентом растаращивания). Даже при отключении всей отладочной информации с помощью -g:none class-файл весит
6 522 221 байт, что ненамного меньше. Посмотрим, что внутри с помощью утилиты
javap.
Пул констант
Пул констант получился небольшой: всего 16 записей. По сути всё самое необходимое: имена атрибутов типа Code, имя класса, Java-файла, ссылка на конструктор родительского класса Object и т. д. При отключении отладочной информации исчезают три записи: имена атрибутов LineNumberTable, SourceFile и значение A.java для атрибута SourceFile.
Код
Код конструктора по умолчанию составил 64507 байт, почти упираясь в максимально допустимый предел. Начинается он с нормального выполнения:
Код 0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: iconst_0
5: istore_1
6: iconst_0
7: istore_1
8: iconst_0
9: istore_1
10: iconst_0
11: istore_1
12: iconst_0
13: istore_1
14: iconst_0
15: istore_1
16: iconst_0
17: istore_1
18: iconst_0
19: istore_1
20: iconst_0
21: istore_1
22: iconst_0
23: istore_1
24: iconst_0
25: istore_1
26: iconst_0
27: istore_1
28: iconst_0
29: istore_1
30: goto 38
То есть вызывается конструктор родительского класса, а затем 13 раз записывается единица в первую локальную переменную. После этого начинается длинная цепочка goto, которая обходит мимо всех остальных копий finally: 30->38->58->104->198->388->770->1536->3074->7168->15358->31740->64506, а по адресу 64506 мы находим долгожданную инструкцию return.
В промежутках между этими goto всевозможные комбинации нормальных и исключительных завершений каждого блока try. Неожиданным оказалось то, что для каждого finally, обрабатывающего исключение, для хранения исключения создаётся новая локальная переменная, даже если блоки заведомо взаимоисключающие. Из-за этого коду требуется 4097 локальных переменных. Небольшая статистика по инструкциям:
- iconst_1 — 8191 раз
- istore_1 — 8191 раз
- goto — 4095 раз
- athrow — 4095 раз
- astore_2/aload_2 — 1 раз
- astore_3/aload_3 — 1 раз
- astore/aload — 252 раза (локальные переменные с номерами от 4 до 255)
- astore_w/aload_w — 3841 раз (локальные переменные с номерами больше 255)
Плюс один aload_0, один invokespecial и один return — итого 32765 инструкций. Желающие могут нарисовать
граф потока управления и повесить на стенку.
Таблица исключений
Таблица исключений содержит записи вида (start_pc, end_pc, handler_pc, catch_type) и говорит виртуальной машине «если при выполнении инструкций от адреса start_pc до адреса end_pc произошло исключение типа catch_type, то передай управление на адрес handler_pc». В данном случае catch_type везде равен any, то есть исключения любого типа. Записей в таблице 8188 и занимает она примерно столько же, сколько и код — около 64 килобайт. Начало выглядит так:
from to target type
26 28 33 any
24 26 41 any
42 44 49 any
49 51 49 any
22 24 61 any
Таблица номеров строк
Таблица номеров строк — это отладочная информация, сопоставляющая адресам инструкций байткода номера строк в исходнике. В ней 12288 записей и чаще всего попадаются ссылки на строчку с самым внутренним finally. Занимает она около 48 килобайт.
StackMapTable
Куда же ушло всё остальное место? Его заняла таблица
StackMapTable, которая необходима для верификации class-файла. Если совсем грубо, для каждой точки ветвления в коде эта таблица содержит типы элементов в стеке и типы локальных переменных в данной точке. Так как локальных переменных у нас очень много и точек ветвления тоже, размер этой таблицы растёт квадратично от размера кода. Если бы локальные переменные для исключений в непересекающихся ветках переиспользовались, их бы потребовалось всего 13 и таблица StackMapTable была бы куда скромнее по размерам.
Таращим дальше
Можно ли растаращить class-файл ещё сильнее? Конечно, можно раскопировать метод, содержащий вложенные try-finally. Но компилятор вполне может сделать это за нас. Вспомните, что блок инициализации приклеивается к каждому конструктору автоматически. Достаточно добавить в код много пустых конструкторов с разными сигнатурами. Здесь будьте осторожны, а то у компилятора кончится память. Ну вот так можно скромненько написать, упаковав код в одну строчку:
class A{{int a;try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{try{a=0;}finally{a=0;}}}}}}}}}}}}}A(){}A(int a){}A(char a){}A(double a){}A(float a){}A(long a){}A(short a){}A(boolean a){}A(String a){}A(Integer a){}A(Float a){}A(Short a){}A(Long a){}A(Double a){}A(Boolean a){}A(Character a){}}
Здесь у меня 16 конструкторов, исходник занимает
430 байт. После компиляции имеем
104 450 071 байт, коэффициент растаращивания составил
242 907. И это не предел!