-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
类相关的指令 #10
Comments
Invoke method接下说说调用一个方法相关的指令 invokestatic调用一个静态方法。 public class Hello {
public static void func(){
hi();
}
public static void hi(){};
}
invokevirtual调用一个成员方法 public class Hello {
public void func(){
hi();
}
public void hi(){};
}
invokespecial调用类初始化方法或者私有方法 public class Hello {
public void func(){
hi();
}
private void hi(){};
}
然后这里你可能就有问题了,为什么调用一个方法会有这么多的指令,合并成一个invokemethod不行嘛?下面就来分析下为什么会有四个调用方法的指令。 invokevirtual vs invokespecial这两个都是调用类的实例方法的,但是jvm为什么要设计两个指令呢?为什么不把它们合并在一起? 接下来我解释一下。 首先我们来看一个简单的示例: public class Hello {
public static void main(String[] args) {
Animal animal;
// 这里应该有一个给animal赋值的语句
// 但是这里我把赋值语句省略了
animal.run();
}
}
class Animal{
void run(){
System.out.println("Animal is running.");
}
} 我在代码注释的地方省略了一个赋值语句,然后现在我希望你在这个地方添加一个赋值语句,让程序正常执行。 你会填写什么呢? 同学们肯定会这样写: public class Hello {
public static void main(String[] args) {
Animal animal;
animal = new Animal();
animal.run();
}
}
class Animal{
void run(){
System.out.println("Animal is running.");
}
} 运行上面的程序会打印出 如果你这样写,没有任何错误,但是现在我更新了一下代码。 public class Hello {
public static void main(String[] args) {
Animal animal;
// insert a statement
animal.run();
}
}
class Animal{
void run(){
System.out.println("Animal is running.");
}
}
class Cat extends Animal{
@Override
void run() {
System.out.println("Cat is running.");
}
} 这种时候,你会怎么填写呢? 如果你填写了 animal = new Cat() 然后再运行程序,结果会不同吗? 或者这样写: public class Hello {
public static void main(String[] args) {
}
static void run(Animal animal){
animal.run();
}
} 在编译阶段,我们能够确定 所以我们可以非常明确的发现:在编译阶段,当我们调用一个对象的public、protected、default方法时,我们如法确认在运行时期到底是执行了那个方法。因为这个方法有可能被子类覆盖了。 而对象的私有方法,super方法,只能在类的成员函数内部调用,不会出现被子类覆盖的情况。 我们的jvm区分了这两种情况。那些在编译阶段就可以确认的方法会被 示例应为对象的私有方法,调用的super方法以及init方法在编译阶段可以完全确定,不会有任何疑问。 public class Hello {
public static void main(String[] args) {
Animal animal;
// 给animal赋值
// 但是这里我把赋值语句省略了
animal.run();
}
}
class Animal{
void run(){
System.out.println("Animal is running.");
}
} 中间我们省略了一个复制语句,现在运行你添加一下代码,请问 当我执行animal.run的时候,控制台一定会答应这句话吗? 请你思考几分钟。 很多同学可能会这样想 public class Hello {
public static void main(String[] args) {
Animal animal;
animal = new Animal();
animal.run();
}
}
class Animal{
void run(){
System.out.println("Animal is running.");
}
} 然后打印出这句话。如果你这样想,没有任何错误,但是你可能胡萝了下面的这个情况。 public class Hello {
public static void main(String[] args) {
Animal animal;
animal = new Cat();
animal.run();
}
}
class Animal{
void run(){
System.out.println("Animal is running.");
}
}
class Cat extends Animal{
@Override
void run() {
System.out.println("Cat is running.");
}
} 发现了没有,虽然我们的animal是Animal类型,但是他的实例其实是一个Cat,这就导致了,在运行阶段,我们的animal 可能是一个animal,也可能是一个cat,我们没办法确定。 而对象的私有方法,super方法,只能在类的成员函数内部调用,不会出现被子类覆盖的情况,而init方法是new时候调用的, 我们的jvm区分了这两种情况,应为invokespecial在编译阶段就可以确定,所以它的性能比invokevirtual更加的高。 但是接下来我们在看一下这样的指令 public final class Hello {
public static void main(String[] args) {
}
static void run(Runnable runnable){
runnable.run();
}
}
interface Runnable{
void run();
}
class Animal implements Runnable{
@Override
public void run(){
System.out.println("Animal is running.");
}
}
哎,好像多可以了一个指令。 invokeinterface 用于调用接口方法,在运行时再确定一个实现此接口的对象。 那它跟 invokevirtual 有什么区别呢?为什么不用 invokevirtual 来实现接口方法的调用?其实也不是不可以,只是为了效率上的考量。 invokestatic 指令需要调用的方法只属于某个特定的类,在编译期唯一确定,不会运行时动态变化,是最快的 invokespecial 指令可能调用的方法也在编译期确定,且只有少数几个需要处理的方法,查找也非常快 invokevirtual 和 invokeinterface 的关系就比较微妙了,区别没有那么明显,我们用一个实际的例子来说明,可以这么认为,每个类文件都关联着一个「虚方法表」(virtual method table),这个表中包含了父类的方法和自己扩展的方法。比如 为了便于理解,我换了一个示例。 class A {
public void method1() { }
public void method2() { }
public void method3() { }
}
class B extends A {
public void method2() { } // overridden from BaseClass
public void method4() { }
} 对应的虚方法表: 现在 B 类的虚方法表保留了父类 A 中方法的顺序,只是覆盖了 method2() 指向的函数链接和新增了method4()。 假设这时需要调用 method2 方法,不管是A的method2还是B的method2,我们不关心,我们的invokevirtual 只需要直接去找虚方法表位置为 2 的地方的函数引用就可以了。不管这个对象是A的实例还是B的实例,它的method2方法都在虚方法表中索引为2的地方。 如果是用如果我们是接口的话,情况就不一样了。 interface X {
void methodX()
}
class B extends A implements X {
public void method2() { } // overridden from BaseClass
public void method4() { }
public void methodX() { }
}
Class C implements X {
public void methodC() { }
public void methodX() { }
} 可以看到,B和C都实现了X接口,但是二者有不同的方法,它们的虚方法表在内存排不上没有任何规律。当我们调用X的methodX方法时,我们没办法优化他,只能遍历一遍虚方法表来。 invokeinterface 不得不搜索整个虚方法表来找到对应方法,效率上远不如 invokevirtual。这就是为什么jvm需要两个不用的指令,主要是考虑性能优化的问题。 |
接下里的指令涉及到jvm的类加载机制,如果你对类加载机制不熟悉,请务必阅读我之前的文章
#3
一个类会有 cinit 方法,和init方法,类初始化的时候会执行cinit方法,对象初始化的时候会执行init方法。
上面的类编译后的字节码是这样的:
这次我们来看看一个对象的构造方法里面的指令究竟是什么。
这里有几个新的指令,我来介绍下
aload_0将本地变量表中第一个元素添加对操作栈中,和iload_0不同,这个操作针对的是jVM的reference类型,而不是int类型。
invokespectal 用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法, 我们我们还会
invokestatic:用于调用静态方法
invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法
invokevirtual:用于调用非私有实例方法
invokeinterface:用于调用接口方法
new: 创建一个对象的实例
dup: 复制栈顶的值,并且将复制的值压入栈中。
一开始创建对象,此时他所有的地段的值都是为空。
就是想将这对象压入栈中,然后调用它父类的初始化方法,然后把100入栈,赋值给它的age字段,然后就结束了。过程其实很简单。
然后我们看下我们创建一个对象时候的
就是创建 =》 初始化方法 》 保存下
为什么创建一个对象需要三条指令呢? 首先,我们需要清楚类的构造器函数是以函数名出现的,被称为实例的初始化方法。调用 new 指令时,只是创建了一个类的实例,但是还没有调用构造器函数,使用 invokespecial 调用了 后才真正调用了构造器函数,正是因为需要调用这个函数才导致中间必须要有一个 dup 指令,不然调用完函数以后,操作数栈为空,就再也找不回刚刚创建的对象了。
The text was updated successfully, but these errors were encountered: