Pull to refresh

Comments 63

Для сравнения стоило бы привести размеры этого же кода при компиляции с -source 1.5 -target 1.5: 711 и 7 978 байтов соответственно.
Class-файл версии 50 и ниже так просто не растаращишь.
Дельное замечание. Вот они, передовые технологии!
Путь для исключения формируется даже в том случае, если в блоке try совсем ничего предосудительного. Например, присваивание целочисленной константы в локальную переменную — это две инструкции iconst и istore, ни про одну из них в спецификации не сказано, что они могут сгенерировать исключение.
Тут вы не правы. Параграф 2.10 JVMS говорит:
An asynchronous exception, by contrast, can potentially occur at any point in the execution of a program.
An asynchronous exception occurred because:
* The stop method of class Thread or ThreadGroup was invoked, or
* An internal error occurred in the Java Virtual Machine implementation.
Таким образом JVM должна быть готова к тому, что во время исполнения любой инструкции может быть выброшено исключение.
И в этих случаях действительно будет выполняться finally-блок?

Почитал, в случае Thread.stop выполнится. Ваша правда.
Я слышал, что есть заказчики, которые платят за строчку кода. Было было забавно найти заказчиков, которые платят за килобайт байт-кода и провернуть такое.
Кстати, если заказчики, которые требуют 80% покрытие кода тестами. Если считать по байткоду (как, например, EMMA делает), с таким методом возникнут очень большие проблемы.
Наоборот, можно только один этот класс и покрыть — а остальной проект в 20% уложится!
А как вы его собрались покрывать? Там большая часть кода исполняется только когда звёзды встанут в очень специфическую конфигурацию.
Можно случайным образом вбрасывать исключения из разных мест. Правда, может не скомпилироваться.
Я слышал, что есть заказчики, которые платят за строчку кода.

О, да с удовольствием!

HelloWriter.java:

public class HelloWriter {
    public void doIt () {
        System.out.println ("Hello world!");
    }
}

Main.java:

public class Main {
    public static void main (String[] args) {
        HelloWriter writer = new HelloWriter ();
        writer.doIt ();
    }
}

javac HelloWriter.java Main.java
java Main
А ещё можно было открывающую скобочку с новой строки печатать. И параметры метода по своим отдельным строчкам. И инициализировать отдельной строчкой, а не сразу при об'явлении. И вообще всё по блочным операторам разбить. Заменить на printf с параметрами и try/catch ещё добавить. Конструктор по-умолчанию добавить. А package где?
Плохо работаете.
То, что вы предложили — для меня зло, ибо так меня научили, поэтому как я мог стать тем, с чем так борюсь?) Я усложнил код, однако и усложнение должно быть в меру. Пусть дальше усложняют любители чего-нибудь погорячее, я предложил саму парадигму)
Разве на этом заработаешь? Надо примерно так.

Техническое задание: написать программу печатающую Hello world!
Шаг 1:
Как известно константы в коде это не наш метод, поэтому добавим программе гибкости и будем хранить их вне кода. Для гибкости в json, xml, ymi и properites формате. На работу с этими форматами запросим мелочь по тысячи строк с каждого.

Шаг 2:
Естественно, реализуем фабрику, абстрактную фабрику и систему редеров и лодеров всех этих файлов. Ну, пара тысяч строк.

Шаг 3:
Вспомним про мультиязыковую поддержку и создадим файлы на 50 мировых языках, реализуем поддержку всех возможных кодировок. Мы ведь хотим чтобы у нас было гибкое приложение? Ну, ещё тысяч пять строк.

Шаг 4:
Нет, ну как в нашем системе без unit tes'ов? Никак, увеличем кол-во строк ещё вдвое.

Шаг 5:
А ещё интеграционные тесты, ну пару тысяч. Это мелочь.

Шаг 6:
Таак, а если пользователь захочет поменять сообщение сам? Нет, мы за гибкость. Реализуем Rest Api, web интерфейс, desktop интерфейс для основных ОС и делаем приложение для основных мобильных платформ. Ну, тут без 50 тысяч строк никак.

Шаг 7:
Нам ведь строку надо где-то хранить, правильно? Релизуем работу с основными СУБД (мало ли что заказчик захочет), вспомним про nosql и поддержим несколько nosql систем.

Шаг 8:
Ну да, юнит тесты, интеграционные тест, автоматические тесты, тесты производительности. Все по правильному. Умножаем все строчки кода ещё раза в три.

Шаг 9:
Вспоминаем про многопоточность и реализуем коллизии одновременных изменений строки hello word. Вспомнием про отказоустойчивость, обработку ошибок, логирование…

Шаг…

Шаг N:
Понимаем что технологии на шагах 1..N-1 уже устарели, их выкидываем, делаем рефакторинг и делаем все заново.

