Java虚拟机(十六)--类方法解析

Java方法的解析大体上分为3步:

  1. 在Java源代码编译期间,编译器负责将Java类源代码翻译为对应的字节码指令,同时完成的工作还有Java方法的局部变量表的计算,以及最大操作数栈的计算。
  2. 在jvm运行期间,jvm加载类型,调用classFileParser:parseClassFile()函数对Java class字节码文件进行解析,这一步将会完成Java方法的解析、字节码指令存放、父类与接口类方法继承与重载等一系列逻辑。
  3. 在调用系统类加载器SCL对应用程序的Java类进行加载过程中,完成方法符号链接、验证,最重要的是完成vtable与itable的构建,从而支持在jvm运行期的方法动态绑定。

对于步骤2,解析方法时,先从字节码文件中获取方法总数。
然后遍历每个方法,创建相应的mehodoop(也就是方法对象)。
创建方法对象的步骤主要有以下几步:

  1. 读取和验证Java方法的访问标识、名称以及描述符
  2. 解析方法的属性
  3. 创建methodoop
  4. 复制字节码

方法的访问标识,名称、描述符构成方法的签名。这3个各占2个字节。

属性常见的有Code,LVT(局部变量表),具体内容可以看十三篇的附录。


methodOop对象中保存方法的访问标识,虚方法表索引,方法大小,方法名等信息,内部constMethodOop保存了方法的字节码,局部变量表,异常表,字节码与源码行对应值、最大操作数栈等信息。

< init>方法是编译器自动生成的构造方法。
在没有明确定义构造方法时,init方法就是构造方法,所有类变量非静态的都在这里初始化。所有非静态代码块的内容也在这执行

其实在字节码层次上,所有的构造方法都是叫init,只是参数不同。不管是手工生成的,还是编译器自动生成的。
如果定义了多个构造方法,那么每个构造方法其实都会初始化类变量。
当然,一个构造方法明确调用另一个构造方法的话,只会在被调用的构造方法中做类变量的初始化工作。

< clint>方法也是编译器自动生成的,执行所有静态变量的初始化以及静态代码块。


vtable(虚函数表)是Java多态中的一个十分关键的技术。

在jvm加载类的过程中,会动态解析Java类的方法及其对父类方法的重写,进而构建出一个vtable分配到instanceKlass内存区的尾部,从而支持运行期动态绑定。

计算vtable主要分为2步:

  1. 获取父类vtable大小,并将当前类的vtable的设置为父类vtable的大小
  2. 循环遍历当前Java类的每一个方法,并调用need_new_vtable_entry()函数判断,如果是true,则vtable增加1

need_vtable_entry():

  1. 检查方法有没有被final,static修饰,或者是构造函数,或者类被final修饰。满足一条都返回false
    2.检查方法是否被private修饰,如果是则返回true
  2. 检查父类有没有这个方法,父类方法是不是public或者protect的。如果是,也就是说子类重写了父类的方法,返回false

如果Java类中声明了一个非private,非final,非static的方法:如果该方法是对父类方法的重写,那么就直接更新从父类继承来的vtable的方法指针;
如果不是对父类方法的重写,那么向这个类的vtable中新增一个位置,指向该方法的内存位置。

vtable分配在instanceKlass对象实例末尾,而instance在jdk8中固定占用0x1b8大小,所以得到类对应的InstanceKlass实例地址,就可以得到虚方法表。
比如有下面2个类, Test1和p

public class Test1 extends p{
    public int a=0;
    static Integer si=6;
    String s="Hello world";

    public Test1(int d){

    }

     final void do1(){
        a++;
        System.out.println(a);

    }
    public static void main(String[] args) {
        Test1 test1=new Test1(4);
        test1.do1();
        test1.a=8;
        si=9;
        int d=si;
    }

    private void test(){
        this.a=a;
    }
}
class p{
     void do1(){}
}

Java虚拟机(十六)--类方法解析

是hsdb查看内存,可以发现
Test1的虚方法表长度为7
Java虚拟机(十六)--类方法解析
父类p的虚方法表长度为6
Java虚拟机(十六)--类方法解析
查看内存中相应的地址,可以看到从object类中继承来的前5个方法都是一样的,后面的方法不一样。
Java虚拟机(十六)--类方法解析

查看父类p中第6个方法,对应的名字,是常量池第11个元素,也就是do1
Java虚拟机(十六)--类方法解析
子类Test1中虚方法表中最后一个方法就是那个私有方法test()。


miranda方法
指那些该类所实现的接口(间接实现也算)中没有在当前类中实现的方法。会被添加到vtable中,当然这个类一定是abtract的,不然必须要实现。
如果一个米兰达方法已经在父类的vtable中了,那么子类不需要新增,只需要修改就行了。


那么vtable中会有哪些方法呢?
当前类中声明的方法method,

如果method是被static修饰,那么一定不在。
如果被final修饰,那要看父类的vtable中有没有这个方法,有的话就把地址替换(因为子类会先复制父类的vtable)。没有的话,那么这个方法就不在vtable中,因为是这个类独有的,不会被覆盖,也就不需要转发。

从修饰符的角度来说private修饰的方法,一定在vtable中,而且是新增的。


查看一个类中的方法
有以下类

public class CW1 {

    @Override
    public String toString() {
        return super.toString();
    }

    public static void main(String[] args) throws IOException {
        ClassWriter cw=new ClassWriter(0);
        cw.visit(V1_8,ACC_PUBLIC+ACC_ABSTRACT+ACC_INTERFACE,
                "org/by/Cwtest",null,"java/lang/Object",
                null);
        cw.visitField(ACC_PUBLIC+ACC_STATIC+ACC_FINAL,"LESS","I",
                null,new Integer(-1)).visitEnd();
        cw.visitField(ACC_PUBLIC+ACC_STATIC+ACC_FINAL,"EQUAL","I",
                null,new Integer(0)).visitEnd();
        cw.visitField(ACC_PUBLIC+ACC_STATIC+ACC_FINAL,"GRATER","I",
                null,new Integer(1)).visitEnd();

        cw.visitMethod(ACC_PUBLIC+ACC_ABSTRACT,"compareTo","(Ljava/lang/Object;)I",
                null,null).visitEnd();

        cw.visitEnd();

        UdClassloader udClassloader=new UdClassloader();
        Class clz= udClassloader.defineClass("org.by.Cwtest",cw.toByteArray());

        System.out.println('1');

//        String systemRootUrl = (new File("")).toURI().toURL().getPath();
//        File file=new File(systemRootUrl+"org/by/Cwtest.class");
//        String parent=file.getParent();
//        File parent1=new File(parent);
//        parent1.mkdirs();
//        file.createNewFile();
//        FileOutputStream fileOutputStream=new FileOutputStream(file);
//        fileOutputStream.write(cw.toByteArray());
    }
}

打上断点,查看
Java虚拟机(十六)--类方法解析
查看类,
Java虚拟机(十六)--类方法解析
查看方法数组处的内存,可以看到当前类中定义了多少个方法,
第一个字节03可以直到,类中有3个方法,查看上面的类信息,可以知道分别是init,main,toString。也就是说当前类中定义的方法都会记录在这里
Java虚拟机(十六)--类方法解析

查看第一个方法init的字节码
Java虚拟机(十六)--类方法解析