Разъяснения по байт-коду и объектам

Я пишу инструктор байт-кода. Прямо сейчас я пытаюсь выяснить, как это сделать при наличии предметов. Я хотел бы получить разъяснения по двум строкам, которые я прочитал в JVMS (раздел 4.9.4):

1) "Верификатор отклоняет код, который использует новый объект до его инициализации".

У меня вопрос, что здесь означает "использует"? Я предполагаю, что это означает: передать его как атрибут метода, вызывая GETFIELD а также PUTFIELD на нем, или вызов любого метода экземпляра на нем. Их другое запрещенное использование? И я считаю, что из этого следует, что другие инструкции, такие как DUP, LOAD а также STORE разрешены.

2) "До того, как этот метод вызовет другой метод инициализации экземпляра myClass или его прямого суперкласса, единственная операция, которую может выполнить метод, - это присвоение полей, объявленных в myClass".

Что означает, что в <init> Метод, GETFIELD и PUTFIELD разрешены перед другим <init> называется. Однако в Java выполнение любой операции над полем экземпляра перед вызовом super() или же this() приводит к ошибке компиляции. Может ли кто-нибудь уточнить это?

3) У меня есть еще один вопрос. Когда ссылка на объект становится инициализированной и, следовательно, готовой для свободного использования? Прочитав JVMS, я пришел к ответу, что независимо от того, инициализируется объект или нет, зависит от каждого метода. В определенный момент времени объект может быть инициализирован для метода, но не для другого. В частности, объект становится инициализированным для метода, когда <init> вызванный этим методом возвращает.

Например, учтите, что main() Метод создал объект и называется <init> который затем называется суперклассом <init>, После возвращения из super()объект считается инициализированным <init>, но еще не инициализирован для main(), Значит ли это, что в <init> после super()Я могу передать объект в качестве параметра методу, даже до возврата из main().

Может ли кто-нибудь подтвердить, что весь этот анализ правдив? Спасибо за ваше время.

ps: я фактически отправил тот же вопрос на форумах Sun, но с ответом. Надеюсь, мне повезет больше. Спасибо.

Обновить

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

Главное, что я обнаружил, - это то, что поведение Verifier отличается в разных реализациях и версиях (что значительно усложняет работу с байт-кодом). Проблема заключается либо в несоответствии JVMS, либо в недостаточной документации со стороны разработчиков верификатора, либо в JVMS есть небольшая неопределенность в области верификатора.

И последнее, ТАК, рок! Я разместил тот же вопрос на официальном форуме спецификаций Sun JVM, и до сих пор не получил ответа.

3 ответа

Решение

Я предлагаю вам скачать копию исходных текстов OpenJDK и посмотреть, что на самом деле проверяет верификатор. Если ничего другого, это может помочь вам понять, что говорит спецификация JMV.

(Тем не менее, @Joachim прав. Полагаться на то, что делает реализация верификатора, а не на то, что спецификация говорит, довольно рискованно.)

"Верификатор отклоняет код, который использует новый объект до его инициализации".

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

РЕДАКТИРОВАТЬ: раздел 4.9.4 JVMS гласит:

Метод инициализации экземпляра ( §3.9) для класса myClass видит новый неинициализированный объект в качестве аргумента this в локальной переменной 0. До того, как этот метод вызовет другой метод инициализации экземпляра myClass или его прямого суперкласса, единственная операция, которую может выполнить метод это назначение полей, объявленных в myClass.

Это назначение полей в вышеприведенном выражении является "начальной" инициализацией переменных экземпляра начальными значениями по умолчанию (например, int равно 0, float равно 0.0f и т. Д.), Когда выделяется память для объекта. Существует еще одна "правильная" инициализация переменных экземпляра, когда виртуальная машина вызывает метод инициализации экземпляра (конструктор) для объекта.


Ссылка, предоставленная Джоном Хорстманном, помогла уточнить вещи. Так что эти утверждения не верны."Это НЕ означает, что в <init> метод, getfield а также putfield разрешены перед другим <init> называется ". getfield а также putfield инструкции используются для доступа (и изменения) переменных экземпляра (полей) класса (или экземпляра класса). И это может произойти только тогда, когда переменные экземпляра (поля) инициализируются."

Из JVMS:

Каждый метод инициализации экземпляра (§3.9), за исключением метода инициализации экземпляра, полученного из конструктора класса Object, должен вызывать либо другой метод инициализации экземпляра этого объекта, либо метод инициализации экземпляра своего прямого суперкласса super, прежде чем будут доступны его члены экземпляра. Однако поля этого экземпляра, объявленные в текущем классе, могут быть назначены до вызова любого метода инициализации экземпляра.

Когда виртуальная машина Java создает новый экземпляр класса, неявно или явно, он сначала выделяет память в куче для хранения переменных экземпляра объекта. Память выделяется для всех переменных, объявленных в классе объекта и во всех его суперклассах, включая скрытые переменные экземпляра. Как только виртуальная машина выделяет кучу памяти для нового объекта, она немедленно инициализирует переменные экземпляра начальными значениями по умолчанию. Как только виртуальная машина выделит память для нового объекта и инициализирует переменные экземпляра значениями по умолчанию, она готова присвоить переменным экземпляра их правильные начальные значения. Виртуальная машина Java использует для этого два метода, в зависимости от того, создается ли объект из-за вызова clone(). Если объект создается из-за clone(), виртуальная машина копирует значения переменных экземпляра объекта, клонируемого в новый объект. В противном случае виртуальная машина вызывает метод инициализации экземпляра для объекта. Метод инициализации экземпляра инициализирует переменные экземпляра объекта их правильными начальными значениями. И только после этого вы можете использовать getfield а также putfield,

