← Назад к вопросам

Что будет происходить когда программа Java запустится?

3.0 Senior🔥 161 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Жизненный цикл Java приложения: От запуска до завершения

Полный процесс выполнения программы

Когда ты пишешь java -jar myapp.jar, происходит целый ряд операций. Разберу этот процесс пошагово с уровня Java Machine до уровня твоего кода.

Этап 1: Загрузка и инициализация JVM

Шаг 1: Запуск JVM процесса

// Команда
java -Xmx1024m -Xms512m -jar myapp.jar

// Что происходит:
// 1. ОС создаёт новый процесс
// 2. JVM выделяет памяти (Xms - initial, Xmx - maximum)
// 3. JVM инициализирует внутренние структуры

Шаг 2: Поиск и загрузка jar файла

// JVM читает META-INF/MANIFEST.MF
// Manifest-Version: 1.0
// Main-Class: com.example.Application
// Class-Path: lib/*

// JVM находит класс Main-Class

Этап 2: Bootstrap ClassLoader

Шаг 1: Загрузка core классов JVM

// Bootstrap ClassLoader загружает:n
// - java.lang.Object (базовый класс всего)
// - java.lang.String
// - java.lang.System
// - Все классы из rt.jar (runtime library)

public class Main {
    public static void main(String[] args) {
        // Object уже загружен, иначе это не работало бы
    }
}

Шаг 2: Инициализация System класса

// System.class загружается и выполняет статический инициализатор
public final class System {
    static {
        // Инициализация System.out, System.err, System.in
        // Загрузка native методов
        // Установка security manager
    }
}

Этап 3: Application ClassLoader и загрузка твоего кода

Шаг 1: Загрузка Main класса

// JVM использует Application ClassLoader для загрузки
// com.example.Application из JAR файла

public class Application {
    // Статические переменные инициализируются ДО выполнения
    private static final Logger log = LoggerFactory.getLogger(Application.class);
    private static int counter = 0;
    
    // 1. Загрузится класс
    // 2. Выполнится статический блок
    // 3. Инициализируются статические переменные
    
    static {
        System.out.println("Статический блок выполнится ДО main");
        counter = 100;
    }
}

Шаг 2: Вызов main метода

public class Application {
    public static void main(String[] args) {
        System.out.println("main метод начал выполняться");
    }
}

// На этом этапе:
// 1. Все статические блоки уже выполнены
// 2. System.out готов к использованию
// 3. args содержит аргументы командной строки

Этап 4: Выполнение main метода

Пример сложного приложения:

public class Application {
    // 1. ПЕРВОЕ: статическая инициализация
    private static final Logger log = LoggerFactory.getLogger(Application.class);
    
    static {
        System.out.println("1. Статический блок");
    }
    
    public static void main(String[] args) {
        System.out.println("2. Main метод начал");
        
        // 3. Создание объекта класса
        Application app = new Application();
        
        // 4. Вызов метода
        app.run();
        
        System.out.println("5. Main закончил");
    }
    
    public Application() {
        System.out.println("3. Конструктор");
    }
    
    public void run() {
        System.out.println("4. Метод run");
    }
}

// Вывод при запуске:
// 1. Статический блок
// 2. Main метод начал
// 3. Конструктор
// 4. Метод run
// 5. Main закончил

Этап 5: Во время выполнения (Runtime)

Динамическая загрузка классов

public class RuntimeLoader {
    public static void main(String[] args) throws Exception {
        System.out.println("1. JVM запустилась");
        
        // Класс DatabaseConnection ещё НЕ загружен
        // Он загрузится только когда понадобится
        
        if (args.length > 0 && args[0].equals("--with-db")) {
            // 2. Только сейчас загружается DatabaseConnection
            DatabaseConnection db = new DatabaseConnection();
            db.connect();
        }
        
        System.out.println("3. Программа завершилась");
    }
}

// Если запустить БЕЗ --with-db:
// DatabaseConnection никогда не загрузится
// ClassNotFoundException не будет

Garbage Collection во время работы

public class GCExample {
    public static void main(String[] args) {
        // JVM отслеживает объекты, которые не используются
        
        String str1 = new String("Hello"); // Объект создан
        str1 = null; // Объект становится недостижимым
        // GC может удалить этот объект
        
        // В современной JVM:
        // - Young Generation GC часто (быстро)
        // - Old Generation GC редко (долго)
        // - G1GC балансирует паузы
    }
}

Этап 6: Spring Boot приложение (более сложный пример)

@SpringBootApplication
public class Application {
    private static final Logger log = LoggerFactory.getLogger(Application.class);
    
    public static void main(String[] args) {
        // 1. Spring Bootstrap
        log.info("1. Запуск Spring");
        
        SpringApplication app = new SpringApplication(Application.class);
        ConfigurableApplicationContext context = app.run(args);
        
        // 2. На этом этапе:
        // - Все @Bean инициализированы
        // - Все @Service, @Repository инициализированы
        // - HTTP сервер запущен (Tomcat на 8080)
        
        log.info("2. Приложение запущено, готово к запросам");
        
        // 3. Основной поток ждёт (не завершается)
        // Это позволяет background threads продолжать работу
    }
}

