На собеседованиях часто спрашивают, какие в нем есть методы, поэтому они как-то сами собой выучились. Пришло время посмотреть на этот класс более внимательно. Первый вопрос, который у меня возник, есть ли вообще в исходниках Java класс java.lang.Object. Он же ведь необычный, он вполне может быть жестко зашит в реализацию, как самый верхний.
Однако, такой класс есть и я приведу тут исходники java/lang/Object.java, опустив javadoc, и попытаюсь пролить свет на некоторые моменты связанные с реализацией jvm:
package java.lang;
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
protected void finalize() throws Throwable { }
}
Что бы я хотел отметить в этом коде?
Всего в Object 11 публичных методов, 5 обычных и 6 с нативной реализацией.
Рассмотрим обычные методы, так как их код уже доступен.
По дефолту все объекты сравниваются на равенство ссылок. Мне, кстати, в своем время понравилась шутка про то, что для того, чтобы запутать C++ программистов указатели в Java названы ссылками.
public boolean equals(Object obj) {
return (this == obj);
}
toString тоже не содержит ничего необычного, кроме разве того, что hashCode() преобразуется в шестнадцатеричную строку. И если бы apangin не написал, что нынче как только нельзя посчитать hashCode, я бы подумал, что раньше Java программисты могли найти свой объект по hashCode, т.к. он был не чем иным как ссылкой. Те 32 битные времена для многих прошли, и теперь даже не знаю, есть ли смысл в toString() выводить hashCode.
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
Кроме того, что wait относится к примитивам обеспечивающим многопоточность, хочется отметить бесполезность параметра nanos.
В некоторых случаях он просто добавляет одну милисекунду. Интересно, это закладка на будущее или уже есть системы в которых у wait(long timeout, int nanos) другая реализация.
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
Завершает парад обычных методов в java.lang.Object:
protected void finalize() throws Throwable { }
Этот метод ничего не делает, и есть куча материалов о том, что следует избегать его использования finalize и Finalizer, смысл finalize.
Теперь посмотрим на на java/lang/Object.class Например, мне интересно что в нем указано в качестве супер класса. Находим в установленном jre или jdk rt.jar, распаковываем:
jar -xf rt.jar
И видим, что в super class у него прописаны 00 00, интересно что будет, если руками создать class файл без супер класса.
Я взял Hello.class из моей предыдущей заметки.
Открыл его в vim и заменил содержание буфера на hex дамп vim.wikia.com/wiki/Hex_dump:
:%!xxd
Поразился мощи vim редактора. Быстренько нашел байты для super_class. Напомню, они лежат согласно спецификации через 4 байта после окончания constant_pool. Конец constant_pool ищется по тегу строки 00 01 и последовательности не нулевых байтов, когда начинаются нули идут другие разделы constant_pool. Для других class файлов это может быть не так, но в моем случае сработало.
Возвращаемся обратно к бинарному виду:
:%!xxd -r
Сохраняем изменения. Запускаем наше поправленное приложение:
java -cp classes/ hello.App
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Invalid superclass index 0 in class file hello/App
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Ошибка, да еще не какая-нибудь, а выброшенная из нативного метода во время загрузки классов. Пойдем разбираться, за одно может поймем как выбрасывать такие ошибки.
Нам нужны исходники jdk. Я выбрал OpenJDK для исследования. Будем качать их отсюда:
hg.openjdk.java.net/jdk8/jdk8
И хранить в Меркурии:
hg clone hg.openjdk.java.net/jdk8/jdk8
Но на этом не всё.
Надо еще запустить:
./get_source.sh
И подождать. Отлично, исходники скачались и можно искать нашу ошибку. Я делаю это grep-ом:
grep -nr 'Invalid superclass index' *
hotspot/src/share/vm/classfile/classFileParser.cpp:3095: "Invalid superclass index %u in class file %s",
hotspot/src/share/vm/classfile/classFileParser.cpp:3100: "Invalid superclass index %u in class file %s",
Открываем classFileParser.cpp и там на 3095 строчке:
instanceKlassHandle ClassFileParser::parse_super_class(int super_class_index,
TRAPS) {
instanceKlassHandle super_klass;
if (super_class_index == 0) {
check_property(_class_name == vmSymbols::java_lang_Object(),
"Invalid superclass index %u in class file %s",
super_class_index,
CHECK_NULL);
} else {
check_property(valid_klass_reference_at(super_class_index),
"Invalid superclass index %u in class file %s",
super_class_index,
CHECK_NULL);
// The class name should be legal because it is checked when parsing constant pool.
// However, make sure it is not an array type.
bool is_array = false;
if (_cp->tag_at(super_class_index).is_klass()) {
super_klass = instanceKlassHandle(THREAD, _cp->resolved_klass_at(super_class_index));
if (_need_verify)
is_array = super_klass->oop_is_array();
} else if (_need_verify) {
is_array = (_cp->unresolved_klass_at(super_class_index)->byte_at(0) == JVM_SIGNATURE_ARRAY);
}
if (_need_verify) {
guarantee_property(!is_array,
"Bad superclass name in class file %s", CHECK_NULL);
}
}
return super_klass;
}
Нас интересует вот эта часть:
if (super_class_index == 0) {
check_property(_class_name == vmSymbols::java_lang_Object(),
"Invalid superclass index %u in class file %s",
super_class_index,
CHECK_NULL);
}
check_property лежит в заголовочном файле classFileParser.hpp и выглядит так:
inline void check_property(bool property, const char* msg, int index, TRAPS) {
if (_need_verify) {
guarantee_property(property, msg, index, CHECK);
} else {
assert_property(property, msg, index, CHECK);
}
}
Я стал искать где выставляется _need_verify и за что отвечает. Оказалось в classFileParser.cpp есть вот такая строчка:
_need_verify = Verifier::should_verify_for(class_loader(), verify);
verify передается при вызове:
instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name,
ClassLoaderData* loader_data,
Handle protection_domain,
KlassHandle host_klass,
GrowableArray<Handle>* cp_patches,
TempNewSymbol& parsed_name,
bool verify,
TRAPS)
Этот метод вызывается во многих местах, но нас интересует в hotspot/src/share/vm/classfile/classLoader.cpp:
instanceKlassHandle result = parser.parseClassFile(h_name,
loader_data,
protection_domain,
parsed_name,
false,
CHECK_(h));
Как же устроен should_verify_for в hotspot/src/share/vm/classfile/verifier.cpp:
bool Verifier::should_verify_for(oop class_loader, bool should_verify_class) {
return (class_loader == NULL || !should_verify_class) ?
BytecodeVerificationLocal : BytecodeVerificationRemote;
}
Так как в should_verify_class мы передаем false, смотрим BytecodeVerificationLocal в hotspot/src/share/vm/runtime/arguments.cpp:
// -Xverify
} else if (match_option(option, "-Xverify", &tail)) {
if (strcmp(tail, ":all") == 0 || strcmp(tail, "") == 0) {
FLAG_SET_CMDLINE(bool, BytecodeVerificationLocal, true);
FLAG_SET_CMDLINE(bool, BytecodeVerificationRemote, true);
} else if (strcmp(tail, ":remote") == 0) {
FLAG_SET_CMDLINE(bool, BytecodeVerificationLocal, false);
FLAG_SET_CMDLINE(bool, BytecodeVerificationRemote, true);
} else if (strcmp(tail, ":none") == 0) {
FLAG_SET_CMDLINE(bool, BytecodeVerificationLocal, false);
FLAG_SET_CMDLINE(bool, BytecodeVerificationRemote, false);
} else if (is_bad_option(option, args->ignoreUnrecognized, "verification")) {
return JNI_EINVAL;
}
// -Xdebug
}
Зарываясь дальше можно найти черную магию с макросами в hotspot/src/share/vm/runtime/globals_extension.hpp:
#define FLAG_SET_CMDLINE(type, name, value) (CommandLineFlagsEx::type##AtPut(FLAG_MEMBER_WITH_TYPE(name,type), (type)(value), Flag::COMMAND_LINE))
class CommandLineFlagsEx : CommandLineFlags {
public:
static void boolAtPut(CommandLineFlagWithType flag, bool value, Flag::Flags origin);
static void intxAtPut(CommandLineFlagWithType flag, intx value, Flag::Flags origin);
static void uintxAtPut(CommandLineFlagWithType flag, uintx value, Flag::Flags origin);
static void uint64_tAtPut(CommandLineFlagWithType flag, uint64_t value, Flag::Flags origin);
static void doubleAtPut(CommandLineFlagWithType flag, double value, Flag::Flags origin);
static void ccstrAtPut(CommandLineFlagWithType flag, ccstr value, Flag::Flags origin);
static bool is_default(CommandLineFlag flag);
static bool is_ergo(CommandLineFlag flag);
static bool is_cmdline(CommandLineFlag flag);
};
Но меня это пока не интересует. Мне надо выяснить значение BytecodeVerificationLocal, в случае когда jvm стартует без параметра -Xverify. Это можно найти в коде, но мне кажется, сейчас не уместным лезть дальнейшие дерби и пора выбираться. Документация в помощь. По дефолту jvm запускается с параметром -Xverify:remote и BytecodeVerificationLocal будет false.
Значит _need_verify тоже false и в check_property вызывается assert_property(property, msg, index, CHECK) с параметрами false, «Invalid superclass index %u in class file %s», 0, CHECK_NULL.
inline void assert_property(bool b, const char* msg, int index, TRAPS) {
#ifdef ASSERT
if (!b) {
ResourceMark rm(THREAD);
fatal(err_msg(msg, index, _class_name->as_C_string()));
}
#endif
}
Собственно, здесь и выбрасывается сообщение об ошибке. Теперь посмотрим на fatal(msg), чтобы узнать как это делается.
Хотя, на часть вопроса мы уже ответили. Нельзя сделать classfile в котором для поля super_class будет значение 0 и загружать его с помощью дефолтного ClassLoader.
Итак, fatal определенный в hotspot/src/share/vm/utilities/debug.hpp:
#define fatal(msg) do { report_fatal(__FILE__, __LINE__, msg); BREAKPOINT; } while (0)
hotspot/src/share/vm/utilities/debug.cpp:
void report_fatal(const char* file, int line, const char* message)
{
report_vm_error(file, line, "fatal error", message);
}
void report_vm_error(const char* file, int line, const char* error_msg,
const char* detail_msg)
{
if (Debugging || error_is_suppressed(file, line)) return;
Thread* const thread = ThreadLocalStorage::get_thread_slow();
VMError err(thread, file, line, error_msg, detail_msg);
err.report_and_die();
}
Реализация report_and_die() в hotspot/src/share/vm/utilities/vmError.cpp нетривиальна, но из нее следует, что в Java мы уже не возвращаемся и выводим сообщение об ошибке из недр jvm. На этом я хочу переостановить исследование jvm и java.lang.Object.
Выводы
java.lang.Object особый класс, имеющий уникальный class file, в котором в качестве суперкласса не указан ни один класс. Создать такой же класс средствами языка Java нельзя, но также затруднительно, если вообще возможно, сделать это и манипуляциями с байтами class файла. Надеюсь у меня получилось передать часть восхищения, которое я испытывал исследуя исходники jvm. Призываю всех попробовать сделать то же самое.
Комментарии (16)
gurinderu
25.08.2015 11:29+1Хардкорно. Зародилась идея сделать свои Object с блекджеком.
Спасибо за статью.

