В прошлой публикации мы установили и настроили Sublime Text в качестве редактора для написания кода на Java.
Из этой статьи вы узнаете:
- о компиляции и запуске Java-программ из консоли
- как выводить в нее корректно русский текст
- способ отображения байт-кода
Введение
На первых порах начинающему программисту очень важно научиться выполнять необходимые действия минимальными средствами, чтобы:
- иметь представление, что происходит «под капотом»
- не быть зависимым от среды разработки
- не превратиться в специалиста по нажиманию кнопок в инструментах для профессионалов
- не оказаться беспомощным за их пределами
Вам сейчас нужно максимально набить шишек, находясь в спартанских условиях, которые закалят и научат самостоятельно и оперативно решать возникающие проблемы. Это придаст уверенности и повысит вашу экспертность.
Есть такое понятие — профессиональный кругозор. Он важен для людей любой профессии. Программисты не исключение. Если человек хочет стать настоящим разработчиком, обладающим фундаментальным знанием, а не поверхностным и неструктурированным, то ему нужно изучать возможности языка последовательно и системно. Компиляция и запуск — это основа основ, это база, мимо которой никак нельзя пройти.
#8. Компиляция и запуск java программы с командной строки
Компилировать и запускать программы, на первых порах, мы будем из консоли. В этой и последующих статьях мы будем много работать с этим инструментом. Это важный навык, который используется программистами повсеместно.
1. Компиляция
Откроем в Sublime Text терминал, нажав Ctrl + Alt + T (при этом файл MyFirstApp.java должен быть открыт в редакторе). Обязательно проверьте, что терминал запустился в папке StartJava (или где вы сохранили класс).
Для компиляции воспользуемся уже знакомой утилитой javac (java compiler, компилятор java). Она нужна для преобразования Java-кода в язык виртуальной машины (байт-код).
В терминале пишем команду javac MyFirstApp.java
Компиляция
Отсутствие каких-либо сообщений свидетельствует о том, что компилятор не нашел ошибок в нашем коде. Результатом его раоты стал файл MyFirstApp.class.
На текущем этапе директория содержит два файла.
2. Запуск
Как мы уже знаем, MyFirstApp.class содержит байт-код. Для его исполнения воспользуемся утилитой java. Именно она стартует JVM, которая в свою очередь запустит класс MyFirstApp.
Пишем java MyFirstApp :
Программа отработала верно, выведя в консоль WORA.
Обратите внимание, что необходимо указать не имя файла MyFirstApp.class, а имя класса. При этом писать какое-либо расширение не нужно. Утилита java принимает в качестве параметра именно имя класса, а не имя файла, где он находится.
3. Вывод байт-кода
Инструменты для запуска и разработки Java приложений, компиляция, выполнение на JVM
Ни для кого не секрет, что на данный момент Java — один из самых популярных языков программирования в мире. Дата официального выпуска Java — 23 мая 1995 года.
Как запускать Java программу из командной строки
Эта статья посвящена основам основ: в ней изложены базовые особенности языка, которые придутся кстати начинающим “джавистам”, а опытные 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 этапа:
- Загрузка байт-кода и создание экземпляра класса Class
Грубо говоря, чтобы попасть на 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’а, который мы указали при запуске приложения.
Процесс загрузки классов происходит по иерархии:
- В первую очередь мы запрашиваем поиск в кэше System Class Loader (кэш системного загрузчика содержит классы, которые уже были им загружены);
- Если класс не был найден в кэше системного загрузчика, мы смотрим кэш Extension class loader;
- Если класс не найден в кэше загрузчика расширений, класс запрашивается у загрузчика Bootstrap.
4. Структура Сlass-файлов и процесс загрузки
Перейдем непосредственно к структуре Class-файлов.
Один класс, написанный на Java, компилируется в один файл с расширением .class. Если в нашем Java файле лежит несколько классов, один файл Java может быть скомпилирован в несколько файлов с расширением .class — файлов байт-кода данных классов.
Все числа, строки, указатели на классы, поля и методы хранятся в Сonstant pool — области памяти Meta space. Описание класса хранится там же и содержит имя, модификаторы, супер-класс, супер-интерфейсы, поля, методы и атрибуты. Атрибуты, в свою очередь, могут содержать любую дополнительную информацию.
Таким образом, при загрузке классов:
- происходит чтение класс-файла, т.е проверка корректности формата
- создается представление класса в Constant pool (Meta space)
- грузятся супер-классы и супер-интерфейсы; если они не будут загружены, то и сам класс не будет загружен
5. Исполнение байт-кода на JVM
В первую очередь, для исполнения байт-кода, JVM может его интерпретировать. Интерпретация — довольно медленный процесс. В процессе интерпретации, интерпретатор “бежит” построчно по класс-файлу и переводит его в команды, которые понятны JVM.
Также JVM может его транслировать, т.е. скомпилировать в машинный код, который будет исполняться непосредственно на CPU.
Команды, которые исполняются часто, не будут интерпретироваться, а сразу будут транслироваться.
6. Компиляция
Компилятор — это программа, которая преобразует исходные части программ, написанные на языке программирования высокого уровня, в программу на машинном языке, “понятную” компьютеру.
Компиляторы делятся на:
- Не оптимизирующие
- Простые оптимизирующие (Hotspot Client): работают быстро, но порождают неоптимальный код
- Сложные оптимизирующие (Hotspot Server): производят сложные оптимизирующие преобразования прежде чем сформировать байт-код
Также компиляторы могут классифицироваться по моменту компиляции:
- Динамические компиляторы
Работают одновременно с программой, что сказывается на производительности. Важно, чтобы эти компиляторы работали на коде, который часто исполняется. Во время исполнения программы JVM знает, какой код выполняется чаще всего, и, чтобы постоянно не интерпретировать его, виртуальная машина сразу переводит его в команды, которые уже будут исполняться непосредственно на процессорe. - Статические компиляторы
Дольше компилируют, но порождают оптимальный код для исполнения. Из плюсов: не требуют ресурсов во время исполнения программы, каждый метод компилируется с применением оптимизаций.
7. Организация памяти в Java
Стек — это область памяти в Java, которая работает по схеме LIFO — “Last in — Fisrt Out” или “Последним вошел, первым вышел”.
Он нужен для того, чтобы хранить методы. Переменные в стеке существуют до тех пор, пока выполняется метод в котором они были созданы.
Когда вызывается любой метод в Java, создается фрейм или область памяти в стеке, и метод кладется на его вершину. Когда метод завершает выполнение, он удаляется из памяти, тем самым освобождая память для следующих методов. Если память стека будет заполнена, Java бросит исключение java.lang.StackOverFlowError. К примеру, это может произойти, если у нас будет рекурсивная функция, которая будет вызывать сама себя и памяти в стеке не будет хватать.
Ключевые особенности стека:
- Стек заполняется и освобождается по мере вызова и завершения новых методов
- Доступ к этой области памяти осуществляется быстрее, чем к куче
- Размер стека определяется операционной системой
- Является потокобезопасным, поскольку для каждого потока создается свой отдельный стек
Куча разбита на несколько более мелких частей, называемых поколениями:
- Young generation — область, где размещаются недавно созданные объекты
- Old (tenured) generation — область, где хранятся “долгоживущие” объекты
- До Java 8 существовала ещё одна область — Permanent generation — которая содержит метаинформацию о классах, методах, статических переменных. После появления Java 8 было решено хранить эту информацию отдельно, вне кучи, а именно в Meta space
Почему отказались от Permanent generation? В первую очередь, это из-за ошибки, которая была связана с переполнением области: так как Perm имел константный размер и не мог расширяться динамически, рано или поздно память заканчивалась, кидалась ошибка, и приложение падало.
Meta space же имеет динамический размер, и во время исполнения он может расширяться до размеров памяти JVM.
Ключевые особенности кучи:
- Когда эта область памяти заполняется полностью, Java бросает java.lang.OutOfMemoryError
- Доступ к куче медленнее, чем к стеку
- Для сбора неиспользуемых объектов работает сборщик мусора
- Куча, в отличие от стека, не является потокобезопасной, так как любой поток может получить к ней доступ
Основываясь на информации выше, рассмотрим, как происходит управление памятью на простом примере:
public class App < public static void main(String[] args) < int String pName = «Jon»; Person p = null; p = new Person(id, pName); >> class Person < int pid; String name; // constructors, getters/setters >
У нас есть класс 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 могут иметь различные алгоритмы сборки мусора, также существуют разные сборщики мусора.
Компиляция и исполнение Java приложений под капотом
Всем привет! Сегодня я хотел бы поделиться знаниями о том, что происходит под капотом JVM (Java Virtual Machine) после того, как мы запускаем написанное Java приложение. В наше время существуют моднейшие среды разработки, которые помогают не думать о внутреннем устройстве JVM, компиляции и выполнении Java-кода, из за чего новые разработчики могут упустить эти важные аспекты. В то же время, на собеседованиях часто задают вопросы касательно этой темы, из-за чего я и решил написать статью.
2. Компиляция в байт-код
Начнем с теории. Когда мы пишем какое-либо приложение, мы создаем файл с расширением .java и помещаем в него код на языке программирования Java. Такой файл, содержащий код, понятный человеку, называется файлом с исходным кодом. После того, как файл с исходным кодом готов, нужно его выполнить! Но на стадии в нем содержится информация, понятная только человеку.
Java — мультиплатформенный язык программирования. Это значит, что программы, написанные на языке Java, можно выполнять на любой платформе, где установлена специальная исполняющая система Java. Такая система называется Java Virtual Machine (JVM). Для того, чтобы перевести программу из исходного кода в код, понятный JVM, нужно её скомпилировать.
Код, понятный JVM называется байт-кодом и содержит набор инструкций, которые в дальнейшем будет исполнять виртуальная машина. Для компиляции исходного кода в байт-код существует компилятор javac , входящий в поставку JDK (Java Development Kit). На вход компилятор принимает файл с расширением .java , содежащий исходный код программы, а на выходе выдает файл с расширением .class , содержащий байт-код, необходимый для исполнения программы виртуальной машиной. После того, как программа была скомпилирована в байт-код, она может быть выполнена с помощью виртуальной машины.
3. Пример компиляции и выполнения программы
Предположим, что у нас есть простая программа, содержащаяся в файле Calculator.java , которая принимает 2 численных аргумента командной строки и печатает результат их сложения:
class Calculator < public static void main(String[] args)< int a = Integer.valueOf(args[0]); int b = Integer.valueOf(args[1]); System.out.println(a + b); >>
Для того, чтобы скомпилировать эту программу в байт-код, воспользуемся компилятором javac в командной строке:
javac Calculator.java
После компиляции на выходе мы получаем файл с байт-кодом Calculator.class , который мы можем исполнить при помощи установленной на нашем компьютере java-машины командой java в командной строке:
java Calculator 1 2
Заметим, что после названия файла были указаны 2 аргумента командной строки — числа 1 и 2. После выполнения программы, в командной строке выведется число 3. В примере выше у нас был простой класс, который живет сам по себе. Но что, если класс находится в каком либо пакете? Смоделируем такую ситуацию: создадим директории src/ru/javarush и поместим туда наш класс. Теперь он выглядит следующим образом (добавили имя пакета в начале файла):
package ru.javarush; class Calculator < public static void main(String[] args)< int a = Integer.valueOf(args[0]); int b = Integer.valueOf(args[1]); System.out.println(a + b); >>
Скомпилируем такой класс следующей командой:
javac -d bin src/ru/javarush/Calculator.java
В этом примере мы использовали дополнительную опцию компилятора -d bin , которая складывает скомпилированные файлы в директорию bin со структурой, аналогичной директории src , при этом директория bin должна быть создана заранее. Такой прием используется, чтобы не путать файлы с исходным кодом с файлами с байт-кодом. Перед запуском скомпилированной программы стоит пояснить понятие classpath . Classpath — это путь, относительно которого виртуальная машина будет искать пакеты и скомпилированные классы. Тоесть, таким образом мы говорим виртуальной машине какие директории в файловой системе являются корневыми для иерархии пакета Java. Classpath можно укзать при запуска программы с помощью флага -classpath . Запуск программы осуществляем с помощью команды:
java -classpath ./bin ru.javarush.Calculator 1 2
В этом примере нам потребовалось указать полное имя класса, включая имя пакета, в котором он находится. Финальное дерево файлов выглядит следующим образом:
├── src │ └── ru │ └── javarush │ └── Calculator.java └── bin └── ru └── javarush └── Calculator.class
4. Выполнение программы виртуальной машиной
Итак, мы запустили написанную программу. Но что же происходит в момент запуска скомпилированной программы виртуальной машиной? Для начала разберемся, что означают понятия компиляции и интерпретации кода.
Компиляция — трансляция программы, составленной на исходном языке высокого уровня, в эквивалентную программу на низкоуровневом языке, близком машинному коду. Интерпретация — пооператорный (покомандный, построчный) анализ, обработка и тут же выполнение исходной программы или запроса (в отличие от компиляции, при которой программа транслируется без её выполнения).
Язык Java обладает как компилятором ( javac ), так и интерпретатором, в роли которого выступает виртуальная машина, которая построчно преобразует байт-код в машинный код и тут же его исполняет. Таким образом, когда мы запускаем скомпилированную программу, виртуальная машина начинает её интерпретацию, то есть построчное преобразование байт-кода в машинный код, а так же его исполнение. К сожалению, чистая интерпретация байт-кода является довольно долгим процессом и делает язык java медленным в сравнении с его конкурентами. Дабы избежать этого, был введен механизм, позволяющий ускорить интерпретацию байт-кода виртуальной машиной. Этот механизм называется Just-in-time компиляцией (JITC).
5. Just-in-time (JIT) компиляция
Простыми словами, механизм Just-In-Time компиляции заключается в следующем: если в программе присутствуют части кода, которые выполняются много раз, то их можно скомпилировать один раз в машинный код, чтобы в будущем ускорить их выполнение. После компиляции такой части программы в машинный код, при каждом следующем вызове этой части программы виртуальная машина будет сразу выполнять скомпилированный машинный код, а не интерпретировать его, что естественно ускорит выполнение программы.
Ускорение работы программы достигается за счет увеличения потребления памяти (где-то же нам нужно хранить скомпилированный машинный код!) и за счет увеличения временных затрат на компиляцию во время исполнения программы. JIT компиляция — довольно сложный механизм, поэтому пройдемся по верхам. Всего существует 4 уровня JIT компиляции байт-кода в машинный код.
Чем выше уровень компиляции, тем он сложнее, но и одновременно выполнение такого участка будет быстрее, чем участка с меньшим уровнем. JIT — компилятор самостоятельно решает, какой уровень компиляции задать для каждого фрагмента программы на основе того, как часто выполняется этот фрагмент. Под капотом JVM использует 2 JIT-компилятора — C1 и C2. C1 компилятор так же называется клиентским компилятором и способен скомпилировать код только до 3-его уровня. За 4-ый, самый сложны и быстрый уровень компиляции отвечает компилятор C2.
Из вышесказанного можно сделать вывод о том, что для простых, клиентских приложений, выгоднее использовать компилятор C1, так как в этом случае нам важно как быстро стартует приложение. Серверные, долгоживущие приложения могут стартовать большее количество времени, однако в дальнейшем должны работать и выполнять свою функцию быстро — тут нам подойдет компилятор C2.
При запуске Java — программы на x32 версии JVM мы в ручную можем указать, какой режим мы хотим использовать, при помощи флагов -client и -server . При указании флага -client JVM не будет производить сложные оптимизации с байт-кодом, что ускорит время старта приложения и уменьшит количество потребляемой памяти. При указании флага -server приложение будет стартовать большее количество времени из-за сложных оптимизаций байт-кода и будет использовать больше памяти для хранения машинного кода, однако в дальнейшем работать такая программа будет быстрее. В x64 версии JVM флаг -client игнорируется и по умолчанию используется серверная конфигурация приложения.
6. Заключение
Компилятор javac преобразует исходный код программы в байт-код, который может быть выполнен на любой платформе, на которой установлена виртуальная машина Java;
После компиляции JVM интерпретирует получившийся байт-код;
Для ускорения работы Java-приложений, JVM использует механизм Just-In-Time компиляции, который преобразует наиболее часто выполняемые участки программы в машинный код и хранит их в памяти.
Источник: javarush.com