深入理解java虛擬機器——讀書筆記1

weixin_33807284發表於2019-02-22

JVM:指以軟體的方式模擬具有完整硬體系統功能,執行在一個完全隔離環境中的完整計算機系統。JVM主要包含3個子系統:類載入子系統,執行時資料區,執行引擎。

本篇筆記主要關於執行時資料區。

1. java記憶體區域與執行記憶體異常

1.1 執行時資料區域

16366530-9892930fcea4ce33.png
轉自https://blog.csdn.net/qq_41701956/article/details/81664921

1.1.1 程式計數器

程式計數器(Program Counter Register)是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器,用於指示下一條需要執行的位元組碼指令的行號。它是執行緒私有的,即每條執行緒都有一個獨立的程式技術器,各條執行緒之間的計數器互不影響。
如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Native方法,這個計數器值則為空(Undefined)。此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

1.1.2 Java虛擬機器棧

JVM為每個執行緒單獨分配一個Java虛擬機器棧,即執行緒私有。Java虛擬機器棧為每個方法劃分棧幀(Stack Frame)用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫至完成的過程,就對應一個棧幀在虛擬機器棧中入棧到出棧的過程。
區域性變數表存放了編譯期可知的各種基本資料型別、物件引用和returnAddress型別。其中64位長度的long和double型別的資料會佔用2個區域性變數空間(Slot),其餘的資料型別只佔用1個。對於資料空間的分配是在編譯期完成,在方法執行期間不會改變區域性變數表的大小。
在Java虛擬機器規範中,對這個區域規定了兩種異常狀況:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常:如果虛擬機器棧可以動態擴充套件,且擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。

1.1.3 本地方法棧

區別於 Java 虛擬機器棧的是,Java 虛擬機器棧為虛擬機器執行 Java 方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的 Native 方法服務。也會有 StackOverflowError 和 OutOfMemoryError 異常。

1.1.4 方法區

方法區(Method Area)是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。方法區並不是一個實際存在的區,它更像是一個概念,用來區別於堆。如果硬給方法區劃分出一個記憶體範圍,它類似與堆中的“永久代”,也有叫元空間(MetaData)。


16366530-83a2cb5234c3857a.png
圖片.png

1.1.5 堆

對於大多數應用來說,Java堆(Java Heap)是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC 堆”(GarbageCollected Heap)。Java堆中可以細分為新生帶和老年帶:再細緻一點分為Eden空間、From Survivor空間、To Survivor空間。

1.1.6 執行時常量池

執行時常量池(Runtime Constant Pool)是方法區的一部分。執行時常量池相對與Class檔案常量池的一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,執行期間也可能將新的常量放入池中。當常量池無法再申請到記憶體時會丟擲OutOfMemoryError異常。

1.2 HotSpot虛擬機器物件探祕

1.2.1 物件的建立

虛擬機器執行一條new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、解析和初始化過。如果沒有,那必須先執行相應的類載入過程。在類載入檢查通過後,虛擬機器將為新生物件分配記憶體,物件所需記憶體的大小在類載入完成後便可完全確定。然後,將分配到的記憶體空間都初始化為零值。接下來,虛擬機器對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到類的後設資料資訊、物件的雜湊碼、物件的GC分代年齡等資訊。最後執行init方法,把物件按程式設計師的醫院進行初始化。

1.2.2 物件訪問的定位

目前的訪問方式有使用控制程式碼和直接指標兩種。
1.如果使用控制程式碼訪問的話,那麼Java堆中將會劃分出一塊記憶體來作為控制程式碼池,fegerence中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與型別資料各自的具體地址資訊。
2.如果使用直接指標訪問,那麼Java堆物件中的佈局就必須考慮如何放置訪問型別資料的相關資訊,而reference中儲存的直接就是物件地址。


面試題

1. 記憶體溢位的原因是什麼?

記憶體溢位是由於沒被引用的物件(垃圾)過多造成JVM沒有及時回收,造成的記憶體溢位。如果出現這種現象可行程式碼排查:

  • 程式中的類中和引用變數過多使用了Static修飾 如public staitc Student s;在類中的屬性中使用 static修飾的最好只用基本型別或字串。如public static int i = 0; //public static String str;

  • 程式中使用了大量的遞迴或無限遞迴(遞迴中用到了大量的建新的物件)

  • 程式中使用了大量迴圈或死迴圈(迴圈中用到了大量的新建的物件)

  • 程式是否使用了向資料庫查詢所有記錄的方法。即一次性全部查詢的方法,如果資料量超過10萬多條了,就可能會造成記憶體溢位。所以在查詢時應採用“分頁查詢”。

  • 檢查是否有陣列,List,Map中存放的是物件的引用而不是物件,因為這些引用會讓對應的物件不能被釋放。會大量儲存在記憶體中。

  • 檢查是否使用了“非字面量字串進行+”的操作。因為String類的內容是不可變的,每次執行"+"就會產生新的物件,如果過多會造成新String物件過多,從而導致JVM沒有及時回收而出現記憶體溢位。


String s2 = "is";

String s3 = "xuwei";

String str = s1 + s2 + s3 +.........;

這是會容易造成記憶體溢位的。
但是String str = "My name" + " is " + " xuwei" + " nice " + " to " + " meet you";這種就不會造成記憶體溢位。因為這是”字面量字串“,在執行"+"時就會在編譯期間執行好。不會按照JVM來執行的。

在使用String,StringBuffer,StringBuilder時,如果是字面量字串進行"+"時,應選用String效能更好;如果是String類進行"+"時,在不考慮執行緒安全時,應選用StringBuilder效能更好。

public class Test {  
  
    public void testHeap(){  
        for(;;){  //死迴圈一直建立物件,堆溢位
              ArrayList list = new ArrayList (2000);  
          }  
    }  
    int num=1;  
    public void testStack(){  //無出口的遞迴呼叫,棧溢位
        num++;  
        this.testStack();  
     }  
    public static void main(String[] args){  
        Test  t  = new Test ();  
        t.testHeap();  
        t.testStack();     
    }  
}  

七)使用 DDMS工具進行查詢記憶體溢位的大概位置

2、棧溢位的原因

1). 是否有遞迴呼叫
2). 是否有大量迴圈或死迴圈
3). 全域性變數是否過多
4). 陣列、List、map資料是否過大
5). 使用DDMS工具進行查詢大概出現棧溢位的位置

相關文章