corvette
25.08.2015 11:36+2Довольно интересно, хотя я не знаю метода сделать это не нарушая лицензию добавить свои методы java.lang.Object. Мои эксперименты показали, что final методы добавить довольно просто, а вот с не-final возникают трудности, из-за того, что отступы в таблице виртуальных методов зашиты в jvm.

WFrag
25.08.2015 18:45Через DCEVM (это патченная/хаченная JVM из OpenJDK) можно redefineClasses на java/lang/Object сделать. Там даже тест такой был, но он оказался несколько, гм, нестабильным, так как последствия такого расползаются по всей JVM.
Ну т.е DCEVM-то про другое, про перегрузку в дебаге произвольных изменений в классах, но как побочный эффект можно и в java/lang/Object добавлять методы.
Делается все через стандартный API redefineClasses из java.lang.instrument.Instrumentation.
В стандаратной JVM, без DCEVM, скорее всего можно так поведение не native методов поменять.

vedenin1980
25.08.2015 12:58+1Если сравнивать Object в C# (и вообще всех языках .Net платформы), то он почти такой же за исключения того что нет функций параллельного программирования, вроде wait, sleep, hold и т.п. (по-хорошему они и в Java остались как рудименты эпохи когда не было java.util.concurrent). Есть функции-аналоги Equals (и ReferenceEquals отдельно для сравнения ссылок), Finalize, GetHashCode, GetType (тот же getClass), MemberwiseClone (тот же clone), ToString.
У D минимализм судя по всему есть функции-аналоги Equals, getHashCode, compare, toString
vedenin1980
25.08.2015 13:07Да, у D в Object'е есть метод factory, если правильно понимаю суть его в создание произвольного объекта по его тестовому имени.

