При изучении инициализации класса System в JDK 14 и более поздних версиях становится очевидным, что стандартные потоки ввода/вывода настраиваются путем вызова метода registerNatives(), который вызывается из статического блока инициализации и сам по себе является нативным. Однако бросается в глаза то, что переменные out, in и err определены как static и final. Будучи final, эти поля требуют явной инициализации постоянным значением. Изначально внутри класса для них установлено значение null.
Учитывая, что их инициализация может обрабатываться собственным методом, явное присвоение null не исчезает просто так из кода, и логично, что эта часть кода также должна выполняться. Более того, если мы предположим, что весь этот процесс происходит за кулисами в методе <clinit>, сначала завершится собственный метод, а затем инициализатор поля, который назначает null (например, глядя на поле out). Как эффективно переопределяется null и где происходит эта фактическая инициализация?
Более того, если покопаться в комментариях, то станет очевидно, что инициализация этого класса каким-то образом отделена от обычного выполнения <clinit>:
/* Register the natives via the static initializer.
*
* The VM will invoke the initPhase1 method to complete the initialization
* of this class separate from <clinit>.
*/
private static native void registerNatives();
static {
registerNatives();
}
Что такое initPhase1? Что происходит на этом этапе и что именно это влечет за собой? Мне невероятно любопытно! Спасибо всем заранее!
@ElliottFrisch Спасибо, коллега! Я понимаю, что с практической точки зрения эти знания могут мне не очень пригодиться, но меня всегда увлекало понимание того, что происходит «под капотом». Вот почему я задал этот вопрос. Желаю вам также удачи и всего наилучшего!




