Когда Java VM пытается загрузить зависимости класса?

Я столкнулся с проблемой загрузчика класса, которую я не понимаю. Я видел такое же поведение на OSX с Java 1.6.0 и Windows XP.

Когда я запускаю следующий код с MyListener и MyObject не в пути класса, я получаю a NoClassDefFoundError. Однако, если я удаляю строку MyObject.add(my) или заменяю ее на MyObject.add(null), тогда код работает нормально.

Обратите внимание, что метод с неразрешимыми зависимостями никогда не используется.

Я не понимаю, почему MyObject.add(my) заставляет VM пытаться загрузить MyListener, но MyListener my = new MyListener(){}; не делает.

public class Main {

    public  void neverCalled(){
        MyListener my = new MyListener(){};
        MyObject.add(my);
    }

    public static void sayHi(){
        System.out.println("Hello");
    }

    public static void main(String[] args) {
        System.out.println("Starting...");
        sayHi();
    }
}

Нет ничего интересного в MyObject и MyListener:

public class MyObject {
    public static void add(MyListener in){}
}

public interface MyListener {}

Я сделал несколько дополнительных исследований, основанных на информации, представленной biaobiaoqi ниже. Очевидно, по какой-то неизвестной причине вызов метода с параметром, похоже, вызывает загрузку класса параметров, тогда как просто объявление переменной не выполняется.

Раздел 2.17.1 Java VM Spec, второе издание гласит:

Единственное требование, касающееся того, когда выполняется разрешение, заключается в том, что любые ошибки, обнаруженные во время разрешения, должны быть выброшены в точку в программе, где программа выполняет какое-либо действие, которое может прямо или косвенно требовать привязки к классу или интерфейс, участвующий в ошибке

Раздел 2.17.3 Java VM Spec, второе издание гласит:

Язык программирования Java позволяет гибкость реализации при связывании действий (и из-за рекурсии, загрузки), при условии соблюдения семантики языка, что класс или интерфейс полностью проверены и подготовлены до него инициализируется и что ошибки, обнаруженные во время связывания, бросаются в точку в программе, где некоторые действия выполняются программой, которая может потребовать привязки к классу или интерфейсу, участвующим в ошибке.

и, наконец, глава 8 Inside Virtual Machine говорит:

Как описано в главе 7 "Время жизни класса", различным реализациям виртуальной машины Java разрешено выполнять разрешение в разное время во время выполнения программы. Реализация может выбрать, чтобы связать все по фронту, следуя всем символическим ссылкам из исходного класса, затем все символические ссылки из последующих классов, пока каждая символическая ссылка не будет разрешена. В этом случае приложение будет полностью связано до того, как будет вызван метод main(). Такой подход называется ранним разрешением. В качестве альтернативы, реализация может решить подождать до последней минуты, чтобы разрешить каждую символическую ссылку. В этом случае виртуальная машина Java разрешит символическую ссылку только тогда, когда она будет сначала использоваться запущенной программой. Этот подход называется поздним разрешением. Реализации могут также использовать стратегию разрешения между этими двумя крайностями.

Несмотря на то, что реализация виртуальной машины Java имеет определенную свободу выбора, когда нужно разрешать символические ссылки, каждая виртуальная машина Java должна давать внешнее впечатление о том, что использует позднее разрешение. Независимо от того, когда конкретная виртуальная машина Java выполняет свое разрешение, , она всегда будет вызывать любую ошибку, которая возникает при попытке разрешить символическую ссылку в точке выполнения программы, где символическая ссылка была фактически использована в первый раз. Таким образом, пользователь всегда будет выглядеть так, как будто разрешение было запоздалым. Если виртуальная машина Java делает раннее разрешение, и во время раннего разрешения обнаруживает, что файл класса отсутствует, он не будет сообщать о файле класса, отсутствующем, бросая соответствующую ошибку до тех пор, пока в программе не будет использоваться что-то в этом файле класса. Если класс никогда не используется программой, ошибка никогда не будет выбрана.

На первый взгляд поведение, которое я вижу, по-видимому, нарушает спецификацию JVM.

Ответ 1

Я тестировал это. Когда он MyObject.add(my);, нужен только MyListener, а не MyObject. И что удивительно: когда я заменяю MyObject.add(my); на System.out.println(my);, ничего не вышло. единственное отличие заключается в том, что тип аргумента static method println() - это Object, а не MyListener.

Я много искал и нашел полезную информацию. Давайте посмотрим на следующие слова: Внутри виртуальной машины Java2

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

Первая половина может ответить, почему существует NoClassDefFoundError. Поскольку JVM может решить сам по себе, когда он должен загружать класс, возможно,

    MyListener my = new MyListener(){};
    MyObject.add(my);

такой стиль просто заставляет его загружать интерфейс MyListener.

Но вторая половина, похоже, противоречит этому. Метод neverCalled фактически никогда не вызывается, активного использования нет. Я думаю, что единственной причиной может быть спецификация java1.2.

Ответ 2

Я тестировал ваш код в eclipse в одном пакете, и он отлично работает.

Ответ 3

Метод MyObject.add является статическим и, следовательно, должен быть загружен при загрузке основного объекта, так что статические инициализаторы вызывают до загрузки класса Main. С другой стороны, объект MyListener может инициализировать статические инициализаторы при первом использовании, поскольку код может видеть, что в классе MyListener отсутствуют какие-либо вызовы GET_STATIC или PUT_STATIC.