Блог компании OTUS. Онлайн-образование
Программирование
Java
3 декабря 2019

Внутренности JVM, Часть 2 — Структура class-файлов

Автор оригинала: Prateek Saini
Перевод
Всем привет! Перевод статьи подготовлен специально для студентов курса «Разработчик Java».




Продолжаем разговор о том, как 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 для того, чтобы увидеть, чем лямбды отличаются от анонимных внутренних классов.

+20
3,8k 104
Комментарии 4