1. 程式人生 > >從JVM記憶體管理的角度談談JAVA類的靜態方法和靜態屬性

從JVM記憶體管理的角度談談JAVA類的靜態方法和靜態屬性

JVM的記憶體分為兩部分:

stack(棧)是JVM的記憶體指令區。stack管理很簡單,push一定長度位元組的資料或者指令,stack指標壓棧相應的位元組位移;
pop一定位元組長度資料或者指令,stack指標彈棧。stack的速度很快,管理很簡單,並且每次操作的資料或者指令位元組長度是已知的。
所以Java 基本資料型別,Java 指令程式碼,常量都儲存在stack中。

heap (堆)是JVM的記憶體資料區。heap 的管理很複雜,每次分配不定長的記憶體空間,專門用來儲存物件的例項。
在heap 中分配一定的記憶體來儲存物件例項,實際上也只是儲存物件例項的屬性值,屬性的型別和物件本身的型別標記等,
並不儲存物件的方法(方法是指令,儲存在stack中),在heap 中分配一定的記憶體儲存物件例項和物件的序列化比較類似。
而物件例項在heap 中分配好以後,需要在stack中儲存一個4位元組的heap 記憶體地址,用來定位該物件例項在heap 中的位置,便於找到該物件例項。

由於stack的記憶體管理是順序分配的,而且定長,不存在記憶體回收問題;而heap則是隨機分配記憶體,不定長度,存在記憶體分配和回收的問題;
因此在JVM中另有一個GC程序,定期掃描heap ,它根據stack中儲存的4位元組物件地址掃描heap ,
定位heap中這些物件,進行一些優化(例如合併空閒記憶體塊什麼的),並且假設heap中沒有掃描到的區域都是空閒的,
統統refresh(實際上是把stack中丟失了物件地址的無用物件清除了),這就是垃圾收集的過程。

我們首先要搞清楚的是什麼是資料,什麼是指令?然後要搞清楚物件的方法和物件的屬性分別儲存在哪裡?

為了便於描述,我簡單的統稱:
1)方法本身是指令的操作碼部分,儲存在stack中;
2)方法內部變數作為指令的運算元部分,跟在指令的操作碼之後,儲存在stack(簡單型別儲存在stack中,物件型別在stack中儲存地址,在heap中儲存值);
上述的指令操作碼和指令運算元構成了完整的Java指令。
3)物件例項包括其屬性值作為資料,儲存在資料區heap 中。
非靜態的物件屬性作為物件例項的一部分儲存在heap 中,而物件例項必須通過stack中儲存的地址指標才能訪問到。
因此能否訪問到物件例項以及它的非靜態屬性值完全取決於能否獲得物件例項在stack中的地址指標。

先分析一下非靜態方法和靜態方法的區別:
非靜態方法有一個和靜態方法很重大的不同:
非靜態方法有一個隱含的傳入引數,該引數是JVM給它的,和我們怎麼寫程式碼無關,
這個隱含的引數就是物件例項在stack中的地址指標。因此非靜態方法(在stack中的指令程式碼)總是可以找到自己的專用資料(在heap 中的物件屬性值)。
當然非靜態方法也必須獲得該隱含引數,因此非靜態方法在呼叫前,必須先new一個物件例項,獲得stack中的地址指標,否則JVM將無法將隱含引數傳給非靜態方法。


而靜態方法無此隱含引數,因此也不需要new物件,只要class檔案被ClassLoader load進入JVM的stack,該靜態方法即可被呼叫。
當然此時靜態方法是存取不到heap 中的物件屬性的。

總結一下該過程:當一個class檔案被ClassLoader load進入JVM後,方法指令儲存在stack中,此時heap區沒有資料。
然後程式技術器開始執行指令,如果是靜態方法,直接依次執行指令程式碼,當然此時指令程式碼是不能訪問heap 資料區的;
如果是非靜態方法,由於隱含引數沒有值,會報錯。因此在非靜態方法執行前,要先new物件,在heap中分配資料,
並把stack中的地址指標交給非靜態方法,這樣程式技術器依次執行指令,而指令程式碼此時能夠訪問到heap資料區了。

再說一下靜態屬性和動態屬性:
前面提到物件例項以及動態屬性都是儲存在heap 中的,而heap必須通過stack中的地址指標才能夠被指令(類的方法)訪問到。
因此可以推斷出:靜態屬性是儲存在stack中的(基本型別儲存在stack中,物件型別地址儲存在stack,值儲存在heap 中),
而不同於動態屬性儲存在heap 中。正因為都是在stack中,而stack中指令和資料都是定長的,因此很容易算出偏移量,
也因此不管什麼指令(類的方法),都可以訪問到類的靜態屬性。也正因為靜態屬性被儲存在stack中,所以具有了全域性屬性。

總結一下:靜態屬性儲存在stack指令記憶體區,動態屬性儲存在heap 資料記憶體區。