Шаг…

Итого: это проект в несколько миллионов строк кода и много лет работы команды из сотни человек.
Насчёт наскольких миллионов строк вы, пожалуй, загнули, но многое из вам описанного в GNU hello всё-таки есть. И локализация, и включение/выключение псевдографики и многое другое.
Хо-хооооо! Я должен внести это в Избранное! Это просто гениально! Это ладно реализовать, но это ж надо же такое предложить!)))
Спасибо за коэффициент растаращивания, прям чувствуется дух пятницы )
Ради интереса взял аналогичный код на C#:
Код
using System;

namespace TestConsole
{
    public class Program
    {

        public static void Main(string[] args)
        {
            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 {
            }}}}}}}}}}}}

            Console.WriteLine(a);
        }

        // Точка входа для Java
        public static void main(string[] args)
        {
            Main(args);
        }
    }
}

Скомпилировал в .NET и натравил на него свой компилятор CIL2Java. Сильно не углублялся что к чему, но статистика получилась такая:
Размер class файла: 184 915 байт
Пул констант: 4127 записей (из за отладочной информации)
Размер кода метода: 40 949 байт
А вот локальных переменных потребовалось всего 14 (CIL2Java всё таки освобождает временную переменную когда она становится не нужна)
Записей в таблице исключений: 4095 (что равно 32 760 байт)
Вот вкратце получилось так. Можно сделать вывод что мой компилятор генерирует даже более компактный код чем java =))
О, хорошая проверка. Интересно, а что конкретно в пуле констант такое большое добавилось?
Имена для временных переменных. Не смотря на то, что переменные в результате переиспользуются, для каждого отдельного использования генерируется новое имя. Вот и получилось столько.
А, вона что. Javac не заморачивается давать им имена. Может, конечно, настройка какая-то есть…
А какую версию байткода поддерживает ваш компилятор на выходе? Или может выложите куда-нибудь получившийся .class?
Версия class файла — 49, но сам по себе байт код соответствует больше 1.6 и выше. По крайней мере try/finally блоки генерируются похожим образом. class файл — здесь
Class-файл версии 50 и ниже так просто не растаращишь.
Там выше был пример с полуторкой. Собственно потому и результат такой.
Вы не поняли. У zebraxxl свой компилятор, самописный, не имеющий ничего общего с javac. Вот, почитайте его статью. Javac в режиме 1.5 работает через инструкцию JSR, но в class-файлах 51+ она запрещена (видимо, как раз для упрощения верификации). Хотя в 49-й можно было использовать JSR, но zebraxxl не пользуется ей при генерации. Другое дело, что он не создаёт StackMapTable и переиспользует переменные, поэтому он экономит на другом (в частности, обходится без aload_w, что уже снижает длину байткода).
А можно в двух словах пояснить — зачем?
because I can?
По-моему, это хороший пример почему крайне аккуратно стоит использовать вложенные finally блоки.
А! Каюсь, в этом аспекте не рассматривал. Спасибо.
О как. Минус за просьбу пояснить? Хабр торт.
Ваш комментарий имеет два смысла — это и просьба объяснить, и одновременно упрек автору поста.

Также хочу отметить, что минус комментарию означает всего лишь «мне не понравился этот комментарий», а то что негативный комментарий кому-то не понравился — это совершенно нормально.

Вот если бы вам за такой комментарий прилетел минус в карму (что равносильно «я не хочу видеть тут этого человека») — это было бы неправильно и можно было бы возмущаться.
Возмущаться как правило чревато прилетом еще пачки «не хочу видеть тут этого человека».
Если возмущение некорректное — то да. А если вежливое и по делу — иногда и компенсация прилететь может…
Вообще я разбирался с генерацией кода finally для FindBugs: из-за анализа всех альтернативных веток иногда возникают ложные срабатывания в моём новом детекторе, который отлавливает некоторые заведомо бесполезные условия в коде. В смысле, найденные условия действительно бесполезны, но они добавлены компилятором автоматически, а не автором кода, поэтому сообщать о них бессмысленно. Сейчас стоит задача по заданному байт-коду восстановить, какие фрагменты были в finally-блоках. Данная статья — смешной побочный эффект этой работы. Но даже несмотря на её несерьёзность, я лучше понял процедуру компиляции и устройство StackMapTable (её тоже мы в FindBugs разбираем иногда) и поделился информацией с другими. Кроме того, выше были дельные комментарии, которые способствовали обмену знаниями. Наконец, подобные исследования из игры могут привести к важным последствиям: к примеру, можно сочинить вектор DoS-атаки на какие-нибудь системы, которые автоматически компилируют фрагменты Java-кода из недоверенных источников.

