какая команда выполняет только компиляцию кода проекта

Инструменты для запуска и разработки Java приложений, компиляция, выполнение на JVM

Ни для кого не секрет, что на данный момент Java — один из самых популярных языков программирования в мире. Дата официального выпуска Java — 23 мая 1995 года.

Эта статья посвящена основам основ: в ней изложены базовые особенности языка, которые придутся кстати начинающим “джавистам”, а опытные Java-разработчики смогут освежить свои знания.

* Статья подготовлена на основе доклада Евгения Фраймана — Java разработчика компании IntexSoft.
В статье присутствуют ссылки на внешние материалы
.

какая команда выполняет только компиляцию кода проекта

1. JDK, JRE, JVM

Java Development Kit — комплект разработчика приложений на языке Java. Он включает в себя Java Development Tools и среду выполнения Java — JRE (Java Runtime Environment).

Java development tools включают в себя около 40 различных тулов: javac (компилятор), java (лаунчер для приложений), javap (java class file disassembler), jdb (java debugger) и др.

Среда выполнения JRE — это пакет всего необходимого для запуска скомпилированной Java-программы. Включает в себя виртуальную машину JVM и библиотеку классов Java — Java Class Library.

JVM — это программа, предназначенная для выполнения байт-кода. Первое преимущество JVM — это принцип “Write once, run anywhere”. Он означает, что приложение, написанное на Java, будет работать одинаково на всех платформах. Это является большим преимуществом JVM и самой Java.

До появления Java, многие компьютерные программы были написаны под определенные компьютерные системы, а предпочтение отдавалось ручному управлению памятью, как более эффективному и предсказуемому. Со второй половины 1990-х годов, после появления Java, автоматическое управление памятью стало общей практикой.

Существует множество реализаций JVM, как коммерческих, так и с открытым кодом. Одна из целей создания новых JVM — увеличение производительности для конкретной платформы. Каждая JVM пишется под платформу отдельно, при этом есть возможность написать ее так, чтобы она работала быстрее на конкретной платформе. Самая распространённая реализация JVM — это JVM Hotspot от OpenJDK. Также есть реализации IBM J9, Excelsior JET.

2. Выполнение кода на JVM

Согласно спецификации Java SE, для того, чтобы получить код, работающий в JVM, необходимо выполнить 3 этапа:

3. Загрузчики классов и их иерархия

Вернемся к загрузчикам классов — это специальные классы, которые являются частью JVM. Они загружают классы в память и делают их доступными для выполнения. Загрузчики работают со всеми классами: и с нашими, и с теми, которые непосредственно нужны для Java.

Представьте ситуацию: мы написали свое приложение, и помимо стандартных классов там есть наши классы, и их очень много. Как с этим будет работать JVM? В Java реализована отложенная загрузка классов, иными словами lazy loading. Это значит, что загрузка классов не будет выполняться до тех пор, пока в приложении не встретится обращение к классу.

Иерархия загрузчиков классов

какая команда выполняет только компиляцию кода проекта

Первый загрузчик классов — это Bootstrap classloader. Он написан на C++. Это базовый загрузчик, который загружает все системные классы из архива rt.jar. При этом, есть небольшое отличие между загрузкой классов из rt.jar и наших классов: когда JVM загружает классы из rt.jar, она не выполняет все этапы проверки, которые выполняются при загрузке любого другого класс-файла т.к. JVM изначально известно, что все эти классы уже проверены. Поэтому, включать в этот архив какие-либо свои файлы не стоит.

Следующий загрузчик — это Extension classloader. Он загружает классы расширений из папки jre/lib/ext. Допустим, вы хотите, чтобы какой-то класс загружался каждый раз при старте Java машины. Для этого вы можете скопировать исходный файл класса в эту папку, и он будет автоматически загружаться.

Еще один загрузчик — System classloader. Он загружает классы из classpath’а, который мы указали при запуске приложения.

Процесс загрузки классов происходит по иерархии:

4. Структура Сlass-файлов и процесс загрузки

Перейдем непосредственно к структуре Class-файлов.

Все числа, строки, указатели на классы, поля и методы хранятся в Сonstant pool — области памяти Meta space. Описание класса хранится там же и содержит имя, модификаторы, супер-класс, супер-интерфейсы, поля, методы и атрибуты. Атрибуты, в свою очередь, могут содержать любую дополнительную информацию.

Таким образом, при загрузке классов:

5. Исполнение байт-кода на JVM

В первую очередь, для исполнения байт-кода, JVM может его интерпретировать. Интерпретация — довольно медленный процесс. В процессе интерпретации, интерпретатор “бежит” построчно по класс-файлу и переводит его в команды, которые понятны JVM.

Также JVM может его транслировать, т.е. скомпилировать в машинный код, который будет исполняться непосредственно на CPU.

Команды, которые исполняются часто, не будут интерпретироваться, а сразу будут транслироваться.

6. Компиляция

Компилятор — это программа, которая преобразует исходные части программ, написанные на языке программирования высокого уровня, в программу на машинном языке, “понятную” компьютеру.

Компиляторы делятся на:

Также компиляторы могут классифицироваться по моменту компиляции:

7. Организация памяти в Java

Стек — это область памяти в Java, которая работает по схеме LIFO — “Last in — Fisrt Out” или “Последним вошел, первым вышел”.

какая команда выполняет только компиляцию кода проекта

Он нужен для того, чтобы хранить методы. Переменные в стеке существуют до тех пор, пока выполняется метод в котором они были созданы.

Когда вызывается любой метод в Java, создается фрейм или область памяти в стеке, и метод кладется на его вершину. Когда метод завершает выполнение, он удаляется из памяти, тем самым освобождая память для следующих методов. Если память стека будет заполнена, Java бросит исключение java.lang.StackOverFlowError. К примеру, это может произойти, если у нас будет рекурсивная функция, которая будет вызывать сама себя и памяти в стеке не будет хватать.

Ключевые особенности стека:

Куча разбита на несколько более мелких частей, называемых поколениями:

какая команда выполняет только компиляцию кода проекта

Почему отказались от Permanent generation? В первую очередь, это из-за ошибки, которая была связана с переполнением области: так как Perm имел константный размер и не мог расширяться динамически, рано или поздно память заканчивалась, кидалась ошибка, и приложение падало.

Meta space же имеет динамический размер, и во время исполнения он может расширяться до размеров памяти JVM.

Ключевые особенности кучи:

Основываясь на информации выше, рассмотрим, как происходит управление памятью на простом примере:

У нас есть класс App, в котором единственный метод main состоит из:

— примитивной переменой id типа int со значением 23
— ссылочной переменной pName типа String со значением Jon
— ссылочной переменной p типа person

какая команда выполняет только компиляцию кода проекта

Как уже упоминалось, при вызове метода на вершине стека создаётся область памяти, в которой хранятся данные, необходимые этому методу для выполнения.
В нашем случае, это ссылка на класс person: сам объект хранится в куче, а в стеке хранится ссылка. Также в стек кладется ссылка на строку, а сама строка хранится в куче в String pool. Примитив хранится непосредственно в стеке.

Для вызова конструктора с параметрами Person (String) из метода main() в стеке, поверх предыдущего вызова main() создается в стеке отдельный фрейм, который хранит:

this — ссылка на текущий объект
— примитивное значение id
— ссылочную переменную personName, которая указывает на строку в String Pool.

После того, как мы вызвали конструктор, вызывается setPersonName(), после чего снова создается новый фрейм в стеке, где хранятся те же данные: ссылка на объект, ссылка на строку, значение переменной.

Таким образом, когда выполнится метод setter, фрейм пропадет, стек очистится. Далее выполняется конструктор, очищается фрейм, который был создан под конструктор, после чего метод main() завершает свою работу и тоже удаляется из стека.

Если будут вызваны другие методы, для них будут также созданы новые фреймы с контекстом этих конкретных методов.

8. Garbage collector

В куче работает Garbage collector — программа, работающая на виртуальной машине Java, которая избавляется от объектов, к которым невозможно получить доступ.

Разные JVM могут иметь различные алгоритмы сборки мусора, также существуют разные сборщики мусора.

Мы поговорим о самом простом сборщике Serial GC. Сборку мусора мы запрашиваем при помощи System.gc().

какая команда выполняет только компиляцию кода проекта

Как уже было упомянуто выше, куча разбита на 2 области: New generation и Old generation.

New generation (младшее поколение) включает в себя 3 региона: Eden, Survivor 0 и Survivor 1.

Old generation включает в себя регион Tenured.

Что происходит, когда мы создаем в Java объект?

В первую очередь объект попадает в Eden. Если мы создали уже много объектов и в Eden уже нет места, срабатывает сборщик мусора и освобождает память. Это, так называемая, малая сборка мусора — на первом проходе он очищает область Eden и кладёт “выжившие” объекты в регион Survivor 0. Таким образом регион Eden полностью высвобождается.

Если произошло так, что область Eden снова была заполнена, garbage collector начинает работу с областью Eden и областью Survivor 0, которая занята на данный момент. После очищения выжившие объекты попадут в другой регион — Survivor 1, а два остальных останутся чистыми. При последующей сборке мусора в качестве региона назначения опять будет выбран Survivor 0. Именно поэтому важно, чтобы один из регионов Survivor всегда был пустым.

JVM следит за объектами, которые постоянно копируются и перемещаются из одного региона в другой. И для того, чтобы оптимизировать данный механизм, после определённого порога сборщик мусора перемещает такие объекты в регион Tenured.

Когда в Tenured места для новых объектов не хватает, происходит полная сборка мусора — Mark-Sweep-Compact.

какая команда выполняет только компиляцию кода проекта

Во время этого механизма определяется, какие объекты больше не используются, регион очищается от этих объектов, и область памяти Tenured дефрагментируется, т.е. последовательно заполняется нужными объектами.

Источник

Компиляция JavaScript проекта с помощью Maven и Closure Compiler

Добрый день, коллеги!

Хотел поделиться своими наработками в области автоматизации процесса сборки javascript проекта использующего Google Closure Compiler и Google Closure Library при помощи Apache Maven. Страничка проекта https://github.com/urmuzov/closure-maven, там же лежит документация по каждому из компонентов проекта.

О проекте

Главный компонент проекта — это архетип. Архетип объединяет в себя все остальные компоненты проекта, которые при желании могут использоваться отдельно от него.

Cоздание проекта

Создать проект при помощи этого архетипа очень просто, достаточно выполнить команду (вам потребуется maven версии 3):

