02. 方法的存储和调用

233 阅读5分钟

方法存储的概念

在Java中,方法并不是每个对象独立存储的。相反,方法是属于类的,而不是属于对象的。因此,方法的字节码、定义和相关信息存储在方法区(Method Area)中,而不是对象实例的数据中。

1. 方法存储在类的元数据中

  • 方法存储在方法区:Java的方法代码(字节码)和方法的相关信息(如方法名、参数类型、返回类型等)存储在方法区中

2. 方法的执行与对象实例

  • 执行时的引用:当调用对象的方法时,JVM通过对象的类型指针找到类的方法定义,并通过方法表执行相应的方法。
  • 静态方法与实例方法的区别
    • 实例方法:实例方法依赖于对象实例进行调用。实例方法在调用时会自动将当前对象(this)作为一个隐式参数传递给方法。
    • 静态方法:静态方法不依赖于对象实例,可以通过类名直接调用,静态方法也存储在方法区/元空间中。

简单示例

考虑以下示例:

public class MyClass {
    private int value; // 实例变量

    public void instanceMethod() {
        System.out.println("Instance method called.");
    }

    public static void staticMethod() {
        System.out.println("Static method called.");
    }
}

在这个例子中:

  • 实例数据valueMyClass 的实例变量,每个 MyClass 对象的实例数据部分都包含一个独立的 value
  • 方法存储instanceMethod()staticMethod() 都属于类 MyClass,它们的字节码和元数据存储在方法区/元空间中,并不存储在对象的实例数据部分。

当你创建一个 MyClass 的实例时:

MyClass obj = new MyClass();
  • obj 的实例数据部分:仅包含实例变量 value
  • 方法的调用:当你调用 obj.instanceMethod() 时,JVM通过 obj 的类型指针找到 MyClass 类的元数据,从而执行 instanceMethod() 的字节码。

结论

在Java中,方法的定义、字节码和元数据与对象实例是分离的,存储在方法区/元空间中,而对象实例的数据部分仅包含实例变量。对象通过其类型指针(在对象头中)引用其类的元数据,从而可以访问和执行类中的方法。

这种设计使得Java的内存布局更加高效,同时避免了方法在每个对象实例中重复存储,节省了内存资源。

提问:多态的方法是如何调用的?

当你调用一个实例方法时,JVM通过以下步骤来确定并执行正确的方法:

  1. 通过引用找到对象
    • 通过强引用(例如animal),找到堆内存中对应的对象。
  2. 获取对象的类型指针
    • 通过对象头中的类型指针,JVM可以访问方法区中的类信息。
  3. 访问虚方法表
    • 从类型指针指向的类信息中,JVM找到该类的虚方法表。
  4. 查找方法的实现
    • 在虚方法表中,查找要调用的方法对应的条目,找到这个方法的实际实现地址(字节码的入口地址)。
  5. 执行方法
    • JVM跳转到该地址,开始执行方法对应的字节码指令。

实例

以以下代码为例:

Animal animal = new Dog();
animal.speak();
  • 编译时
    • 编译器生成调用speak()方法的字节码指令,但在编译时并不确定具体调用的是哪一个类的speak()方法,因为animal的实际类型只有在运行时才知道。
  • 运行时
    • animal指向Dog对象。
    • JVM通过animal找到Dog对象,并通过其类型指针访问Dog类的虚方法表。
    • JVM在虚方法表中查找speak()方法的实现,并找到Dog类的speak()方法地址。
    • 最后,JVM跳转到Dog类的speak()方法实现,并开始执行。

提问:私有方法,静态方法,接口方法的调用呢?和多态的普通实例方法有什么区别?

私有方法

通过引用找到对象

  • 与普通实例方法一样,JVM首先通过引用找到对象在堆中的位置。

获取对象的类型指针

  • 访问对象头中的类型指针,找到方法区中的类信息。

直接定位方法实现

  • 因为私有方法在编译时就确定了,不需要使用虚方法表(Vtable)进行查找。编译器已经在字节码中记录了私有方法的具体位置(字节码入口地址)。
  • JVM直接跳转到这个位置并执行方法。

普通实例方法

通过引用找到对象

  • 与私有方法一样,JVM首先通过引用找到对象在堆中的位置。

获取对象的类型指针

  • 访问对象头中的类型指针,找到方法区中的类信息。

查找虚方法表

  • JVM根据对象的实际类型,在对应的虚方法表中查找方法的实现地址。

执行方法

  • JVM跳转到虚方法表中找到的方法实现位置,执行该方法。

接口方法

通过引用找到对象

  • JVM首先通过接口类型的引用找到对象在堆中的位置。

获取对象的类型指针

  • 访问对象头中的类型指针,找到对象的实际类型(这个类型是实现了接口的具体类)。

查找实现类的方法

  • JVM检查对象实际类型的类信息,确定它是否实现了所调用的接口。
  • 然后在该类的虚方法表或其他结构中查找接口方法的具体实现。

执行方法

  • 找到实现后,JVM调用该方法。

静态方法

通过类名直接调用

  • 静态方法的调用不涉及对象引用。JVM通过类名找到方法区中的类信息。

直接执行方法

  • JVM通过 invokestatic 指令直接调用静态方法。这种调用是静态的、确定的,不需要虚方法表,也不需要动态分派。

对比总结

image-20240814135851787.png