先随便写一个非常简单的类Test
public class Test{
private int i;
public void testMethod(){
i=i+1;
}
}
然后被下面这个main方法生成并调用,期间我们通过期间发生的内容,来简单了解一下JVM的内部机制。
public static void main(String[] args) {
Test test = new Test();
test.testMethod();
}
首先是编译:
Test类被编译成一个叫做Test.class的类文件,它的内容以字节码的形式存在。即Ox0F这样的形式存在。
这个类文件中依次包含了魔数、常量池、类索引、父类索引、接口索引集合、字段表集合、方法表集合、属性表集合。
这里我们来回答几个问题:
1、对一个方法进行调用,入口地址在哪:常量池中的方法常量,在编译期间记录的是符号引用,里面的nameAndType唯一确定了这个方法对应类中的哪一个,特征为参数和方法名,不指向代码,在编译期间,虚拟机通过这种方式已经解决了方法重载的问题,关于如何实现覆盖(override)是运行时实现的。
2、一个方法的属性、参数类型、返回值类型、名称等在哪:方法表集合中记录了所有属于这个类的方法(不包括继承来的),常量池中的方法常量中也有一个NameAndType的引用。
3、一个方法的Code在哪:属性表集合中有一个叫做Code属性的字段,它的内容是Code表集合,里面有属于这个类的各个方法Code(不包括继承来的方法)。
所以一个方法在一个类中的存在,被三个地方记录,一个是入口,一个是属性,一个是code。
在完成编译之后,几样东西已经确定:即main方法的code中,调用方法的指令指向了Test类的常量池中的testMethod方法,已经确定了调用的符号引用。
然后是类加载期间:
加载过程被分为了5个阶段,加载、校验、准备、解析、初始化。
其中,加载通过classLoader把类加载进了内存中的方法区。方法区包括了类信息和运行时常量区两个部分,其中class的常量池被加载入运行时常量区,其他信息加载入类信息处。到这里,我们跟踪一下:
1、调用入口在运行时常量池,内容还是符号引用,。
2、方法描述在方法区的类信息区。
3、code在方法区的属性表集合的code属性的code属性表中。
此时,与上一步没有本质的变化。
然后是校验、准备、解析。每一步的功能可以查阅其他资料。
这里解析这一步比较关键,它将常量池中的符号引用替换为了真实的内存引用。但是注意了,这里并没有把所有的方法的符号引用都替换掉。这里被替换掉的符号引用有:
静态方法、私有方法、实例构造器方法、父类方法四类。他们有个共同特点:唯一确定,没有override。
所以这里的testMethod没有被解析,还是一个符号引用。
而如果有构造方法,那么在这里已经被解析了。
这样做实现了重载。因为方法到这一步的时候,并不知道他的实际调用对象是谁,只知道他的静态类型是Test,可能是Test,也可能是test的子类,如果是test的子类,事实上可能指向子类重载的方法,也可能还是指向父类的方法(子类没有重载)。这就涉及到运行是的动态分派了。
最后初始化,这时没有发生太大的根方法有关的变化。
针对testMethod,继续跟踪一下,发现没有变化。
但如果假设他是静态方法,那么会是这样:
1、入口在运行是常量池,内容是直接引用,指向code所在的内存(这里可能不准确,实际可能还要带上方法信息)。
2、其他内容位置不变。
最后是运行。
当运行到方法被调用的时候,这个时候就需要获得这个类调用对象的信息了:
分析这里的test,得到他的类型为Test。同时根据调用信息,得到符号引用。
根据符号引用在Test的方法表集合中搜索符合符号引用的方法,如果找到了,则将调用地址指向Test中的testMethod的code。否则依次在父类中查找,找到则指向那个方法。如果查找失败,则抛异常。通过这个方式实现了多态性。
随后根据动态指向的code中的信息,生成栈帧。这里需要根据code得到max_locals、max_stacks数据,这些数据是在编译时计算好放在code属性中的。
到这里,就完成了一次方法的调用。由于涉及到jvm的知识点较多,阅读前需要先做了解,如有错误,请指正。