Maven сгенерирует такую иерархию каталогов и файлов: ( //так обозначены мои комментарии )

Для того чтобы разобраться что здесь для чего, попробуем собрать проект.

Сборка проекта

Для того чтобы собрать проект необходимо выполнить следующую команду. Вместо compiled можно использовать любой из 5 профилей.

Большие куски логов, которые не интересны с точки зрения архетипа я убрал.

Скомпилированный проект выглядит так:

Операции выполненные при сборке

В процессе сборки были выполнены следующие операции:

А при использовании профилей sources или sources-no-compile необходимо подключать три файла, вот так:

Интеграция с IDE

В принципе все IDE имеют схожий функционал для работы с maven.

Для быстрого переключения модулей в NetBeans есть выпадающий список в тулбаре, профили в списке появляются автоматически:
какая команда выполняет только компиляцию кода проекта

В IntelliJ IDEA тоже выбирается из выпадающего списка в тулбаре:
какая команда выполняет только компиляцию кода проекта
Но добавляются вручную:
какая команда выполняет только компиляцию кода проекта

Для Eclipse, я думаю, все делается аналогично.

Дополнительная информация

Источник

Виды компиляции в JVM: сеанс черной магии с разоблачением

Сегодня вашему вниманию предлагается перевод статьи, в котором на примерах разобраны варианты компиляции в JVM. Особое внимание уделено AOT-компиляции, поддерживаемой в Java 9 и выше.

Приятного чтения!

Полагаю, любой, кому доводилось программировать на Java, слышал о мгновенной компиляции (JIT), а, возможно, и о компиляции перед выполнением (AOT). Кроме того, не приходится объяснять, что такое «интерпретируемые» языки. В этой статье будет рассказано, каким образом все эти возможности реализованы в виртуальной машине Java, JVM.

Чтобы байт-код Java выполнял какую-либо конкретную работу, есть 3 возможности заставить его это сделать:

Ради полноты картины упомяну, что существует и четвертый подход – напрямую интерпретировать исходный код, но в Java так не принято. Так делается, например, в Python.
Теперь давайте разберемся, как “java” работает в качестве (1) интерпретатора (2) JIT-компилятора и/или (3) AOT-компилятора – и когда.

Если коротко – как правило, “java” делает и (1), и (2). Начиная с Java 9 возможен и третий вариант.

Итак, если скомпилировать и запустить вышеприведенный код, то вывод будет вполне ожидаемым (конечно, значения истекшего времени у вас получатся другими):

А теперь вопрос: является ли этот вывод результатом работы “java” как интерпретатора, то есть, вариант (1), “java” как JIT-компилятора, то есть, вариант (2) либо он каким-то образом связан с AOT-компиляцией, то есть, вариант (3)? В этой статье я собираюсь найти верные ответы на все эти вопросы.

Первый ответ, который хочется дать – скорее всего, здесь имеет место только (1). Я говорю «скорее всего», так как не знаю, не установлена ли здесь какая-либо переменная окружения, которая бы изменяла опции JVM, заданные по умолчанию. Если ничего лишнего не установлено, и именно так “java” работает по умолчанию, то здесь мы 100% наблюдаем именно вариант (1), то есть, код полностью интерпретируемый. Я в этом уверен, так как:

(Ради удобочитаемости я буду писать каждую опцию с новой строки)

В данном случае должно быть совершенно понятно, что мы исключаем все функции (последняя *) во всех классах из всех пакетов, которые начинаются с java, jdk и sun (имена пакетов разделяются символом /, и можно использовать *). Команда quiet приказывает JVM не писать ничего об исключенных классах, поэтому в консоль будут выведены лишь те, которые сейчас скомпилируются. Итак, я запускаю:

Посмотрим, к чему же мы пришли. Теперь у нас есть простой код, где мы запускаем нашу функцию 10 раз. Ранее я упоминал, что эта функция интерпретируется, а не компилируется, поскольку так указано в документации, а теперь мы видим ее в логах (при этом, не видим в логах компиляции, и это означает, что JIT-компиляции она не подвергается). Отлично, вы только что видели инструмент “java” в действии, интерпретирующий и только интерпретирующий нашу функцию в 100% случаев. Итак, можем поставить галочку, что с вариантом (1) разобрались. Переходим к (2), динамической компиляции.

Вот вывод (напоминаю, у меня пропущены строки, касающиеся java.lang.invoke.MethodHandle ):

Приветствуем (hello!) динамически скомпилированную функцию Test.f или Test:: сразу после вызова номер 5, ведь я задал для CompileThreshold значение 5. JVM интерпретирует функцию 5 раз, затем компилирует ее и, наконец, запускает скомпилированную версию. Поскольку функция скомпилирована, она должна выполняться быстрее, но здесь мы этого проверить не можем, так как эта функция ничего не делает. Думаю, это хорошая тема для отдельного поста.

Вывод получится длинным, и там будут строки вроде:

Как видите, соответствующие функции компилируются в нативный машинный код.

Наконец, обсудим вариант 3, AOT. Компиляция перед выполнением, AOT, была недоступна в Java до версии 9.

В JDK 9 появился новый инструмент, jaotc – как понятно из названия, это AOT-компилятор для Java. Идея такова: запускаем компилятор Java “javac”, потом AOT-компилятор для Java “jaotc”, после чего запускаем JVM “java” как обычно. JVM в обычном порядке выполняет интерпретацию и JIT-компиляцию. Однако, если в функции есть AOT-скомпилированный код, она прямо его и использует, а не прибегает к интерпретации или JIT-компиляции. Поясню: вы не обязаны запускать AOT-компилятор, он опционален, а если вы им и воспользуетесь – то можете скомпилировать им до выполнения лишь те классы, которые хотите.

В нашем выводе, среди прочего, будет:

Как и в случае с соответствующей опцией “java”, в данном случае опцию можно сопровождать файлом, для этого существует опция –compile-commands к jaotc. В JEP 295 приводятся соответствующие примеры, которые я не буду здесь показывать.

Можно задать несколько AOT-библиотек, разделенных запятыми.

Перед выводом программы Test эта команда показывает следующее:

Как и планировалось, загружается библиотека AOT, и используются AOT-скомпилированные функции.

Если вам интересно, можете запустить следующую команду и проверить, происходит ли JIT-компиляция.

Как и предполагалось, JIT-компиляции не происходит, поскольку методы в классе Test скомпилированы до выполнения и предоставлены в виде библиотеки.

Возможен вопрос: если мы предоставляем нативный код функций, то как JVM определяет, не является ли нативный код устаревшим/несвежим? В качестве заключительного примера давайте модифицируем функцию f и зададим для a значение 6.

Я сделал этого лишь для того, чтобы изменить файл класса. Теперь мы заставим “javac” скомпилировать и запустить ту же команду, что и выше.

Как видите, я не запускал “jaotc” после “javac”, поэтому код из библиотеки AOT сейчас старый и некорректный, а у функции f значение a=5.

Вывод команды “java” выше демонстрирует:

Это означает, что функции в данном случае были динамически скомпилированы, поэтому код, полученный в результате AOT-компиляции, не использовался. Итак, обнаружено изменение в файле класса. Когда компиляция выполняется при помощи “javac”, ее отпечаток заносится в класс, а отпечаток класса также хранится в библиотеке AOT. Поскольку новый отпечаток класса отличается от того, что сохранен в библиотеке AOT, нативный код, скомпилированный заранее (AOT) не использовался. Вот и все, что я хотел рассказать о последнем варианте компиляции, до выполнения.

В этой статье я попытался объяснить и проиллюстрировать на простых реалистичных примерах, как JVM выполняет код Java: интерпретируя его, компилируя динамически (JIT) или заранее (AOT) – причем, последняя возможность появилась только в JDK 9. Надеюсь, вы узнали что-то новое.

Источник

Просто о make

Меня всегда привлекал минимализм. Идея о том, что одна вещь должна выполнять одну функцию, но при этом выполнять ее как можно лучше, вылилась в создание UNIX. И хотя UNIX давно уже нельзя назвать простой системой, да и минимализм в ней узреть не так то просто, ее можно считать наглядным примером количество- качественной трансформации множества простых и понятных вещей в одну весьма непростую и не прозрачную. В своем развитии make прошел примерно такой же путь: простота и ясность, с ростом масштабов, превратилась в жуткого монстра (вспомните свои ощущения, когда впервые открыли мэйкфайл).

Мое упорное игнорирование make в течении долгого времени, было обусловлено удобством используемых IDE, и нежеланием разбираться в этом ‘пережитке прошлого’ (по сути — ленью). Однако, все эти надоедливые кнопочки, менюшки ит.п. атрибуты всевозможных студий, заставили меня искать альтернативу тому методу работы, который я практиковал до сих пор. Нет, я не стал гуру make, но полученных мною знаний вполне достаточно для моих небольших проектов. Данная статья предназначена для тех, кто так же как и я еще совсем недавно, желают вырваться из уютного оконного рабства в аскетичный, но свободный мир шелла.

Make- основные сведения

make — утилита предназначенная для автоматизации преобразования файлов из одной формы в другую. Правила преобразования задаются в скрипте с именем Makefile, который должен находиться в корне рабочей директории проекта. Сам скрипт состоит из набора правил, которые в свою очередь описываются:

1) целями (то, что данное правило делает);
2) реквизитами (то, что необходимо для выполнения правила и получения целей);
3) командами (выполняющими данные преобразования).

