自制Java虚拟机(五)实现继承、多态、invokevirtual

自制Java虚拟机(五)实现继承、多态、invokevirtual

本篇文章将研究如何实现面向对象的继承和多态特性,同时实现invokevirtual

一、实例属性的继承

继承实现了数据与方法的复用。

  1. 类属性与实例属性

    • 类属性的修饰符要加上static,是属于类的
    • 类属性只有一份,该类创建的多个对象共享同一份类属性,jvm中由getstaticputstatic指令操作
    • 实例属性每个对象各自一份,各管各的互不干扰,jvm中由getfieldputfield指令操作

      上篇文章在没有考虑没有继承的情况下实现了getfieldputfield指令,本篇文章来对其进行修正。

  2. publicprotectedprivate

    不管是父类的实例属性是publicprotected还是private,子类继承自父类时,这些属性都会一一继承,只不过private的属性在子类中不能直接访问而已。你看不到它们,但它们确实存在。

现在问题来了:

  • 创建对象时,我们要给从父类(以及父类的父类…)继承过来的实例属性分配内存,并指定索引以便访问,如何安排这些索引?
  • 子类中访问父类的publicprotected实例属性时,索引是指向子类常量池的Fieldref类型的结构,而这个结构的class_index是指向当子类的,必须能够正确地解析到定义该属性的类

解决方法:

  • 实例属性的安排。在类的继承链上,自顶向下给每个类的实例属性从小到大依次指定索引,这样就保证每个类的实例属性在该继承链中索引是唯一的。如下图布局1所示:

    自制Java虚拟机(五)实现继承、多态、invokevirtual_第1张图片

    因此我们在创建对象的时候必须从当前类开始,沿着继承链遍历父类,计算继承而来的属性个数并重新给每个类的实例属性指定索引(之前计算的的索引是在当前类的)。

    修正之后创建对象的函数newObject实现可以如下:

    Object* newObject(OPENV *env, Class* pclass) {
      CONSTANT_Class_info* parent_class_info;
      Object *obj;
      Class *tmp_class;
      int i = 0, total_size = 0;
      // step 1: load parent class recursively
      tmp_class = pclass;
      while (tmp_class->super_class) {
          parent_class_info = tmp_class->constant_pool[tmp_class->super_class];
          if (NULL == parent_class_info->pclass) {
              parent_class_info->pclass = systemLoadClassRecursive(env, (CONSTANT_Utf8_info*)(tmp_class->constant_pool[parent_class_info->name_index]));
              tmp_class->parent_class = parent_class_info->pclass;
          }
          tmp_class = tmp_class->parent_class;
      }
      // step 2: calculate fields size of parent class recursively, if not calculated yet
      if (pclass->parent_fields_size == -1) {
          if (pclass->parent_class == NULL) {
              pclass->parent_fields_size = 0;
          } else {
              pclass->parent_fields_size = getClassFieldsSize(pclass->parent_class);
          }
          for(i=0; ifields_count; i++) {
              // 重新计算属性的索引:过滤掉类属性
              if (NOT_ACC_STATIC((pclass->fields[i])->access_flags)) {
                  (pclass->fields[i])->findex += pclass->parent_fields_size;
              }
          }
      }
      // 该对象属性的总大小 = 父类实例属性大小 + 本类实例属性大小
      total_size = pclass->parent_fields_size  + pclass->fields_size;
      obj = (Object*)malloc(sizeof(Object) + ((total_size+1)<<2));
    
      obj->fields = (char*)(obj+1);
      obj->pclass = pclass;
      obj->length = (total_size+1) << 2;
      return obj;
    }

    由于父类可能需要多次用到,我们可以把解析好的父类保存在一个哈希中,用类的全限定名做为索引,如test/Parent,需要引用父类的时候先用哈希表中找,找不到就加载,加载完毕保存在哈希表中,为了方便,在当前类中设置个字段指向父类。

    typedef struct _ClassFile{
    ...
      struct _ClassFile *parent_class; // 指向父类
      int parent_fields_size;
      int fields_size;
    } ClassFile;
    
    typedef ClassFile Class;

    (java/lang/Object是所有类的终极父类,我们一直往上解析的话最终会涉及到这个类,可以从Java的安装目录的jdk目录下找到src.zip这个包,这个包有jdk的源码,把它解压到指定目录,编译成class文件,到时我们的类加载函数从这里加载就行)

    自制Java虚拟机(五)实现继承、多态、invokevirtual_第2张图片

  • 实例属性的解析

    ​ 实例属性的符号链的class_index指向子类。这时必须从子类的fields数组中查找,找不到则上溯到父类,直到找到为止。拿下面的例子来说明下:

    Parent.java

    package test;
    
    class Parent
    {
    public int x1;
    protected int y1;
    private int z1;
    
    public void setZ1(int z1)
    {
        this.z1 = z1;
    }
    public int getZ1()
    {
        return this.z1;
    }
    
    protected int addXY()
    {
        return this.x1 + this.y1;
    }
    }

    Child.java

    package test;
    
    class Child extends Parent
    {
      private int x2 = 4;
    
      public int doSomething()
      {
          int xy = this.addXY(); // invoke protected method of parent class
    
          return xy + super.getZ1() + this.x2;
      }
    }

    TestInheritance.java

    package test;
    
    class TestInheritance
    {
      public static void main(String[] args)
      {
          Child someone = new Child();
    
          someone.setZ1(3); // invoke public method of parent class
          someone.x1 = 1;   // access public field
          someone.y1 = 2;   // access protected field
    
          int result = someone.doSomething();
      } 
    }