// При запуске Spring:
// 1. Создаёт ApplicationContext
// 2. Сканирует classpath на @Component аннотации
// 3. Создаёт beans в правильном порядке (зависимости first)
// 4. Вызывает @PostConstruct методы
// 5. Запускает Tomcat/Jetty
// 6. Выполняет @EventListener(ContextRefreshedEvent.class) методы

Этап 7: Обработка исключений

UncaughtExceptionHandler

public class SafeApplication {
    public static void main(String[] args) {
        // Перехватываем необработанные исключения в main потоке
        Thread.setDefaultUncaughtExceptionHandler((thread, exception) -> {
            System.err.println("Необработанное исключение в потоке: " + thread.getName());
            exception.printStackTrace();
            
            // Логируем, отправляем alert, etc
            // Приложение может продолжить работу или завершиться
        });
        
        // В других потоках нужно ловить исключения
        new Thread(() -> {
            try {
                throw new RuntimeException("Ошибка в background потоке");
            } catch (Exception e) {
                // ЗДЕСЬ нужно перехватить, иначе поток просто умрёт
            }
        }).start();
    }
}

Этап 8: Завершение программы

Нормальное завершение

public class GracefulShutdown {
    public static void main(String[] args) {
        System.out.println("Приложение работает");
        
        // Регистрируем shutdown hook
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Приложение завершается, очищаем ресурсы");
            // Закрываем БД соединения
            // Завершаем background потоки
            // Сохраняем состояние
        }));
        
        // Ждём
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println("main завершилась нормально");
        // JVM проверяет, есть ли non-daemon потоки
        // Если нет → JVM завершается
    }
}

// Порядок завершения:
// 1. main метод возвращает значение
// 2. Выполняются Shutdown Hooks (в обратном порядке регистрации)
// 3. Если все потоки daemon → JVM завершается
// 4. JVM закрывает файловые дескрипторы
// 5. Процесс ОС завершается с exit code

Аварийное завершение

public class EmergencyShutdown {
    public static void main(String[] args) {
        // Нормальное завершение
        System.exit(0); // exit code 0 = успешно
        
        // Аварийное завершение
        System.exit(1); // exit code 1 = ошибка
        
        // Мгновенное завершение (без Shutdown Hooks)
        Runtime.getRuntime().halt(1);
    }
}

Полная диаграмма для Spring Boot приложения

public class CompleteLifecycle {
    public static void main(String[] args) {
        // PHASE 1: JVM Bootstrap
        // - ОС запускает JVM процесс
        // - Bootstrap ClassLoader загружает core классы
        // - Инициализируется System класс
        
        // PHASE 2: Application Bootstrap
        // - Application ClassLoader загружает твой код
        // - Выполняются статические блоки в Main классе
        
        // PHASE 3: Spring Initialization
        log.info("PHASE 3: Spring начинает инициализацию");
        SpringApplication.run(Application.class, args);
        
        // PHASE 4: Runtime
        // - Приложение обрабатывает HTTP запросы
        // - Background потоки работают
        // - GC периодически чистит память
        // - JVM отслеживает потоки
        
        // PHASE 5: Graceful Shutdown (при Ctrl+C)
        // - Shutdown hooks выполняются
        // - Потоки завершаются
        // - Ресурсы закрываются
        // - JVM выходит
    }
}

Практические следствия этого знания

1. Статические блоки выполняются при загрузке класса

// Экспенсивная операция в static блоке может замедлить запуск
static {
    initializeDatabaseConnections(); // Будет выполнено сразу при загрузке
}

// Решение: ленивая инициализация
private static DatabaseConnections connections;
public static DatabaseConnections getConnections() {
    if (connections == null) {
        connections = new DatabaseConnections(); // Инициализация по требованию
    }
    return connections;
}

2. Shutdown hooks важны для production

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    log.info("Gracefully shutting down");
    application.stop();
    database.close();
    kafkaProducer.close();
}));

3. Exception в main не означает crash приложения (в Spring)

// Spring обработает это и будет работать дальше
Thread thread = new Thread(() -> {
    throw new RuntimeException("Boom!"); // Поток умрёт, приложение продолжит
});
thread.start();

Итоговый вывод

Основные этапы:

  1. JVM Bootstrap — инициализация виртуальной машины
  2. ClassLoading — загрузка классов (ленивая)
  3. Статическая инициализация — выполнение static блоков
  4. main метод — начало выполнения твоего кода
  5. Runtime — обработка запросов, GC, monitoring
  6. Graceful shutdown — cleanup и завершение

Это знание критично для отладки проблем с инициализацией, performance и lifecycle управления.

Что будет происходить когда программа Java запустится? | PrepBro