В общем виде синтаксис makefile можно представить так:

То есть, правило make это ответы на три вопроса:

Несложно заметить что процессы трансляции и компиляции очень красиво ложатся на эту схему:

Простейший Makefile

Предположим, у нас имеется программа, состоящая всего из одного файла:

Для его компиляции достаточно очень простого мэйкфайла:

Компиляция из множества исходников

Предположим, что у нас имеется программа, состоящая из 2 файлов:
main.c

Makefile, выполняющий компиляцию этой программы может выглядеть так:

Он вполне работоспособен, однако имеет один значительный недостаток: какой — раскроем далее.

Инкрементная компиляция

Представим, что наша программа состоит из десятка- другого исходных файлов. Мы вносим изменения в один из них, и хотим ее пересобрать. Использование подхода описанного в предыдущем примере приведет к тому, что все без исключения исходные файлы будут снова скомпилированы, что негативно скажется на времени перекомпиляции. Решение — разделить компиляцию на два этапа: этап трансляции и этап линковки.

Теперь, после изменения одного из исходных файлов, достаточно произвести его трансляцию и линковку всех объектных файлов. При этом мы пропускаем этап трансляции не затронутых изменениями реквизитов, что сокращает время компиляции в целом. Такой подход называется инкрементной компиляцией. Для ее поддержки make сопоставляет время изменения целей и их реквизитов (используя данные файловой системы), благодаря чему самостоятельно решает какие правила следует выполнить, а какие можно просто проигнорировать:

