JVM區域劃分

陳俊成發表於2016-10-12

Java虛擬機器所管理的記憶體將會包括以下幾個執行時資料區域:

  • 程式計數器(執行緒隔離)
  • 虛擬機器棧(執行緒隔離)
  • 本地方法棧(執行緒隔離)
  • JAVA堆(執行緒共享)
  • 方法區(執行緒共享)

(1)程式計數器
程式計數器是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器。

  • 如果執行緒正在執行的是java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;
  • 如果正在執行的是Native方法,這個計數器值為空;
  • 此記憶體區域是唯一一個在JAVA虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域

(2)虛擬機器棧
虛擬機器棧的生命週期與執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型;每個方法在執行的同時都會建立一個棧幀用於儲存局:

  • 區域性變數表(存放編譯器可知的各種基本資料型別,物件引用型別)
  • 運算元棧
  • 動態連結
  • 方法出口 等

對於虛擬機器棧這個區域規定了兩種異常情況:

  • StackOverflowError:執行緒請求的棧深度大於虛擬機器所允許的深度
  • OutOfMemoryError:虛擬機器棧在擴充時無法申請到足夠的記憶體空間

(3)本地方法棧(與虛擬機器棧所發揮的作用相似)

  • 虛擬機器棧:執行JAVA方法(位元組碼)服務
  • 本地方法棧:執行為虛擬機器使用到的Native方法服務
  • 本地方法棧也會丟擲StackOverflowErrorOutOfMemoryError異常

(4)JAVA堆

JAVA堆是JAVA虛擬機器中記憶體最大的一塊。JAVA堆在虛擬機器啟動時建立,被所有執行緒共享。
- 存放幾乎所有的物件例項
- 垃圾收集主要管理的就是JAVA堆
- 物理上不連續,邏輯上連續

(5)方法區
方法區域java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存:

  • 載入的類資訊
  • 常量
  • 靜態變數
  • 即時編譯器編譯後的程式碼資料

執行時常量池位於方法區,存放執行時產生的常量,比如String物件。方法區有兩個值注意的地方:

  • 物理上不需要連續的記憶體,邏輯上連續即可
  • 可選擇固定大小的記憶體區域
  • 可選擇不實現垃圾收集

實戰:outofmemoryerror異常
(1)在單執行緒下,無論是由於棧幀太大還是由於虛擬機器棧容量太小,當記憶體無法分配時,虛擬機器丟擲的都是java.lang.StackOverflowError異常。

package cn.test.two;

public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak(){
        stackLength++;
        stackLeak();
    }

    /**
     * 執行結果表明;在單執行緒下,無論是由於棧幀太大還是由於虛擬機器棧容量太小,當
     * 記憶體無法分配時,虛擬機器丟擲的都是java.lang.StackOverflowError異常
     * @param args
     * @throws Throwable
     */
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try{
            oo`這裡寫程式碼片`m.stackLeak();
        }catch (Throwable e) {
            System.out.println(oom.stackLength);
            throw e;
        }

    }
}

(2)不斷建立多執行緒導致發生OutOfMemory異常。

package cn.test.two;

/**
 * 別執行,會當機
 * 多執行緒導致記憶體溢位
 * @author cjc
 *
 */
public class JavaVMStackOOM {
    private void dontStop(){
        while(true){

        }
    }

    public void stackLeakByThread(){
        while(true){
            Thread thread = new Thread(new Runnable() {

                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

(3)方法區的執行時常量池記憶體溢位

package cn.test.two;

import java.util.ArrayList;
import java.util.List;

/**
 * 方法區的執行時常量池記憶體溢位
 * @author cjc
 *
 */
public class RuntimeConstraintPoolOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i = 0;
        while(true){
            list.add(String.valueOf(i++).intern());
        }
    }
}

在記憶體溢位還是棧溢位方面,關注點在於java棧和java堆的常量池。

JVM是如何判定物件是否應該被回收

  1. 引用計數法

    原理:給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1,;當引用失效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的。
    缺點:這種演算法不能解決物件之間相互迴圈引用的問題。所以主流的JAVA虛擬機器沒有選用引用計數演算法來管理記憶體。(比如:obja.obj = objb; objb.obj = obja;)

  2. 可達性分析

    原理:通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連(即不可達)時,則證明此物件不可用。如圖:
    這裡寫圖片描述
    在Java中,可作為GC Roots的物件包括:
    (1)虛擬機器棧(棧幀中的本地變數表)
    (2)方法區中類靜態屬性引用的物件
    (3)方法區中常量引用的物件
    (4)本地方法棧中JNI

再談引用

相關文章