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中,所以是半獨立。