Попробуйте собрать этот проект. Для его сборки необходимо явно указать цель, т.е. дать команду make hello.
После- измените любой из исходных файлов и соберите его снова. Обратите внимание на то, что во время второй компиляции, транслироваться будет только измененный файл.

После запуска make попытается сразу получить цель hello, но для ее создания необходимы файлы main.o и hello.o, которых пока еще нет. Поэтому выполнение правила будет отложено и make станет искать правила, описывающие получение недостающих реквизитов. Как только все реквизиты будут получены, make вернется к выполнению отложенной цели. Отсюда следует, что make выполняет правила рекурсивно.

Фиктивные цели

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

Командой make производят компиляцию программы, командой make install — установку. Такой подход весьма удобен, поскольку все необходимое для сборки и развертывания приложения в целевой системе включено в один файл (забудем на время о скрипте configure). Обратите внимание на то, что в первом случае мы не указываем цель, а во втором целью является вовсе не создание файла install, а процесс установки приложения в систему. Проделывать такие фокусы нам позволяют так называемые фиктивные (phony) цели. Вот краткий список стандартных целей:

Теперь мы можем собрать нашу программу, произвести ее инсталлцию/деинсталляцию, а так же очистить рабочий каталог, используя для этого стандартные make цели.

Обратите внимание на то, что в цели all не указаны команды; все что ей нужно — получить реквизит hello. Зная о рекурсивной природе make, не сложно предположить как будет работать этот скрипт. Так же следует обратить особое внимание на то, что если файл hello уже имеется (остался после предыдущей компиляции) и его реквизиты не были изменены, то команда make ничего не станет пересобирать. Это классические грабли make. Так например, изменив заголовочный файл, случайно не включенный в список реквизитов, можно получить долгие часы головной боли. Поэтому, чтобы гарантированно полностью пересобрать проект, нужно предварительно очистить рабочий каталог:

Для выполнения целей install/uninstall вам потребуются использовать sudo.

Переменные

Все те, кто знакомы с правилом DRY (Don’t repeat yourself), наверняка уже заметили неладное, а именно — наш Makefile содержит большое число повторяющихся фрагментов, что может привести к путанице при последующих попытках его расширить или изменить. В императивных языках для этих целей у нас имеются переменные и константы; make тоже располагает подобными средствами. Переменные в make представляют собой именованные строки и определяются очень просто:

Существует негласное правило, согласно которому следует именовать переменные в верхнем регистре, например:

Ниже представлен мэйкфайл, использующий две переменные: TARGET — для определения имени целевой программы и PREFIX — для определения пути установки программы в систему.

Это уже посимпатичней. Думаю, теперь вышеприведенный пример для вас в особых комментариях не нуждается.

Автоматические переменные

Автоматические переменные предназначены для упрощения мейкфайлов, но на мой взгляд негативно сказываются на их читабельности. Как бы то ни было, я приведу здесь несколько наиболее часто используемых переменных, а что с ними делать (и делать ли вообще) решать вам:

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *