深入理解Java虚拟机6~虚拟机字节码执行引擎

时间:2022-12-27 19:40:32

1、运行时栈桢结构

栈桢(为什么我每次都读成桢栈)是用于支持虚拟机进行方法调用和方法执行的数据结构,栈桢存储了方法的

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法返回地址
每一个方法从调用开始至执行完成的结束,都对应着一个栈桢在vm里面的入栈到出栈的过程。 我们看一段代码:
package test_technology;

/**
*
* @author zhengchao
*/
public class testGC {

public static void main(String[] args){

byte[] placeholder = new byte[64*1024*1024];
System.gc();
}
}
为了便于观察gc的结构,我们在虚拟机运行参数中加上:-verbose:gc来看看垃圾收集的过程。
我用的是glassfish,服务器启动后,在http://localhost:4848/common/index.jsf中设置:如下图 深入理解Java虚拟机6~虚拟机字节码执行引擎

我们就可以在控制台看见gc运行后的结果。分析结果,我们发现placeholder没有被回收,因为它还处于作用域之内。

{
byte[] placeholder1 = new byte[512*1024*1024];
}
System.gc();
我们这么写,发现也没有被回收。
{    byte[] placeholder1 =  new byte[512*1024*1024];} int a =0;System.gc();
我们这么写,发现placeholder被回收了!

分析如下:placeholder能否被回收的根本原因是,局部变量表中的slot是否还存有关于placeholder数组对象的引用。

                  第一次修改中,代码虽然已经离开了placeholder的作用域,但在此之后,没有任何对局部变量表的读写

                  操作,placeholder原本所占用的slot还没有被其他变量所复用,所有作为gc  roots一部分的局部变量表仍

                  然保持着对它的关联。这种关联没有被及时打断,一般情况下,这无所谓,但如果遇到一个方法,其后

                 面的代码有一些耗时很长的操作,而前面又定义了占用了大量内存、实际已经不会再用的变量,手动将其

                 设置为null值便会有用处。

操作数栈后入先出的栈,比如iadd指令在运行时,将操作数栈最接近栈顶的两个元素出栈,并相加,再把结果入栈,

动态连接:后续详述。

方法返回地址:方法退出之后,即当前栈桢出栈,需要返回到方法被调用的位置,程序才能继续执行。一般pc计数器的值可以作为返回地址。

2、方法调用

  • 解析:所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。
  • 分派:
 静态分派:重载
package jvm;

/**
*
* @author zhengchao
*/
public class StaticDispatch {

static abstract class Human{}

static class Man extends Human{

}

static class Woman extends Human{

}

public void sayHello(Human guy){
System.out.println("hello,guy!");
}

public void sayHello(Man guy){
System.out.println("hello,gentleman!");
}

public void sayHello(Woman guy){
System.out.println("hello,lady!");
}

public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();

StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
运行结果:
run:
hello,guy!
hello,guy!
成功构建 (总时间: 0 秒)
结果可能有一点出乎你对重载的意料,分析如下:
上面的代码中,Human称为变量的静态类型,而Man则称为变量的实际类型,虚拟机在重载时是通过参数的静态类型而不是实际类型作为判断依据的。 修改下代码:
public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();

StaticDispatch sr = new StaticDispatch();
sr.sayHello((Man)man);
sr.sayHello((Woman)woman);
}
运行结果:
run:
hello,gentleman!
hello,lady!
成功构建 (总时间: 0 秒)
上面这个例子是为了说明静态分派,静态分派的典型应用是方法重载。

动态分派:重写
package jvm;

/**
*
* @author zhengchao
*/
public class DynamicDispatch {

static abstract class Human{
protected abstract void sayHello();
}

static class Man extends Human{
@Override
protected void sayHello() {
System.out.println("man say hello!");
}
}

static class Woman extends Human{
@Override
protected void sayHello() {
System.out.println("woman say hello!");
}
}

public void sayHello(Woman guy){
System.out.println("hello,lady!");
}

public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();

man.sayHello();
woman.sayHello();

man = new Woman();
man.sayHello();
}
}
运行结果:
run:
man say hello!
woman say hello!
woman say hello!
成功构建 (总时间: 0 秒)
这个结果,很多人应该不会有疑问,符合大家一直的思想。重写是根据实际类型来分派方法的执行版本。
至于原理,建议读者阅读jvm书籍关于invokevirtual指令的执行过程。