Компилятор Java генерирует по крайней мере один метод инициализации экземпляра (конструктор) для каждого класса, который он компилирует. Если класс явно не объявляет конструкторов, компилятор генерирует конструктор по умолчанию без аргументов, который просто вызывает конструктор суперкласса без аргументов. И правильно делать любую операцию над полем экземпляра перед вызовом super() или же this() приводит к ошибке компиляции.

<init> Метод может содержать три вида кода: вызов другого <init> метод, код, который реализует любые инициализаторы переменных экземпляра, и код для тела конструктора. Если конструктор начинается с явного вызова другого конструктора в том же классе (this() вызов) его соответствующий <init> Метод будет состоять из двух частей:

  • вызов того же класса <init> метод
  • байт-коды, которые реализуют тело соответствующего конструктора

Если конструктор не начинается с this() вызов и класс не является объектом, <init> Метод будет состоять из трех компонентов:

  • вызов суперкласса <init> метод
  • байт-коды для любых инициализаторов переменных экземпляра
  • байт-коды, которые реализуют тело соответствующего конструктора


Если конструктор не начинается с this() вызов и класс является объектом (и объект не имеет суперкласса), то его <init> метод не может начинаться с суперкласса <init> вызов метода. Если конструктор начинается с явного вызова конструктора суперкласса super() вызов), его <init> метод вызовет соответствующий суперкласс <init> метод.



Я думаю, что это отвечает на ваш первый и второй вопрос.

Обновлено:

Например,

  class Demo
  {
     int somint;

     Demo() //first constructor
     {
      this(5);
      //some other stuff..
     }

     Demo(int i) //second constructor
     {
      this.somint = i;
      //some other stuff......
     }
     Demo(int i, int j) //third constructor
     {
      super();
      //other stuffff......
     }
  }

Вот байт-код для вышеупомянутых трех конструкторов из компилятора (javac):

Demo();
  Code:
   Stack=2, Locals=1, Args_size=1
   0:   aload_0
   1:   iconst_5
   2:   invokespecial   #1; //Method "<init>":(I)V
   5:   return

Demo(int);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   invokespecial   #2; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   iload_1
   6:   putfield        #3; //Field somint:I
   9:   return

Demo(int, int);
  Code:
   Stack=1, Locals=3, Args_size=3
   0:   aload_0
   1:   invokespecial   #2; //Method java/lang/Object."<init>":()V
   4:   return

В первом конструкторе <init> Метод начинается с вызова того же класса <init> метод, а затем выполняется тело соответствующего конструктора. Потому что конструктор начинается с this(), его соответствующий <init> Метод не содержит байт-код для инициализации переменных экземпляра.

Во втором конструкторе <init> метод для конструктора имеет

  • супер класс <init> метод, т. е. вызов конструктора суперкласса (без метода arg), компилятор сгенерировал это по умолчанию, потому что нет явного super() был найден в качестве первого утверждения.
  • байт-код для инициализации переменной экземпляра someint,
  • Байт-код для остальной части содержимого в теле конструктора.

Вопреки тому, что определяет язык Java, на уровне байт-кода можно получить доступ к полям класса в конструкторе перед вызовом конструктора суперкласса. Следующий код использует библиотеку asm для создания такого класса:

package asmconstructortest;

import java.io.FileOutputStream;
import org.objectweb.asm.*;
import org.objectweb.asm.util.CheckClassAdapter;
import static org.objectweb.asm.Opcodes.*;

public class Main {

    public static void main(String[] args) throws Exception {
        //ASMifierClassVisitor.main(new String[]{"/Temp/Source/asmconstructortest/build/classes/asmconstructortest/Test.class"});
        ClassWriter cw = new ClassWriter(0);
        CheckClassAdapter ca = new CheckClassAdapter(cw);

        ca.visit(V1_5, ACC_PUBLIC + ACC_SUPER, "asmconstructortest/Test2", null, "java/lang/Object", null);

        {
            FieldVisitor fv = ca.visitField(ACC_PUBLIC, "property", "I", null, null);
            fv.visitEnd();
        }

        {
            MethodVisitor mv = ca.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitInsn(ICONST_1);
            mv.visitFieldInsn(PUTFIELD, "asmconstructortest/Test2", "property", "I");
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
            mv.visitInsn(RETURN);
            mv.visitMaxs(2, 1);
            mv.visitEnd();
        }

        ca.visitEnd();

        FileOutputStream out = new FileOutputStream("/Temp/Source/asmconstructortest/build/classes/asmconstructortest/Test2.class");
        out.write(cw.toByteArray());
        out.close();
    }
}

Инстанциация этого класса работает нормально, без ошибок проверки:

package asmconstructortest;

public class Main2 {
    public static void main(String[] args) {
        Test2 test2 = new Test2();
        System.out.println(test2.property);
    }
}
Другие вопросы по тегам