Child类继承了Parent类,然后在TestInheritance类中,我们创建了Child类的一个实例,someone.x1=1这行代码,访问的是Parent类中定义的实例属性。查找过程如下:

自制Java虚拟机(五)实现继承、多态、invokevirtual_第3张图片

修正后的解析实例属性的代码可以如下:

void resolveClassInstanceField(Class* caller_class, CONSTANT_Fieldref_info **pfield_ref)
{
    ... // 省略
    do {
        callee_cp = callee_class->constant_pool;
        fields_count = callee_class->fields_count;
        for (i = 0; i < fields_count; i++) {
            field = (field_info*)(callee_class->fields[i]);
            tmp_field_name_utf8 = (CONSTANT_Utf8_info*)(callee_cp[field->name_index]);
            if (NOT_ACC_STATIC(field->access_flags) &&
                field_name_utf8->length == tmp_field_name_utf8->length &&
                strcmp(field_name_utf8->bytes, tmp_field_name_utf8->bytes) == 0) {
                tmp_field_descriptor_utf8 = (CONSTANT_Utf8_info*)(callee_cp[field->descriptor_index]);
                if (field_descriptor_utf8->length == tmp_field_descriptor_utf8->length &&
                    strcmp(field_descriptor_utf8->bytes, tmp_field_descriptor_utf8->bytes) == 0) {
                    field_ref->ftype  = field->ftype;
                    field_ref->findex = field->findex;
                    found = 1;

                    break;
                }
            }
        }

        if (found) {
            break;
        }
        // 没找到就从父类中找
        callee_class = callee_class->parent_class;
    } while(callee_class != NULL);
    ... // 省略
}

与之前的代码相比,变化主要为:

  1. 查找的代码外多包裹了一个do while循环,用来自底向上遍历
  2. 属性匹配由纯比较name_index改成比较属性名字的字符串长度和内容(因为常量池不一样了)

另外注意,上一篇我们是用当前类来创建对象的,现在可不是了,我们是在TestInheritance类中创建Child类,所以new指令的实现需要修正下:

Opreturn do_new(OPENV *env)
{
    Class* pclass;
    PRINTSD(TO_SHORT(env->pc));
    short index = TO_SHORT(env->pc);
    Object *obj;

    if (env->current_class->this_class == index) {
        pclass = env->current_class;
    } else {
        // begin resolve non-current class
        class_info = (CONSTANT_Class_info*)(env->current_class->constant_pool[index]);
        // 如果该类没有解析过,则获取类名,根据类名从已加载类的哈希中查找
        if (class_info->pclass == NULL) {
            utf8_info = (CONSTANT_Utf8_info*)(env->current_class->constant_pool[class_info->name_index]);
            pclass = class_info->pclass = findLoadedClass(utf8_info->bytes, utf8_info->length);
        }
        // 要是没找到就调用类加载函数加载(同时加载该类的父类...)
        if (NULL == pclass) {
            pclass = class_info->pclass = systemLoadClassRecursive(env, utf8_info);
        }
        // end resolve non-current class
    }

    obj = newObject(env, pclass);
    PUSH_STACKR(env->current_stack, obj, Reference);

    INC2_PC(env->pc);
}

二、invokevirtual中方法的解析

invokevirtual与invokespecial

上面TestInheritance.java的代码:

someone.setZ1(3)  // 调用父类的public方法(隐式)

该行代码对应的字节码(取最后一行)是invokevirtual #4,#4在当前常量池中指向的是Methodref类型的结构,该method的名称与类型为test/Child.setZ1:(I)V。可是我们从源代码中可以看到,test/Child类并没有定义该方法,该方法是从父类test/Parent中继承过来的。

另一行代码:

int result = someone.doSomething(); // 调用本类的public方法

其中someone.doSomething()对象的字节码为invokevirtual #7,#7在当前常量池指向的是Methodref类型的结构,该method的名称与类型为test/Child.doSomething:()I。该方法是在test/Child类中定义的public方法。

Child.java中的代码:

// doSomething方法中
super.getZ1() // 显式调用父类的public方法

