Всем привет! Перевод статьи подготовлен специально для студентов курса «Разработчик Java».
Продолжаем разговор о том, как Java Virtual Machine работает внутри. В предыдущей статье (оригинал на анг.) мы рассмотрели подсистему загрузки классов. В этой статье мы поговорим о структуре class-файлов.
Как мы уже знаем, весь исходный код, написанный на языке программирования Java, сначала компилируется в байт-код с помощью компилятора
Рисунок — компиляция исходного кода Java
Каждый файл с расширением
Примечание — для простоты файлы с расширением
Давайте напишем простую программу.
Запуск
Как видите, для каждого класса и интерфейса создается отдельный class-файл.
Class-файл имеет бинарный формат. Информация в нем обычно записывается без отступов между последовательными частями информации, все выравнивается по границам байтов. Все 16-битные и 32-битные значения записываются с помощью двух или четырех последовательных 8-битных байтов.
Class-файл содержит следующую информацию.
Магическое число, сигнатура. Первые четыре байта каждого class-файла всегда
Версия файла. Следующие четыре байта содержат мажорную и минорную версию файла. Вместе эти номера определяют версию формата class-файла. Если class-файл имеет основной мажорную версию M и минорную m, то мы обозначаем эту версию как M.m.
У каждой JVM есть ограничения по поддерживаемым версиям class-файлов. Например, Java 11 поддерживает major версию с 45 до 55, Java 12 — с 45 по 56.
Пул констант. Таблица структур, представляющих строковые константы, имена классов, интерфейсов, полей, методов и другие константы, которые есть в структуре ClassFile и ее подструктурах. Каждый элемент пула констант начинается с однобайтового тега, определяющего тип константы. В зависимости от типа константы следующие байты могут быть непосредственным значением константы или ссылкой на другой элемент в пуле.
Флаги доступа. Список флагов, которые указывают класс это или интерфейс, public или private, финальный класс или нет. Различные флаги, такие как
This class. Ссылка на запись в пуле констант.
Super class. Ссылка на запись в пуле констант.
Интерфейсы. Количество интерфейсов, реализованных классом.
Количество полей. Количество полей в классе или интерфейсе.
Поля. После количества полей следует таблица структур переменной длины. По одной для каждого поля с описанием типа поля и названия (со ссылкой на пул констант).
Количество методов. Количество методов в классе или интерфейсе. Это число включает только методы, которые явно определены в классе, без методов, унаследованных от суперклассов.
Методы. Далее находятся сами методы. Для каждого метода содержится следующая информация: дескриптор метода (тип возвращаемого значения и список аргументов), количество слов, необходимых для локальных переменных метода, максимальное количество слов стека, необходимых для стека операндов метода, таблицу исключений, перехватываемых методом, байт-коды метода и таблица номеров строк.
Количество атрибутов. Количество атрибутов в этом классе, интерфейсе или модуле.
Атрибуты. После количества атрибутов следуют таблицы или структуры переменной длины, описывающие каждый атрибут. Например, всегда есть атрибут “SourceFile”. Он содержит имя исходного файла, из которого был скомпилирован class-файл.
Хотя class-файл напрямую не человекочитаемый, в JDK есть инструмент под названием javap, который выводит его содержимое в удобном формате.
Давайте напишем простую программу на Java, указанную ниже.
Давайте скомпилируем эту программу с помощью
Здесь вы можете увидеть, что класс публичный (
Возможно, вы заметили, что в исходном коде есть только один статический метод main, но class-файл говорит, что есть два метода. Вспомните конструктор по умолчанию — это конструктор без аргументов, добавленный компилятором
Больше почитать про javap вы можете здесь.
Продолжаем разговор о том, как Java Virtual Machine работает внутри. В предыдущей статье (оригинал на анг.) мы рассмотрели подсистему загрузки классов. В этой статье мы поговорим о структуре class-файлов.
Как мы уже знаем, весь исходный код, написанный на языке программирования Java, сначала компилируется в байт-код с помощью компилятора
javac
, входящего в состав Java Development Kit. Байт-код сохраняется в бинарный файл в специальный class-файл. Затем эти class-файлы динамически (при необходимости) загружаются в память загрузчиком классов (ClassLoader). Рисунок — компиляция исходного кода Java
Каждый файл с расширением
.java
компилируется как минимум в один файл .class
. Для каждого класса, интерфейса и модуля, определенных в исходном коде, создается по одному .class
файлу. Это также относится к интерфейсам и вложенным классам.Примечание — для простоты файлы с расширением
.class
будем называть “class-файлами”.Давайте напишем простую программу.
public class ClassOne{
public static void main(String[] args){
System.out.println("Hello world");
}
static class StaticNestedClass{
}
}
class ClassTwo{
}
interface InterfaceOne{
}
Запуск
javac
для этого файла приведет к появлению следующих файлов.ClassOne$StaticNestedClass.class
ClassOne.class
ClassTwo.class
InterfaceOne.class
Как видите, для каждого класса и интерфейса создается отдельный class-файл.
Что внутри class-файла?
Class-файл имеет бинарный формат. Информация в нем обычно записывается без отступов между последовательными частями информации, все выравнивается по границам байтов. Все 16-битные и 32-битные значения записываются с помощью двух или четырех последовательных 8-битных байтов.
Class-файл содержит следующую информацию.
Магическое число, сигнатура. Первые четыре байта каждого class-файла всегда
0xCAFEBABE
. Эти четыре байта идентифицируют class-файл Java.Версия файла. Следующие четыре байта содержат мажорную и минорную версию файла. Вместе эти номера определяют версию формата class-файла. Если class-файл имеет основной мажорную версию M и минорную m, то мы обозначаем эту версию как M.m.
У каждой JVM есть ограничения по поддерживаемым версиям class-файлов. Например, Java 11 поддерживает major версию с 45 до 55, Java 12 — с 45 по 56.
Пул констант. Таблица структур, представляющих строковые константы, имена классов, интерфейсов, полей, методов и другие константы, которые есть в структуре ClassFile и ее подструктурах. Каждый элемент пула констант начинается с однобайтового тега, определяющего тип константы. В зависимости от типа константы следующие байты могут быть непосредственным значением константы или ссылкой на другой элемент в пуле.
Флаги доступа. Список флагов, которые указывают класс это или интерфейс, public или private, финальный класс или нет. Различные флаги, такие как
ACC_PUBLIC
, ACC_FINAL
, ACC_INTERFACE
, ACC_ENUM
и т. д. описаны спецификации Java Virtual Machine Specification.This class. Ссылка на запись в пуле констант.
Super class. Ссылка на запись в пуле констант.
Интерфейсы. Количество интерфейсов, реализованных классом.
Количество полей. Количество полей в классе или интерфейсе.
Поля. После количества полей следует таблица структур переменной длины. По одной для каждого поля с описанием типа поля и названия (со ссылкой на пул констант).
Количество методов. Количество методов в классе или интерфейсе. Это число включает только методы, которые явно определены в классе, без методов, унаследованных от суперклассов.
Методы. Далее находятся сами методы. Для каждого метода содержится следующая информация: дескриптор метода (тип возвращаемого значения и список аргументов), количество слов, необходимых для локальных переменных метода, максимальное количество слов стека, необходимых для стека операндов метода, таблицу исключений, перехватываемых методом, байт-коды метода и таблица номеров строк.
Количество атрибутов. Количество атрибутов в этом классе, интерфейсе или модуле.
Атрибуты. После количества атрибутов следуют таблицы или структуры переменной длины, описывающие каждый атрибут. Например, всегда есть атрибут “SourceFile”. Он содержит имя исходного файла, из которого был скомпилирован class-файл.
Хотя class-файл напрямую не человекочитаемый, в JDK есть инструмент под названием javap, который выводит его содержимое в удобном формате.
Давайте напишем простую программу на Java, указанную ниже.
package bytecode;
import java.io.Serializable;
public class HelloWorld implements Serializable, Cloneable {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
Давайте скомпилируем эту программу с помощью
javac
, которая создаст файл HelloWorld.class
, и используем javap
для просмотра файла HelloWorld.class
. Запустив javap
с параметром -v (verbose)
для HelloWorld.class
получим следующий результат:Classfile /Users/apersiankite/Documents/code_practice/java_practice/target/classes/bytecode/HelloWorld.class
Last modified 02-Jul-2019; size 606 bytes
MD5 checksum 6442d93b955c2e249619a1bade6d5b98
Compiled from "HelloWorld.java"
public class bytecode.HelloWorld implements java.io.Serializable,java.lang.Cloneable
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // bytecode/HelloWorld
super_class: #6 // java/lang/Object
interfaces: 2, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #25 // Hello World
#4 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #28 // bytecode/HelloWorld
#6 = Class #29 // java/lang/Object
#7 = Class #30 // java/io/Serializable
#8 = Class #31 // java/lang/Cloneable
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lbytecode/HelloWorld;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 SourceFile
#21 = Utf8 HelloWorld.java
#22 = NameAndType #9:#10 // "<init>":()V
#23 = Class #32 // java/lang/System
#24 = NameAndType #33:#34 // out:Ljava/io/PrintStream;
#25 = Utf8 Hello World
#26 = Class #35 // java/io/PrintStream
#27 = NameAndType #36:#37 // println:(Ljava/lang/String;)V
#28 = Utf8 bytecode/HelloWorld
#29 = Utf8 java/lang/Object
#30 = Utf8 java/io/Serializable
#31 = Utf8 java/lang/Cloneable
#32 = Utf8 java/lang/System
#33 = Utf8 out
#34 = Utf8 Ljava/io/PrintStream;
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (Ljava/lang/String;)V
{
public bytecode.HelloWorld();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lbytecode/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 7: 0
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
Здесь вы можете увидеть, что класс публичный (
public
) и у него в пуле констант 37 записей. Есть один атрибут (SourceFile внизу), класс реализует два интерфейса (Serializable, Cloneable), у него нет полей и есть два метода.Возможно, вы заметили, что в исходном коде есть только один статический метод main, но class-файл говорит, что есть два метода. Вспомните конструктор по умолчанию — это конструктор без аргументов, добавленный компилятором
javac
, байт-код которого также виден в выводе. Конструкторы рассматриваются как методы.Больше почитать про javap вы можете здесь.
Совет: вы также можете использовать javap для того, чтобы увидеть, чем лямбды отличаются от анонимных внутренних классов.