1. 程式人生 > 其它 >執行時資料區域

執行時資料區域

一:概述

瞭解Java虛擬機器的記憶體的各個區域以及各個區域的作用,在Java程式出現記憶體洩漏和溢位等問題的時候,能夠更快速的排查錯誤、修正等問題。
記憶體模型

二:記憶體區域

2.1 程式計數器

概念:程式計數器可以看作是當前執行緒所執行的位元組碼的行號指示器。
作用:
 1.存放著下一條要執行位元組碼指令的地址,位元組碼解釋工作時就是通過改變計數器的值來選取下一條需要執行的位元組碼指令。
 2.在Java虛擬機器中,多執行緒是通過執行緒輪流切換、分配處理器執行時間的方式實現的,當從一個執行緒切換到另一個執行緒執行的時候,為了執行緒切換後能夠恢復到執行時候的位置,程式計數器中記錄的就是當前指令的地址,每條執行緒都需要一個獨立的程式計數器。這類記憶體區域成為執行緒私有

的。
 3.程式的分支、跳轉、迴圈、異常處理,執行緒恢復等基礎功能都依賴程式計數器。
運用:
 1.如果執行緒執行的是一個Java方法,程式計數器記錄的是正在執行的虛擬機器位元組碼指令的地址。
 2.如果執行緒執行的是一個Native(本地)方法,程式計數器值為空(Undefined)。
程式計數器是唯一一個在《Java虛擬機器規範》中沒有規定OutOfMemoryError的記憶體區域。

2.2 虛擬機器棧

概述
每個執行緒所需要的記憶體,稱為虛擬機器棧,每個棧可以由儲存多個棧幀,一個棧幀對應著一次方法呼叫時所佔用的記憶體。虛擬機器棧是執行緒私有的。
棧幀
 每個Java方法執行的時候,Java虛擬機器都會同步建立一個棧幀用於儲存區域性變量表、運算元棧。方法出口等資訊。一個方法從被呼叫到執行完畢,對應的就是一個棧幀在虛擬機器棧入棧出棧的過程。每個執行緒只能有一個活動棧幀,對應著正在執行的方法。
棧容量的預設大小為1M,可以通過引數指定容量大小`

-Xss256k  #設定虛擬機器棧記憶體大小為256k

問題:
1.垃圾回收是否涉及棧記憶體
 由於棧在方法執行完畢後,會自動將棧幀彈出棧,所以不會涉及到垃圾回收。
2.棧記憶體分配越大越好嗎
 實體記憶體的大小是固定的假設為1000M,如果設定棧記憶體為1M,則能夠併發的執行1000個執行緒,如果將棧記憶體設定為2M,則只能夠併發的執行500個執行緒,所以並不是棧記憶體分配越大越好。
3.方法內的區域性變數是否執行緒安全
 1.如果方法內部的區域性變數沒有逃離方法(將靜態變數作為返回值)的作用範圍,它是執行緒安全的。
 2.如果區域性變數引用了物件,並逃離方法的作用範圍,則需要考慮執行緒安全。
棧記憶體溢位(StackOverflowError)


 1.定義了大量的本地遍歷,增大了棧幀的大小超出了棧的容量。
 2.棧幀過多導致棧記憶體溢位,例如遞迴時沒有設定方法出口。

/*
	VM Args:-Xss256k
*/
public class StackOver {
    private int i = 1;
    public static void main(String[] args) {
        StackOver s = new StackOver();
        try{
            s.stackLeak();
        }catch (Throwable e){
            System.out.println("stack Lnegth:" + s.i);
            throw e;
        }
    }
    public void stackLeak(){
        i++;
        stackLeak();
    }
}

記憶體溢位(OutOfMemoryError)
 棧記憶體允許動態擴充套件,當棧容量無法申請到足夠多的記憶體時,丟擲OutOfMemoryError異常。由於HotSpot虛擬機器不支援擴充套件,所以除非在建立執行緒申請記憶體時就因為無法獲得足夠記憶體而出現OutOfMemoryError異常,否則執行緒執行時只會因棧容量不夠報StackOverflowError異常。
定位異常
 在linux下執行的Java程式,快讀定位異常程式碼

top  #檢測後臺程序對CPU的使用情況
ps H -eo pid,tid,%cpu |grep 程序id  #(定位是由哪個執行緒引起的cpu佔用過高)
jstack 程序id  #根據執行緒id找到有問題的程序,進一步定位到問題程式碼的原始碼行號

2.3 本地方法棧

 本地方法棧是為虛擬機器使用到的本地(Native)方法服務,在虛擬機器呼叫本地方法時需要給本地方法分配的記憶體空間,《Java虛擬機器規範》中並沒有強制規定本地方法中方法使用的語言、使用方式、資料結構。一般都是由c/c++編寫,可以直接跟作業系統更底層的API打交道,而Java可以通過呼叫本地方法間接的呼叫到更底層的功能。
本地方法棧也會在棧深度溢位或者棧擴充套件失敗是丟擲StackOverflowError和OutOfMemoryError異常。

2.4 堆

概述
 堆是Java虛擬機器管理的記憶體中最大的一塊,是執行緒共享的,在虛擬機器啟動的時候建立。此記憶體區域唯一的目的就是存放物件例項,堆中的物件都需要考慮執行緒安全的問題。堆是垃圾收集器管理的記憶體區域。
通過設定引數 -Xmx 和 -Xms擴充套件堆的大小

-Xms20m #堆的最小值   -Xmx20m #堆的最大值

堆溢位(heap)
 Java堆用於儲存物件例項,我們不斷建立物件並保證GC Roots到物件之間有可達路徑來避免垃圾回收時清除這些物件,當物件總容量超過堆的最大容量之後丟擲記憶體溢位異常

/*
*       堆記憶體溢位
*       虛擬機器引數:-Xmx8m
*/
public class Heap_01 {
    public static void main(String[] args) {
        int i = 0;
        try{
            List<String> list = new ArrayList<>();
            String a = "hello";
            while(true){
                list.add(a);
                a = a + a;
                i++;
            }
        }catch (Throwable e){
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

堆記憶體溢位
在控制檯執行以下指令進行堆記憶體診斷

jps  #檢視當前系統中有哪些Java程序
jmap -heap 程序id   #檢視堆記憶體佔用情況
jconsole   #圖形化介面,多功能的監測工具,可以連續監測
jvisualvm   #另一個圖形化多功能監測工具

選擇需要連線的程序

堆記憶體診斷
首先確定記憶體中導致OOM的物件是否是必要的,分清楚是記憶體洩漏(Memory Leak)還是記憶體溢位(Memory Overflow)。如果是記憶體洩漏,通過分析工具插看洩漏物件到GC Roots是怎麼的引用路徑、與哪些GC Roots相關聯,才導致垃圾收集器無法回收它們,根據洩漏物件的型別資訊以及它到GC Roots引用鏈的資訊,一般可以比較準確的定位到這些物件建立的位置,找出產生記憶體洩漏的程式碼的具體位置。如果不是記憶體洩漏,說明記憶體中的物件都是必須存貨的,那麼應當檢查Java虛擬機器堆引數(-Xms和-Xmx)設定,與機器記憶體對比看是否還能向上調整,再從程式碼上檢查是否存在某些物件生命週期過長、持有狀態時間過長、儲存結構設計不合理等情況。

2.5 方法區

概述
 方法區是執行緒共享的,用於儲存類相關的資訊,例如常量、靜態變數、構造器、成員方法、即時編譯器編譯後的程式碼快取等資料。在JDK8之前,方法區被稱為永久代,在JDK7的時候,HotSpot將原來存放在永久代的字串常量池、靜態變數等移到了Java堆中,在JDK8之後,完全廢棄了永久代的概念,改用了在本地記憶體中實現的元空間來代替。
對方法區進行記憶體回收目標主要是針對常量池的回收和對型別的解除安裝。
記憶體結構


執行時常量池
 執行時常量池是方法區的一部分,Class檔案除了有類的版本、欄位、方法、介面等描述資訊外,還有常量池表,用於存放編譯期生成的各種字面量與符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。對於執行時常量池,《Java虛擬機器規範》並沒有做任何要求,不同提供商實現的虛擬機器可以按照自己的需要來實現這個記憶體區域。

public class StringTable02 {
    public static void main(String[] args) {
        //常量池中的資訊,都會被載入到執行時常量池中,這時a b ab 都是常量池中的符號還沒有變為 java 字串物件

        //常量池中的字串僅僅是符號,第一次用到時才變為物件
        //ldc  #7   會把 a 符號變為 "a" 字串物件
        //ldc  #9   會把 b 符號變為 "b" 字串物件
        //ldc  #11  會把 ab 符號變為 "ab" 字串物件
        //StringTable["a","b","ab"]
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";

        //字串拼接原理
        //new StringBuild().append("a").append("b").toString()
        //toString()建立了一個新的字串物件 new String("ab");
        String s4 = s1 + s2;

        // ldc  #11   在串池中找到 ab 符號,串池中已經找到,則不會再建立
        String s5 = "a" + "b";

        //堆 new String("a")  new String("b")  new String("ab")
        String s6 = new String("a") + new String("b");
        s5.intern();  //將字串嘗試放入串池,如果串池有則不會加入
    }
}

簡單來說,常量池,就是一張再*.class檔案中的表,虛擬機器指令根據這張常量表找到要執行的類名、方法名、引數型別、字面量等資訊。執行時常量池,當該類被載入的時候,它的常量池資訊就會被放入到執行時常量池,並把裡面的符號地址變為真實地址。
元空間的一些引數

#設定原空間的最大值,預設是-1,即不限制,只受限本地記憶體大小
-XX:MaxMetaspaceSize: 

#指定元空間的初始空間大小,以位元組為單位,達到該值就觸發垃圾收集進行型別解除安裝
-XX:MetaspaceSize: 

#垃圾收集之後控制最小的元空間剩餘容量的百分比,可減少因為元空間不足導致的垃圾收集的頻率
-XX:MinMetaspaceFreeRatio: