В Java в вершине иерархии классов лежит класс java.lang.Object. Лежит и лежит, долгое время я им совсем не интересовался.

На собеседованиях часто спрашивают, какие в нем есть методы, поэтому они как-то сами собой выучились. Пришло время посмотреть на этот класс более внимательно. Первый вопрос, который у меня возник, есть ли вообще в исходниках 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)


  1. vedenin1980
    25.08.2015 10:47
    +1

    теперь даже не знаю, есть ли смысл в toString() выводить hashCode.

    Есть конечно, дефолтный toString() часто используется для логирования, с помощью хеша можно сразу увидеть в логах один это объект или миллион разных, но того же типа.


    1. lany
      26.08.2015 09:06

      Можно также отметить, что там выводится не identityHashCode. Если вы переопределили hashCode, но оставили toString, будет использоваться ваш hashCode. Хотя, конечно, лучше в любых классах переопределять toString.


  1. gurinderu
    25.08.2015 11:29
    +1

    Хардкорно. Зародилась идея сделать свои Object с блекджеком.
    Спасибо за статью.


  1. corvette
    25.08.2015 11:36
    +2

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


    1. 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 методов поменять.


  1. NeoCode
    25.08.2015 12:33
    +2

    Интересно было бы сравнить Object'ы в разных языках программирования. Java, C#, D, что там еще…


    1. zamsky
      25.08.2015 15:42

      VA Smalltalk, чистый имидж.

      Object methodDictionary size. "620"
      


      Dolphin Smalltalk, для сравнения, 190 методов. Даже и не знаю, что даёт такое сравнение.


      1. mifki
        25.08.2015 17:10

        О, Dolphin еще кто-то помнит.


        1. zamsky
          25.08.2015 19:24

          Почему бы и нет? У меня есть на нём пару активных коммерческих проектов и несколько живых утилит для внутрикорпоративной среды.


  1. 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


    1. vedenin1980
      25.08.2015 13:07

      Да, у D в Object'е есть метод factory, если правильно понимаю суть его в создание произвольного объекта по его тестовому имени.


  1. Zlobober
    26.08.2015 08:42

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


    1. SergioShpadi
      26.08.2015 10:43
      +1

      Так часто делают, хоть это и совсем неправильно. Еще часто пишется clazz


      1. lany
        26.08.2015 14:26

        А как правильно?


        1. SergioShpadi
          26.08.2015 18:39
          +1

          Ну лучше instanceClass. Я говорю больше про Java и ему подобные.

          Я не знаю специфики C++, но в том же куске кода из статьи в двух идущих подряд линиях написано и klass и class

          instanceKlassHandle super_klass;
          if (super_class_index == 0) {


          1. WFrag
            27.08.2015 08:18

            Ну, справедливости ради, в Open JDK instanceKlass и klass — это две разные сущности. Например, класс массива будет klass, а класс объекта — instanceKlass (подкласс klass).