Zlobober
26.08.2015 08:42Забавно, при при прочтении статьи глаз зацепился за слово `klass` в исходниках OpenJDK, и в качестве гипотезы я предположил, что это связано с тем, что слово `class` зарезервировано в C++, и использовать его в коде неудобно. Действительно, так и есть.

SergioShpadi
26.08.2015 10:43+1Так часто делают, хоть это и совсем неправильно. Еще часто пишется clazz

lany
26.08.2015 14:26А как правильно?

SergioShpadi
26.08.2015 18:39+1Ну лучше instanceClass. Я говорю больше про Java и ему подобные.
Я не знаю специфики C++, но в том же куске кода из статьи в двух идущих подряд линиях написано и klass и class
instanceKlassHandle super_klass;
if (super_class_index == 0) {
WFrag
27.08.2015 08:18Ну, справедливости ради, в Open JDK instanceKlass и klass — это две разные сущности. Например, класс массива будет klass, а класс объекта — instanceKlass (подкласс klass).
vedenin1980
Есть конечно, дефолтный toString() часто используется для логирования, с помощью хеша можно сразу увидеть в логах один это объект или миллион разных, но того же типа.
lany
Можно также отметить, что там выводится не
identityHashCode. Если вы переопределилиhashCode, но оставилиtoString, будет использоваться вашhashCode. Хотя, конечно, лучше в любых классах переопределятьtoString.