Я достаточно занудно ответил на ваш вопрос? :-)
UFO just landed and posted this here
UFO just landed and posted this here
Этот пост стоит плюсовать только из-за потрясающего глагола в заголовке.
Не пишу на Java, почитываю иногда статьи для расширения кругозора.
Это статья была отменной: одновременно и поучительная и пятничная!
А глагол, да — в мемориз, однозначно.

Реквестирую подобную статью под c#, никто не хочет подготовить небольшое исследование?
Могу предложить перевести цикл Abusing C# от Джона Скита (Jon Skeet). Пусть библиотеки там не растаращивает, но компиляторы вполне.
На C# растаращить можно ещё проще. Рассмотрим вот такой вот код:

class X<A, B, C, D, E> { class Y : X<Y, Y, Y, Y, Y> { Y.Y.Y.Y.Y.Y.Y.Y.Y y;} }

И — вуаля, сборочка весит уже 29 мегабайт. Можно растаращить ещё больше, но это я оставлю в качестве упражнения для читателя :)
Не знаток C#, так что поясните пожалуйста за счет чего здесь происходит растаращивание. Т.е. в примере из статьи причина всему — инлайн finally блоков, а здесь что? Предположу, что генерация классов.
Сходу не могу назвать конкретной причины. Постараюсь изучить вопрос и ответить отдельным постом :)
Было бы интересно почитать! :)
по-идее сама сборка должна весить копейки.Странное поведение если нет
А вы попробуйте сами, соберите. Этот пример кода довольно старый (но я проверил — на компиляторе из VS 2013 воспроизводится); пару лет назад я хотел было разобраться, что ж там такое хранится, но тогдашний рефлектор не сумел разобрать такую большую сборку, на том исследования в тот раз и закончились.
<предположение>
Generic-классы вроде бы компилируются в «невидимые» классы с именами, не соответствующими стандартам именования C#, что бы точно не пересечься с именами классов разработчика.
Соответственно, для каждого вложенного поля нужно было создать отдельный класс с конструктором, таблицей методов и т.д.
Видимо, рекурсия в этом случае генерит очень сложный код.
</предположение>
Не совсем верно. Именования дженериков классов действительно несколько отличаются от стандартных. Но все отличие заключается лишь в том, что к имени класса добавляется суфикс "`N", где N — количество дженерик параметров. И нужно лишь для того, что бы не было пересечений имен в случаях когда есть три класса: C, C и C<T, F> (после компиляции превратятся в C, C`1 и C`2 соответственно). Это описано в стандарте ECMA-335, раздел II.9, второй абзац:
Цитата
A generic type consists of a name followed by a <…>-delimited list of generic parameters, as in C.
Two or more generic types shall not be defined with the same name, but different numbers of generic
parameters, in the same scope. However, to allow such overloading on generic arity at the source
language level, CLS Rule 43 is defined to map generic type names to unique CIL names. That Rule
states that the CLS-compliant name of a type C having one or more generic parameters, shall have a
suffix of the form `n, where n is a decimal integer constant (without leading zeros) representing the
number of generic parameters that C has. For example: the types C, C, and C<K,V> have CLScompliant
names of C, C`1, and C`2<K,V>, respectively. [Note: The names of all standard library
types are CLS-compliant; e.g., System.Collections.Generic.IEnumerable`1. end note]
Я не у Parallels это словечко подхватил, честно :-)
«Коэффициент растаращивания» — записал себе термин ))
Кстати, на практике не пробовал, но по идее похожего эффекта можно достичь при использовании switch со строкой, компилятор это превращает в два tableswitch'a (или в один lookup- и один tableswitch).
Эффекта экспоненциального роста не будет, и даже квадратичного не получится. Будет всего одна копия байт-кода на каждый case.
автор — демон!!!

а если серьёзно, то если в проекте используется кодогенератор, на лету оборачивающий генерируемый код в блоки try-finally, это будет довольно неплохой способ изготовить classloader bomb.

я один такой проект встречал вживую, правда, довольно давно. думаю, при попытке запуска этого приложения со сколько-нибудь сложным скриптом на java 7 его точно бы порвало.

полезное знание, спасибо.
Ну, значит все-таки не зря переводил :)
Я вашу статью постфактум увидал, уже когда сам со всем этим разобрался. Но да, всяко не зря :-)
В 2019 году скомпилировал класс из конца статьи 11 джавой (liberica jdk 11.0.4). class файл получился 101 мегабайт.

Потом в IDEA сконвертировал его в Kotlin (стандартно — по Ctrl-Alt-Del-K) и скомпилировал котлином 1.3.50.

class файл получился чуть меньше четырех с половиной мегабайт.

Однако! Для Котлина, получается, надо как-то по другом растаращить :-)
Sign up to leave a comment.

Articles