5.1 运行时栈帧结构
栈帧(Stack Frame)用于存储方法的信息。方法有关的才说栈。帧就是一块装栈的帧。栈帧用于存储方法的局部变量栈,操作数栈,动态连接,方法返回地址等。虚拟机栈里就都是这种栈帧,栈中有帧,帧中有栈。线程中充满了方法栈,方法调用方法,一个线程就会有一连串的栈帧。
1.局部变量表:方法中存有很多变量。在Java文件编译成Class文件时,虚拟机就为该方法需要的局部变量表的最大容量进行了确定(代码是静态的,运行是动态的,最大容量一开始就定下来,爆了怎么办?)。这个容量用很久以前说过的slot来规定。一个slot可以存放一个标准类型的变量。32位变量1slot,64位变量2slot,连续且不可分割。
局部变量表第0个变量是this,当然了,不能使static的方法,因为static没有this。第一个开始才是正经变量。
* 不使用的大对象应该手动处理:
public void hello(){
{
byte[] holder = new byte[64*1024*1024];
}
System.gc();
}
public void hello(){
{
byte[] holder = new byte[64*1024*1024];
}
int i = 0;
// could be: holder = null;
System.gc();
}
第一段的gc无效,第二段虽然加了没啥用的一句话,但就gc了。第一段代码框内部分执行完,虽然走出了holder的作用范围,但没有对局部变量表作任何事情,所以holder占的slot还在。所以gc不能进行。随便加一句话,走出范围并改动了局部变量表,slot可以被清除重用。所以大对象不用了,一定要处理掉啊,不能攒着。但是直接设为null确实有点暴力了。
类变量的准备阶段会给每个变量赋个初值,但局部变量没有这个方便,所以一定要搞出来一个初始值。
2.操作数栈:
和汇编课学的差不多,后进先出。操作就是方法中赋值啊,计算啊之类的。栈会自己找出一些类型问题,操作error。
3.动态连接:
用于指向运行时常量池中自己属于的方法。
4.方法返回地址:
方法只有两种情况会退出,第一是返回指令,return;第二种是异常。栈帧中要存有这两种方法各自返回的地址。返回结束,当前栈帧就算推出方法栈了。
5.2 方法调用
找方法。由于多态,方法名对不代表方法对。方法调用就是找我到底调用那个方法。Class文件中的常量池中存放的并不是方法,而是方法的符号引用。在类的解析阶段会解析出来一点很明确的方法(eg:静态方法和私有方法)。其余的都要动态找。
1.解析:
类解析阶段可以解析出来的非常明确的方法。静态方法,私有方法,实例构造器,父类方法。这都是实打实的老实方法。
2.分派(dispatch):
分派针对虚拟机解决Java的多态问题。
class A{
static class Human{}
static class Man extends Human{}
static class Women extends Human{}
public static void main(String[] args){
Human man = new Man();
Human women = new Women();
}
}
Human man = new Man(); 在这句话里,Human叫变量的静态类型(static type),也叫外观类型(Apparent Type)。Man才是变量的实际类型(Actual Type)。静态类型编译时可知,实际类型只有运行时可知。可以理解,静态类型你只会定义一次,就是在声明对象时。但实际类型随时可以改,只要家谱里有静态类型就可以。
// 静态类型变化
sayHello((Man) man);
// 实际类型变化
man = new Woman();
到底调用谁,取决于虚拟机中调用方的调用方式。如果吃静态类型,就只看静态类型的方法,如果吃动态类型变量,就只看动态类型的方法。这个调用过程也是自下而上的。
方法的接收者和方法的参数统称为方法的宗量(argument)。在编译阶段,方法解析要考虑调用者的静态类型,以及方法参数,所以说静态为多分派。在运行阶段,方法参数已定,只有方法的调用者会影响方法的选择,所以运行时动态单分配。
public Father father = new Son("first"); // 既要看Father型,又要看方法中的参数
father.getSon(); //调用时看调用者
5.4 动态类型
动态类型语言:在运行期才确定类型,例如javascript, 万物都是var。Java,C++都是在编译时就要确定变量方法的类型,属于静态类型语言。实现原理为变量本身无类型,但变量的值有类型。
例如var str = "abcdefg":单看str,无法确定类型,但后面的值看似String,那就将他划为String类型值。
静态类型编译器可以发现错误,适合大规模程序设计;动态类型简单易写,适用于高效率开发。
JVM为了支持动态类型语言,设计了一些机制。java.lang.invoke。为了实现类似函数指针的功能,java提供了MethodHandle。
static class ClassA{
public void println(String s){
// simulate System.out
System.out.println(s)
}
}
public static void main(String[] args) throws Throwable{
Object obj = System.currentTimeMills() % 2 == 0 ? System.out : new ClassA();
// randomly pick one class
getPrintlnMH(obj).invokeExact("icyfenix");
}
// notice the return type, it's "MethodHandle"
private static MethodHandle getPrintlnMH(Object receiver) throws Throwable{
MethodType mt = MethodType.methodeType(void.class, String.class);
// first param is return type, second is concrete input param
// lookup()->MethodHandles.lookup, looks up some method qualified
return lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver);
// return a concrete method, rather in System.out or in ClassA
// depends on type of receiver
// after all, each type contains a "println"
// All in all, this example shows that a function can be decided dynamically
}
与反射机制(reflection)相比,反射实在模拟Java代码层,而MethodHandle在模拟字节码层。(反射日后细谈)
实际例子:
class Test{
class GrandFather{
void thinking(){
System.out.println("I'm grandfather.");
}
}
class Father extends GrandFather{
void thinking(){
System.out.println("I'm father.");
}
}
class Son extends Father{
void thinking(){
try{
// here, we want to invoke gf's thinking
MethodType mt = MethodType.methodType(void.class);
MethodHandle mh = lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
mh.invoke(this);
// invoke "grandfather's this"
} catch(Throwable e){}
}
}
public static void main(String[] args){
(new Test.new Son()).thinking();
// I'm grandfather.
}
}
5.5 字节码解释执行引擎
由于笔者对JVM接触尚少,对本章节的实际应用不甚了解,所以代码和很多具体原理都是浅尝辄止,只求看懂,暂时不求应用。
解释执行:通过解释器执行;编译执行:通过即时编译器,产生本地代码,再执行。
在物理机执行代码的过程中,要经历词法分析,语法分析,生成抽象语法树。这是编译过程。C/C++将这些过程独立于执行引擎,Java把抽象树之前步骤独立,算是半独立引擎。Javac编译器完成词法分析,语法分析,抽象语法树,再生成线性字节码指令流,而解释器在JVM中,所以是半独立。