7

我正在写一个字节码仪器。现在,我正试图找出如何在存在对象的情况下做到这一点。我想对我在 JVMS(第 4.9.4 节)中读到的两行进行一些澄清:

1)“验证者拒绝在初始化之前使用新对象的代码。”

我的问题是,这里的“用途”是什么意思?我猜这意味着:将它作为方法属性传递,调用GETFIELDPUTFIELD,或者调用它的任何实例方法。他们还有其他被禁止的用途吗?而且我相信它遵循其他指令,例如DUP,LOADSTORE是允许的。

2)“在该方法调用 myClass 或其直接超类的另一个实例初始化方法之前,该方法可以对 this 执行的唯一操作是分配在 myClass 中声明的字段。”

这意味着在一个<init>方法中,在调用另一个方法之前允许使用 GETFIELD 和 PUTFIELD <init>。但是,在 Java 中,在调用super()this()导致编译错误之前对实例字段执行任何操作。有人可以澄清一下吗?

3) 我还有一个问题。对象引用什么时候被初始化,从而可以自由使用?通过阅读 JVMS,我得出了一个答案,即对象是否被初始化取决于每种方法。在某个时间点,可以为一个方法初始化对象,但不能为另一个方法初始化对象。具体来说,当一个对象被该方法<init>调用时,该方法返回时为该方法初始化。

例如,考虑该main()方法创建一个对象并调用<init>它,然后调用超类的<init>. 从 中返回后super(),该对象现在被认为已由 初始化<init>,但尚未为 初始化main()。这是否意味着,在<init>after中super(),我可以将对象作为参数传递给方法,甚至在从 main() 返回之前。

有人可以确认整个分析是正确的吗?感谢您的时间。

ps:我实际上已经在 Sun 论坛上发布了同样的问题,但得到了回应。我希望我能在这里有更多的运气。谢谢你。

更新

首先感谢您的回答和时间。虽然我没有得到一个明确的答案(我有很多问题,其中一些有点模糊),但你的答案和例子,以及随后的实验,对我更深入地理解 JVM 是如何工作的非常有用。

我发现的主要事情是验证程序的行为因不同的实现和版本而异(这使得字节码操作的工作变得更加复杂)。问题在于不符合 JVMS,或者验证者的开发人员缺少文档,或者 JVMS 在验证者的领域有一些微妙的模糊性。

最后一件事,太棒了!!!我在 Sun JVM Specifications 官方论坛上发布了同样的问题,直到现在我仍然没有得到答案。

4

3 回答 3

4

“验证器拒绝在初始化之前使用新对象的代码。”

在字节码验证中,由于验证器是在链接时工作的,因此可以推断方法的局部变量的类型。方法参数的类型在类文件的方法签名中是已知的。其他局部变量的类型是未知的并且是推断出来的,所以我假设上面陈述中的“用途”与此有关。

编辑: JVMS的第 4.9.4 节内容如下:

myClass 类的实例初始化方法(第 3.9 节)将新的未初始化对象视为其在局部变量 0 中的 this 参数。在该方法调用 myClass 或其直接超类的另一个实例初始化方法之前,该方法可以执行的唯一操作这是分配在 myClass 中声明的字段。

上述语句中的字段分配是在为对象分配内存时将实例变量“初始”初始化为默认初始值(如 int 为 0,float 为 0.0f 等)。当虚拟机在对象上调用实例初始化方法(构造函数)时,还有一个实例变量的“正确”初始化。John Horstmann 提供


链接有助于澄清事情。所以这些说法不成立。“这并不意味着在一个<init>方法中,getfield并且putfield在另一个方法被调用之前被允许<init>。” getfield和_putfield指令用于访问(和更改)类(或类的实例)的实例变量(字段)。只有在初始化实例变量(字段)时才会发生这种情况。”

从JVMS:

每个实例初始化方法(第 3.9 节),除了从类 Object 的构造函数派生的实例初始化方法外,必须在访问其实例成员之前调用 this 的另一个实例初始化方法或其直接超类 super 的实例初始化方法。但是,在当前类中声明的 this 的实例字段可以在调用任何实例初始化方法之前分配。

当 Java 虚拟机隐式或显式地创建一个类的新实例时,它首先在堆上分配内存来保存对象的实例变量。为对象类及其所有超类中声明的所有变量分配内存,包括隐藏的实例变量。一旦虚拟机为新对象预留了堆内存,它就会立即将实例变量初始化为默认初始值。一旦虚拟机为新对象分配了内存并将实例变量初始化为默认值,它就可以为实例变量提供适当的初始值。Java 虚拟机使用两种技术来执行此操作,具体取决于对象是否由于 clone() 调用而被创建。如果由于 clone() 正在创建对象,则虚拟机将被克隆对象的实例变量的值复制到新对象中。否则,虚拟机调用对象的实例初始化方法。实例初始化方法将对象的实例变量初始化为其正确的初始值。只有在此之后,您才能使用getfieldand putfield

java 编译器为其编译的每个类生成至少一个实例初始化方法(构造函数)。如果类没有显式声明构造函数,编译器会生成一个默认的无参数构造函数,它只调用超类的无参数构造函数。在调用super()this()导致编译错误之前对实例字段进行任何操作是正确的。

一个<init>方法可以包含三种代码:另一个<init>方法的调用,实现任何实例变量初始化器的代码,以及构造函数主体的代码。如果构造函数以显式调用同一类中的另一个构造函数开始(this()调用),则其对应的<init>方法将由两部分组成:

  • 同类 <init>方法的调用
  • 实现相应构造函数主体的字节码

如果构造函数不是以调用开头this()且类不是 Object,则该<init>方法将具有三个组件:

  • 调用超类 <init>方法
  • 任何实例变量初始化器的字节码
  • 实现相应构造函数主体的字节码


如果构造函数不以this()调用开头并且类是 Object(并且 Object 没有超类),则其方法不能以超类方法调用<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>,然后执行相应构造函数的主体。因为构造函数以 a 开头,所以this()其对应的<init>方法不包含用于初始化实例变量的字节码。

在第二个构造函数中,构造函数的<init>方法有

  • 超类<init>方法,即调用超类构造函数(无arg方法),编译器默认生成this,因为super()第一条语句没有找到显式。
  • 用于初始化实例变量的字节码someint
  • 构造函数主体中其余内容的字节码。
于 2010-07-19T07:10:40.610 回答
4

与 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);
    }
}
于 2010-07-19T11:36:02.327 回答
1

我建议您下载 OpenJDK 源代码的副本并查看验证程序实际检查的内容。如果不出意外,这可能会帮助您理解 JMV 规范所说的内容。

(但是,@Joachim 是对的。依赖验证器实现的功能而不是规范所说的内容是相当冒险的。)

于 2010-07-19T07:06:31.127 回答