При изучении инициализации класса System в JDK 14 и более поздних версиях становится очевидным, что стандартные потоки ввода/вывода настраиваются путем вызова метода registerNatives(), [...]
Нет, они не инициализируются с registerNatives(). Они инициализируются в методе initPhase1(), который представляет собой обычный метод Java (в классе System), который вызывается из машинного кода после инициализации класса System (т. е. после того, как этим полям присвоено значение null).
Вызов initPhase1() выполняется в методе C++ с именем Initialize_java_lang_classes.
Метод initialize_java_lang_classesинициализирует класс System (среди других классов). Эта инициализация выполняет все статические блоки кода и все статические инициализаторы.
Позже он выполняет initPhase1(), что, следовательно, происходит после инициализации класса.
Присваивание конечным полям запрещено для кода Java, но собственный код все равно может изменять эти поля (см. источник setIn0() о том, как это делается). Соответствующие доступные пользователю методы (например, setIn(), setOut() и setErr()) существуют начиная с Java 1.1. Могу только предположить, что в Java 1.0 нельзя было менять System.in и System.out.
Что делает registerNatives()?
Если вы посмотрите исходный код RegisterNatives() , он регистрирует некоторые native методы класса System с помощью JVM. Из комментария чуть выше этого метода он делает это из соображений производительности.
Я уже связывал метод registerNatives() с блоком и включил его сюда для полноты картины. Очевидно, что это не похоже на вызов initPhase1():
/* Only register the performance-critical methods */ static JNINativeMethod methods[] = { {"currentTimeMillis", "()J", (void *)&JVM_CurrentTimeMillis}, {"nanoTime", "()J", (void *)&JVM_NanoTime}, {"arraycopy", "(" OBJ "I" OBJ "II)V", (void *)&JVM_ArrayCopy}, }; #undef OBJ JNIEXPORT void JNICALL Java_java_lang_System_registerNatives(JNIEnv *env, jclass cls) { (*env)->RegisterNatives(env, cls, methods, sizeof(methods)/sizeof(methods[0])); }
Блок кода из источника System.java:
/* Register the natives via the static initializer. * * The VM will invoke the initPhase1 method to complete the initialization * of this class separate from <clinit>. */ private static native void registerNatives(); static { registerNatives(); }
кажется, вас обманывает: вы думаете, что initPhase1 каким-то образом вызывается из registerNatives(). Но, как показывает исходный код registerNatives(), это не так.
А также: registerNatives() вызывается из статического блока инициализатора, что означает, что он вызывается из <clinit>. Если бы registerNatives() действительно вызывал initPhase1(), это означало бы, что он будет вызван косвенно из <clinit>, что противоречит комментарию: виртуальная машина вызовет метод initPhase1 [...] отдельно от <clinit>
Что касается кода нативного метода setIn0() (также уже связанного выше), он также находится в System.c:
/* * The following three functions implement setter methods for * java.lang.System.{in, out, err}. They are natively implemented * because they violate the semantics of the language (i.e. set final * variable). */ JNIEXPORT void JNICALL Java_java_lang_System_setIn0(JNIEnv *env, jclass cla, jobject stream) { jfieldID fid = (*env)->GetStaticFieldID(env,cla,"in","Ljava/io/InputStream;"); if (fid == 0) return; (*env)->SetStaticObjectField(env,cla,fid,stream); }
(Я не стал копировать код для setOut0 и setErr0, поскольку их код тот же, за исключением названий полей.)
Обратите внимание на комментарий над собственными функциями: в нем четко указано их назначение и то, что эти функции делают то, что язык Java не позволяет.
Большое спасибо за ваш ответ! Если позволите, хотелось бы уточнить некоторые детали. Если сначала происходит инициализация нулевым значением, как ее можно переопределить, учитывая тот факт, что это запрещено для конечных переменных? Даже если initPhase1 вызывается из собственного метода, разве он не должен выполняться до присвоения значения null? Если мы рассмотрим последовательность инструкций, то именно так и должно произойти. И, наконец, что в этом случае инициализирует метод RegisterNatives()?
Если мы рассмотрим последовательность инструкций, то именно так и должно произойти. Почему вы так думаете? Обычно классы загружаются и инициализируются, и только после этого вы можете вызывать их методы.
Да, вы правы. Постараюсь объяснить свою мысль более четко и лаконично. В моем понимании инициализацией класса занимается метод <clinit>, который можно наблюдать на уровне байт-кода Java. Сюда входят все статические блоки инициализации и инициализаторы полей. В нашем случае внутри такого блока есть метод RegisterNatives(), за которым следуют выражения, которые явно инициализируют поля (как и в случае прямого присвоения значения null).
Похоже, что сначала выполняется метод RegisterNatives(), внутри которого вызывается initPhase1(), как вы правильно заметили. Таким образом, в моем понимании, сначала должны выполниться вышеупомянутые методы, а затем, судя по имеющемуся листингу, должна произойти инициализация полей с использованием явно заданного нулевого значения. Другими словами, мои рассуждения основаны на существующем исходном коде.
Я просто не до конца понимаю одну вещь. Почему именно initPhase1 позиционируется после инициализации класса, как вы указали в своем подробном ответе, если он, как я понял, вызывается непосредственно из метода RegisterNatives()? Или нативные методы работают особым образом и выполняются отдельно после завершения всех методов Java в блоке инициализации класса? Если это так, то я, честно говоря, не знал об этом. Этот конкретный момент до сих пор вызывает у меня некоторую путаницу.
Я не понимаю, почему вы думаете, что initPhase1() вызывается непосредственно из метода RegisterNatives(), хотя исходный код registerNatives() ясно показывает, что он не вызывает initPhase1.
Спасибо за подробный разбор! Я все рассмотрел и теперь полностью понимаю последовательность вызова. Мне следовало быть более наблюдательным. Оказывается, <clinit> на самом деле вызывается как отдельный метод от встроенной функции Threads::initialize_java_lang_classes(JavaThread* main_thread, TRAPS), за которым следует отдельный вызов initPhase1() из того же метода! Это разъяснение действительно помогает понять процесс инициализации в Java. Еще раз спасибо за подробное объяснение!
Наконец, чтобы завершить тему, я хотел бы понять, как работают нативные сеттеры. Разве они не требуют отражения или подобных механизмов для изменения конечных полей? Если код написан на C/C++, можем ли мы напрямую получить доступ к определенной ячейке памяти и изменить ее значение во время выполнения, как будто ничего не произошло, используя такой нативный метод? Не проводятся ли ограничительные проверки? Я никогда раньше не рассматривал эти свойства машинного кода.
Спасибо тебе за все! Это был один из самых подробных ответов, которые только можно себе представить! Я очень благодарен!
«Назначение конечных полей запрещено для кода Java» — это в лучшем случае вводящее в заблуждение утверждение. Существует три различных сценария: 1) Назначение обычным полям конечного экземпляра (вне конструктора) возможно через Reflection с переопределением доступа. 2) Назначение статических конечных полей или конечных полей записей или скрытых классов после их инициализации вообще не поддерживается. 3) Три поля System.out, System.err и System.in настолько особенные, что спецификация предоставляет им собственную категорию
хотя явное присвоение значения null в коде может показаться излишним, оно служит значением-заполнителем до тех пор, пока не произойдет фактическая инициализация с помощью собственных методов. Процесс инициализации гарантирует, что стандартные потоки ввода/вывода настроены правильно, прежде чем приложение Java сможет получить к ним доступ.
Да, я полностью с вами согласен. Это необходимо для того, чтобы компилятор вообще допускал такой код, поскольку от нас требуется явно инициализировать все конечные поля, которые есть в нашем коде. Мне просто было интересно, где именно происходит инициализация стандартных потоков и как разрешается нулевая коллизия. В целом, мне уже дали небольшое руководство о том, как это происходит. Теперь становится немного яснее. В любом случае, спасибо вам огромное, что не просто прошли мимо!
Я попытался инициализировать стандартные потоки ввода-вывода в классе Java System, вызвав метод RegisterNatives() из статического блока инициализации в JDK 14. Почему? На разных платформах все по-разному. Практически у всех, кроме Windows (и мэйнфреймов), есть «/dev/stdout», «/dev/stderr» и «/dev/stdin», и их можно использовать для открытия файловых дескрипторов и начала обработки. Раньше я знал, как это делает Windows (и мэйнфреймы), но связи с реальной продуктивной работой здесь нет, поэтому я не сохранил это. Удачи!