对应的字节码为invokespecial #4,#4在当前常量池指向的是Methodref类型的结构,该method的名称与类型为test/Parent.getZ1:()I。该方法是在test/Parent类中定义的public方法。

代码 当前常量池中指向的类 定义该方法的类 方法类型 指令
someone.setZ1(3) test/Child test/Parent public invokevirtual
someone.doSomething() test/Child test/Child public invokevirtual
super.getZ1() test/Parent test/Parent public invokespecial

可见,显式调用父类的方法生成的是invokespecial指令,而隐式调用父类的public方法生成的是invokevirtual指令。它们在当前常量池指向的类也不一样,invokespecial #n,n指向的是定义该方法的类,而invokevirtual #n,n指向的类则未必。

jvm(version8)中说道:

The difference between the invokespecial instruction and the invokevirtual instruction (§invokevirtual) is that invokevirtual invokes a method based on the class of the object. The invokespecial instruction is used to invoke instance initialization methods (§2.9) as well as private methods and methods of a superclass of the current class.

invokevirtual是基于对象的类来调用的方法的,而invokespecial用于调用实例初始化方法(构造函数吧),private方法和当前类的父类的方法(需要显式调用,super.method())。所以invokevirtual指令的实现中,方法的解析可以按如下流程:

自制Java虚拟机(五)实现继承、多态、invokevirtual_第4张图片

三、多态

不同类的对象对同一消息做出不同的响应叫做多态。多态需要三个条件:

  1. 有继承关系
  2. 子类重写父类的方法
  3. 父类引用指向子类对象。

下面是一个多态的例子:

Employee.java

package test;

class Employee
{
    private int baseSalary;
    Employee(int baseSalary)
    {
        this.baseSalary = baseSalary;
    }

    public int getSalary()
    {
        return this.baseSalary;
    }
}

Engineer.java

package test;

class Engineer extends Employee
{
    private int bonus;
    Engineer(int baseSalary, int bonus)
    {
        super(baseSalary);
        this.bonus = bonus;
    }
    public int getSalary()
    {
        return super.getSalary() + this.bonus;
    }
}

Manager.java

package test;

class Manager extends Employee
{
    private int bonus;
    Manager(int baseSalary, int bonus)
    {
        super(baseSalary);
        this.bonus = bonus;
    }

    public int getSalary()
    {
        return 2 * super.getSalary() + bonus;
    }
}

TestPoly.java

package test;

class TestPoly
{
    private int getSalary(Employee e)
    {
        // getSalary will be resolved to different method according the real Class of e
        return e.getSalary(); // attention here
    }

    public static void main(String[] args)
    {
        TestPoly obj = new TestPoly();
        Employee emp = new Employee(100);
        Employee eng = new Engineer(150, 50); // upcasting
        Employee mgr = new Manager(100, 100); // upcasting

        int salary1 = obj.getSalary(emp);
        int salary2 = obj.getSalary(eng);
        int salary3 = obj.getSalary(mgr);

        int result = salary1;
        result = salary2;
        result = salary3;
    }
}

上面多态的例子,TestPoly这个类的getSalary方法:

e.getSalary()

这行代码对应的字节码为:

invokevirtual #2

其中#2指向的Methodref,其对应的方法为:test/Employee.getSalary:()I。

实际上,当e为不同类创建的对象时,该方法需要解析到创建该对象的类(可能是Empolyee的子类),不一定是Employee类。如果e是Manager类创建的对象,我们就要把该方法解析到Manager类中定义的getSalary方法。上面我们实现的invokevirtual指令,解析方法的流程仍然是有效的,只不过由于一个Methodref可能对应多个方法入口了,我们需要一个表来保存这些方法入口,这样后面重复调用时就可以先从这个表中查,查不到再解析。

// 方法表中的一项(方法入口)
typedef struct _MethodEntry {
    Class *pclass;        // 类
    method_info *method; // 方法
    struct _MethodEntry *next;
} MethodEntry;
// 方法表
typedef struct _MethodTable {
    MethodEntry *head; // 指向第一项
    MethodEntry *tail; // 指向最后一项
} MethodTable;
// 创建新的方法入口
MethodEntry* newMethodEntry(Class *pclass, method_info *method)
{
}
// 创建新的方法表
MethodTable* newMethodTable()
{
}
// 往方法表中添加一个方法入口
void addMethodEntry(MethodTable *mtable, MethodEntry *mte)
{
}
// 根据类从方法表中查找方法入口
MethodEntry* findMethodEntry(MethodTable *mtable, Class *pclass)
{
}

添加方法表后Methodref如图所示:

自制Java虚拟机(五)实现继承、多态、invokevirtual_第5张图片

invokevirtual可如下实现:

Opreturn do_invokevirtual(OPENV *env)
{
    PRINTSD(TO_SHORT(env->pc));
    short mindex = TO_SHORT(env->pc);
    INC2_PC(env->pc);

    callClassVirtualMethod(env, mindex);
}
void callClassVirtualMethod(OPENV *current_env, int mindex)
{
    ... // 省略
    // 如果还没有建立方法表,建立方法表
    if (NULL == method_ref->mtable) {
        method_ref->args_len = getMethodrefArgsLen(current_class, nt_info->descriptor_index);
        method_ref->mtable = newMethodTable();
    }
    // 获取调用该方法的对象
    caller_obj = *(Reference*)(current_env->current_stack->sp - ((method_ref->args_len+1)<<2));
    // 从方法表中查找方法入口,没有找到就解析
    if(NULL == (mentry = findMethodEntry(method_ref->mtable, caller_obj->pclass))) {
        mentry = resolveClassVirtualMethod(caller_obj->pclass, &method_ref, (CONSTANT_Utf8_info*)(cp[nt_info->name_index]), (CONSTANT_Utf8_info*)(cp[nt_info->descriptor_index]));
    }
    // 调用该方法
    callResolvedClassVirtualMethod(current_env, method_ref, mentry);
}

由于invokevirtual是根据对象的类来调用的,所以需要获取对象的类,则需要先获取对象,而对象在操作数栈中,不知道方法参数的长度是定位的,需要用getMethodrefArgsLen获取参数长度。

解析方法:

MethodEntry* resolveClassVirtualMethod(Class* caller_class, CONSTANT_Methodref_info **pmethod_ref, CONSTANT_Utf8_info *method_name_utf8, CONSTANT_Utf8_info *method_descriptor_utf8)
{
    ... // 省略
    do {
        callee_cp = callee_class->constant_pool;
        for (i = 0; i < callee_class->methods_count; i++) {
            method = (method_info*)(callee_class->methods[i]);
            tmp_method_name_utf8 = (CONSTANT_Utf8_info*)(callee_cp[method->name_index]);
            tmp_method_descriptor_utf8 = (CONSTANT_Utf8_info*)(callee_cp[method->descriptor_index]);

            if (method_name_utf8->length == tmp_method_name_utf8->length &&
                strcmp(method_name_utf8->bytes, tmp_method_name_utf8->bytes) == 0) {
                if (method_descriptor_utf8->length == tmp_method_descriptor_utf8->length &&
                    strcmp(method_descriptor_utf8->bytes, tmp_method_descriptor_utf8->bytes) == 0) {
                    mentry = newMethodEntry(callee_class, method);
                    addMethodEntry(method_ref->mtable, mentry);
                    found = 1;

                    break;
                }
            }
        }

        if (found) {
            break;
        }
        callee_class = callee_class->parent_class;
    } while (callee_class != NULL);
    ... // 省略
}

跟解析字段流程类似。

方法解析出来,调用就很简单了,跟之前的做法类似,就不上代码了。

四、测试

1. 实例属性的继承与普通的invokevirtual

把上面的三个文件:Parent.java, Child.java, TestInheritance.java编译成字节码:

javac Parent.java Child.java TestInheritance.java

这三个文件都在test目录中。我们加载test/TestInheritance.class,运行main函数,结果如下:

自制Java虚拟机(五)实现继承、多态、invokevirtual_第6张图片
该程序的做了以下事情:

1. 在TestInheritance中创建Child类的实例someone  // new 指令
2. someone调用Parent类的public方法setZ1将继承而来的private变量z1设为3 // invokevirtual
3. someone直接访问从Parent类继承而来的x1, y1属性,设置x1=1,y1=2 // setfield
4. someone调用Child类中定义的doSomething public方法 // invokevirtual
   a. doSomething 中隐式调用Parent类的addXY protected方法  // invokevirtual
      i. Parent类的addXY()方法把x1, y1的值相加并返回
   b. doSomething 中用super.getZ1()显式调用Parent类的getZ1 public 方法 // invokespecial
   c. 把几个数相加返回
最终执行的是x1 + y1 + z1 + x2 = 1 + 2 + 3 + 4 = 10
// 故意搞这么复杂就是为了测试我们的程序正确与否

可见程序运行正确。

2. 多态的测试

把test/Employee.java, test/Engineer.java, test/Manager.java, test/TestPoly.java编译成字节码:

javac test/Employee.java test/Engineer.java test/Manager.java test/TestPoly.java

让我们的程序加载test/TestPoly.class运行,结果如下:

自制Java虚拟机(五)实现继承、多态、invokevirtual_第7张图片

可见运行正确,方法被解析到了对象各自的类所定义的方法。

五、总结

综上,本文实现了:

  • 加载多个class文件
  • 面向对象的继承特性
  • 面向对象的多态特性
  • invokevirtual指令

暂时没有考虑接口(interface)。

你可能感兴趣的:(java虚拟机)