Как компилируется java
Компиляцией исходного кода программы в байт-код занимается компилятор javac . Затем этот байт-код интерпретируется JVM в машинный код. Такая последовательность действий позволяет выполнять байт-код на любой платформе. Также для ускорения работы приложений Java используется JIT компилятор, который компилирует байт-код, выполняющийся много раз, в машинный. При каждом вызове такого участка кода JVM будет сразу вызывать скомпилированный машинный код, а не интерпретировать его каждый раз.
Напишем небольшую программу, которая будет заполнять массив из 10 элементов случайными числами, а затем считать количество четных и выводить это значение на экран.
import java.util.Arrays; import java.util.Random; public class Example public static void main(String[] args) int[] arr = createArrayWithRandomNumbers(); System.out.println(Arrays.toString(arr)); // => [57, 96, 22, 25, 24, 86, 30, 48, 23, 12] System.out.println(countEvenNumbers(arr)); // => 7 > // Метод проходит по массиву в цикле и считает // количество четных чисел private static int countEvenNumbers(int[] arr) int count = 0; for (int elem: arr) if (elem % 2 == 0) count++; > > return count; > // Метод создает массив из 10 элементов и заполняет // его случайными значениями с верхней границей - 100, // не включительно private static int[] createArrayWithRandomNumbers() int size = 10; int upperBound = 100; int[] arr = new int[size]; Random random = new Random(); for (int i = 0; i arr.length; i++) arr[i] = random.nextInt(upperBound); > return arr; > >
Чтобы скомпилировать эту программу в байт-код, воспользуемся компилятором javac в командной строке:
javac Example.java
После компиляции получаем файл с байт-кодом Example.class , который можно запустить при помощи команды java в терминале:
java Example
Будет выведено содержимое массива и количество четных чисел.
Компиляция и запуск приложения из командной строки
Для компиляции программ из командной строки (консоли) используется команда javac.
javac [ключи] [исходники]
Чтобы получить список возможных ключей, выполните:
javac –help
Чтобы скомпилировать программу MyFirstApp , запустите компилятор, указав имя исходного файла в командной строке следующим образом:
javac MyFirstApp.java
Компилятор javac создаст файл MyFirstApp.class , содержащий версию байт-кода.
В процессе компиляции исходного кода каждый отдельный класс помещается в собственный выходной файл, называемый по имени класса и получающий расширение .class .
2. Компиляция с помощью -d.
По умолчанию компилятор записывает сгенерированный .class в тот же каталог, где находится исходный класс .java . Это хорошо для маленьких проектов, но при работе на больших проектах файлы .java и .class должны храниться отдельно.
Ключ -d позволяет указать компилятору, куда записывать сгенерированные .class файлы.
Например, у нас есть такая структура папок:
Следующая команда, выполненная из project1 папки сгенерирует файл MyFirstApp.class и запишет его в папку classes (предполагаем, что MyFirstApp не содержит оператора package ):
cd project1 javac -d classes src/MyFirstApp.java
Теперь давайте рассмотрим пример компиляции класса com.company.lesson1.MyFirstApp , находящегося в пакете com.company.lesson1 . Имеется такая структура каталогов:

Следующая команда, выполненная из src каталога, сгенерирует файл MyFirstApp.class , и запишет его в каталог classes/com/company/lesson1:
cd myProject/src javac -d ../classes com/company/lesson1/MyFirstApp.java
Если каталог classes/com/company/lesson1 не существует, то он будет создан при компиляции.
Если же не существует каталог classes, то вы получите ошибку компиляции.
3. Запуск программы с помощью команды java.
Чтобы выполнить программу из командной строки, следует воспользоваться загрузчиком приложений Jаvа, который называется java.
java [ключи] класс [аргументы]
Для получения списка возможных ключей, выполните:
java –help
Чтобы выполнить программу, передадим имя класса MyFirstApp (предполагаем, что MyFirstApp не содержит оператора package ) в качестве аргумента командной строки:
java MyFirstApp
Если класс MyFirstApp находится в пакете, то выполняем команду:
java com.company.lesson1.MyFirstApp
Стоит заметить, что при запуске программы указывается только один класс без расширения .class .
4. Ключевые моменты
- В процессе компиляции исходного кода каждый отдельный класс помещается в собственный выходной файл, называемый по имени класса и получающий расширение .class .
- По умолчанию компилятор записывает сгенерированный .class в тот же каталог, где находится исходный класс .java .
- Ключ -d указывает компилятору, куда записывать сгенерированные .class файлы.
- При компиляции могут быть созданы отсутствующие каталоги соответствующие пакету компилируемого класса.
- Если в ключе -d указан не существующий каталог, будет выброшена ошибка компиляции.
- При запуске программы указывается только один класс без расширения .class .
- -d может содержать относительные и абсолютные пути.
Презентацию с видео можно скачать на Patreon .
Процесс компиляции программ на C++
В данной статье я хочу рассказать о том, как происходит компиляция программ, написанных на языке C++, и описать каждый этап компиляции. Я не преследую цель рассказать обо всем подробно в деталях, а только дать общее видение. Также данная статья — это необходимое введение перед следующей статьей про статические и динамические библиотеки, так как процесс компиляции крайне важен для понимания перед дальнейшим повествованием о библиотеках.
Все действия будут производиться на Ubuntu версии 16.04.
Используя компилятор g++ версии:
$ g++ --version g++ (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609
Состав компилятора g++
Мы не будем вызывать данные компоненты напрямую, так как для того, чтобы работать с C++ кодом, требуются дополнительные библиотеки, позволив все необходимые подгрузки делать основному компоненту компилятора — g++.
Зачем нужно компилировать исходные файлы?
Исходный C++ файл — это всего лишь код, но его невозможно запустить как программу или использовать как библиотеку. Поэтому каждый исходный файл требуется скомпилировать в исполняемый файл, динамическую или статическую библиотеки (данные библиотеки будут рассмотрены в следующей статье).
Этапы компиляции:
Перед тем, как приступать, давайте создадим исходный .cpp файл, с которым и будем работать в дальнейшем.
driver.cpp:
#include using namespace std; #define RETURN return 0 int main()
1) Препроцессинг
Самая первая стадия компиляции программы.
Препроцессор — это макро процессор, который преобразовывает вашу программу для дальнейшего компилирования. На данной стадии происходит происходит работа с препроцессорными директивами. Например, препроцессор добавляет хэдеры в код (#include), убирает комментирования, заменяет макросы (#define) их значениями, выбирает нужные куски кода в соответствии с условиями #if, #ifdef и #ifndef.
Хэдеры, включенные в программу с помощью директивы #include, рекурсивно проходят стадию препроцессинга и включаются в выпускаемый файл. Однако, каждый хэдер может быть открыт во время препроцессинга несколько раз, поэтому, обычно, используются специальные препроцессорные директивы, предохраняющие от циклической зависимости.
Получим препроцессированный код в выходной файл driver.ii (прошедшие через стадию препроцессинга C++ файлы имеют расширение .ii), используя флаг -E, который сообщает компилятору, что компилировать (об этом далее) файл не нужно, а только провести его препроцессинг:
g++ -E driver.cpp -o driver.ii
Взглянув на тело функции main в новом сгенерированном файле, можно заметить, что макрос RETURN был заменен:
int main()
В новом сгенерированном файле также можно увидеть огромное количество новых строк, это различные библиотеки и хэдер iostream.
2) Компиляция
На данном шаге g++ выполняет свою главную задачу — компилирует, то есть преобразует полученный на прошлом шаге код без директив в ассемблерный код. Это промежуточный шаг между высокоуровневым языком и машинным (бинарным) кодом.
Ассемблерный код — это доступное для понимания человеком представление машинного кода.
Используя флаг -S, который сообщает компилятору остановиться после стадии компиляции, получим ассемблерный код в выходном файле driver.s:
$ g++ -S driver.ii -o driver.s
driver.s
.file "driver.cpp" .local _ZStL8__ioinit .comm _ZStL8__ioinit,1,1 .section .rodata .LC0: .string "Hello, world!" .text .globl main .type main, @function main: .LFB1021: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $.LC0, %esi movl $_ZSt4cout, %edi call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi movq %rax, %rdi call _ZNSolsEPFRSoS_E movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1021: .size main, .-main .type _Z41__static_initialization_and_destruction_0ii, @function _Z41__static_initialization_and_destruction_0ii: .LFB1030: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl %edi, -4(%rbp) movl %esi, -8(%rbp) cmpl $1, -4(%rbp) jne .L5 cmpl $65535, -8(%rbp) jne .L5 movl $_ZStL8__ioinit, %edi call _ZNSt8ios_base4InitC1Ev movl $__dso_handle, %edx movl $_ZStL8__ioinit, %esi movl $_ZNSt8ios_base4InitD1Ev, %edi call __cxa_atexit .L5: nop leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1030: .size _Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii .type _GLOBAL__sub_I_main, @function _GLOBAL__sub_I_main: .LFB1031: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $65535, %esi movl $1, %edi call _Z41__static_initialization_and_destruction_0ii popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1031: .size _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main .section .init_array,"aw" .align 8 .quad _GLOBAL__sub_I_main .hidden __dso_handle .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits
Мы можем все также посмотреть и прочесть полученный результат. Но для того, чтобы машина поняла наш код, требуется преобразовать его в машинный код, который мы и получим на следующем шаге.
3) Ассемблирование
Так как x86 процессоры исполняют команды на бинарном коде, необходимо перевести ассемблерный код в машинный с помощью ассемблера.
Ассемблер преобразовывает ассемблерный код в машинный код, сохраняя его в объектном файле.
Объектный файл — это созданный ассемблером промежуточный файл, хранящий кусок машинного кода. Этот кусок машинного кода, который еще не был связан вместе с другими кусками машинного кода в конечную выполняемую программу, называется объектным кодом.
Далее возможно сохранение данного объектного кода в статические библиотеки для того, чтобы не компилировать данный код снова.
Получим машинный код с помощью ассемблера (as) в выходной объектный файл driver.o:
$ as driver.s -o driver.o
Но на данном шаге еще ничего не закончено, ведь объектных файлов может быть много и нужно их всех соединить в единый исполняемый файл с помощью компоновщика (линкера). Поэтому мы переходим к следующей стадии.
4) Компоновка
Компоновщик (линкер) связывает все объектные файлы и статические библиотеки в единый исполняемый файл, который мы и сможем запустить в дальнейшем. Для того, чтобы понять как происходит связка, следует рассказать о таблице символов.
Таблица символов — это структура данных, создаваемая самим компилятором и хранящаяся в самих объектных файлах. Таблица символов хранит имена переменных, функций, классов, объектов и т.д., где каждому идентификатору (символу) соотносится его тип, область видимости. Также таблица символов хранит адреса ссылок на данные и процедуры в других объектных файлах.
Именно с помощью таблицы символов и хранящихся в них ссылок линкер будет способен в дальнейшем построить связи между данными среди множества других объектных файлов и создать единый исполняемый файл из них.
Получим исполняемый файл driver:
$ g++ driver.o -o driver // также тут можно добавить и другие объектные файлы и библиотеки
5) Загрузка
Последний этап, который предстоит пройти нашей программе — вызвать загрузчик для загрузки нашей программы в память. На данной стадии также возможна подгрузка динамических библиотек.
Запустим нашу программу:
$ ./driver // Hello, world!
Заключение
В данной статье были рассмотрены основы процесса компиляции, понимание которых будет довольно полезно каждому начинающему программисту. В скором времени будет опубликована вторая статья про статические и динамические библиотеки.
#8. Компиляция и выполнение java программы с командной строки
Продолжаем курс программирования java для android-разработчиков. Данный урок научит вас запускать java программы из командной строки, для общего понимания процесса компиляции и запуска программ на языке java.
После создания простого приложения, которое выводит что-то на экран, вы должны компилировать ваш код и запустить его.
Независимо от того, какую операционную систему вы используете, Linux, Mac или Windows, если на вашем компьютере установлен JDK (Java Development Kit), вы можете в консоли набрать следующие команды чтобы скомпилировать и запустить программу:
- javac (или javac.exe)
- java (или java.exe)
В первом случае будет вызван компилятор javac.exe, а во втором случае – запускалка java.exe, которая стартует нашу программу. Эти файлы лежат в папке bin вашего JDK.
Рассмотрим на примере. Вспомним код из первого урока – создадим файл с названием Main.java.
public class Main < public static void main(String[] args) < System.out.println("Hello, World!"); >>
Идем в папку, куда среда разработки сохранила проект. Я работаю в IntelliJIDEA, и мой проект лежит в C:\Users\имя пользователя\IdeaProjects\название проекта\src. Находим там наш файл Main.java. Консоль вызывается так: щелкаем правой клавишей мыши с зажатой клавишей Shift на пустом месте в папке, где лежит файл нашей программы, и выбираем пункт контекстного меню “Открыть окно команд”.
Для того, чтобы скомпилировать его нужно набрать в консоли команду javac и в качестве параметра передать имя нашего файла:
javac Main.java
Эта команда вызовет компилятор, который создаст файл Main.class, содержащий скомпилированный код нашей java программы.
Чтобы запустить ее, нужно ввести команду java с именем класса (не файла!) в качестве параметра:
java Main.class //так неправильно
java Main //так правильно
Аргументы
В главном классе нашей программы есть метод public static void main(. ) , который в качестве аргумента принимает массив String[] args.
Массив строк в качестве аргумента можно передать в программу при запуске из командной строки .
Любой массив в Java имеет переменную длину, это число элементов в этом массиве.
Добавим такой код в класс Main.java:
public class Main < public static void main(String[] args) < for (int i = 0; i < args.length; i++) < System.out.println(args[i]); >> >
И чтобы скомпилировать и запустить программу с аргументами, пишем в консоль:
javac Main.java java Main arg0 arg1 arg2
Упражнение
Создайте программу, которая выводит аргументы, переданные при запуске, в одну строку.
Дополнительное чтение:
- Сборка и выполнение Java программ
- Исключения (Exception) и их обработка в Java
