Как растаращить class-файл

31823d27b8b6404386b1fb2811933e2d.gifОбычно при компиляции 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.»»:()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. И это не предел!

© Habrahabr.ru