JVM的記憶體管理
一:JVM記憶體模型
首先看兩個模型圖:
圖1.
圖2.
上面兩個圖都展示了虛擬機器記憶體管理模式,整個執行時資料區又分為不同的記憶體區域,不同區域承擔不同的功能,下面我們來一一分析一下它們的作用:
1.程式計數器(PC Register)
程式計數器是一塊較小的記憶體空間,它的作用可以看做是當前執行緒所執行的位元組碼的行號指示器。由於java虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器只會執行一條執行緒職工的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,並且不能互相被幹擾,我們稱這類記憶體區域為執行緒私有記憶體。
2.java虛擬機器棧(JVM Stack)
java虛擬機器棧也是執行緒私有的,虛擬機器棧描述的是java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於儲存區域性變量表、操作棧、動態連結、方法出口等資訊。區域性變量表存放了編譯期可知的各種基本資料型別、物件引用和returnAddress(指向了一條位元組碼指令的地址)。
3.本地方法棧(Native Method Stack)
本地方法棧與虛擬機器棧所發揮的作用非常相似,其區別不過是虛擬機器棧為虛擬機器指向java方法服務,而本地方法棧則是為虛擬機器使用到的Native方法服務。
4.java堆(Heap)
對於大多數應用來說,java堆(Heap)是java虛擬機器所管理的記憶體中最大的一塊。java堆被所有執行緒共享,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項和陣列,是垃圾收集管理器的主要區域。根據java虛擬機器規範的規定,java堆可以處於物理上不連續的記憶體空間中,只要邏輯上是連續的即可。
5.方法區(Method Area)
方法區也稱“永久代”,它用於儲存虛擬機器載入的類資訊、常量、靜態變數,是各個執行緒共享的記憶體區域。
執行時常量池(Runtime Constant Pool):是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後放到方法區的執行時常量池中。
從JDK7開始移除永久代(但並沒有移除,還是存在),儲存在永久代的一部分資料已經轉移到了Java Heap或者是Native Heap。符號引用轉移到了native heap;字面量轉移到了java heap;類的靜態變數轉移到了java heap。
6.直接記憶體
直接記憶體並不是虛擬機器執行時資料區的一部分,也不是java虛擬機器規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用。
二:例項分析
邏輯記憶體模型我們已經看到了,那當我們建立一個物件的時候是怎麼進行訪問的呢?在java語言中,物件訪問是如何進行的?物件訪問在Java語言中無處不在,是最普通的程式行為,這都會涉及到java棧、java堆、方法區這個三個重要的記憶體區域之間的關聯關係,如下面程式碼:
public class JVMMemoryTest {
/**
* jvm自動尋找main方法,執行main方法,此時虛擬機器棧中有一個代表main方法的棧幀入棧,執行完畢後出棧
* @param args
*/
public static void main(String[] args) {
/**
* 1.student是物件的引用,所有會儲存在棧幀的區域性變數裡
* 2.建立Student的時候,首先進行類的載入工作,類只會載入一次,將類的型別資訊資料載入到jvm的方法區中,如果之前載入過了,那麼就不會再載入。
* 3.類的載入其實就是將.class檔案載入進虛擬機器記憶體中,在載入的時候,在java堆中生成對應的Class物件,
* 最後生成一個Student物件在堆中
*/
Student student=new Student(18,"tom","007");
/**
* 宣告定義一個int型別的變數a,因為a是基本資料型別,所以在棧中直接分配一個記憶體儲存這個變數
*/
int a=9;
int b=10;
/**
* 執行study方法,在棧中加入一個棧幀,執行完畢後這個棧幀將出棧
* 在study方法中,有兩個int型別的區域性變數,是儲存在棧幀的區域性變數記憶體區中的
*/
student.study(a,b);
}
//靜態內部類
public static class Student extends Person implements IStudyable {
private static int cnt = 5;
static {
cnt++;
}
private String sid;
public Student(int age, String name, String sid) {
super(age, name);
this.sid = sid;
}
public void run() {
System.out.println("run()...");
}
public int study(int a, int b) {
int c = 10;
int d = 20;
return a + b * c - d;
}
public static int getCnt() {
return cnt;
}
}
//父類
static class Person {
private String name;
private int age;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public void run() {
}
}
//介面
interface IStudyable {
public int study(int a, int b);
}
}
上面例子展示了jvm是如何分配記憶體的,還有需要說明的就是new Student的時候,在java堆中形成一塊儲存了Student型別的所有例項數值(instance Data,物件中的各個例項欄位資料)的結構化記憶體,這塊記憶體的長度是不固定的。另外,在java堆中還必須包含能查詢到此物件型別資料(如物件型別、父類、實現的介面、方法等)的地址資訊,這些型別資料是儲存在方法區中的。
不同的虛擬機器實現物件的訪問方式有所不同,主流的方式有兩種:使用控制代碼和直接指標。
如果使用控制代碼訪問方式,java堆中將會劃出一塊記憶體來作為控制代碼池,reference中儲存的是物件的控制代碼地址,而控制代碼中包含了物件例項資料和型別資料各自的具體地址資訊,如圖所示:
如果使用指標直接訪問的方式,java對物件的佈局中就必須放置有訪問型別資料的指標,而reference中直接儲存的是物件的地址,如圖所示:
這兩種訪問方式各有優勢,其中使用直接指標訪問方式最大的好處就是訪問速度快,可節省程式的執行成本。