超硬核!!!一篇文章搞定整個JVM運行時數據區


註釋:JVM就是Java虛擬機,Java虛擬機就是JVM

解釋非常詳細:讓你面試不在害怕被問到運行時數據區
在這裏插入圖片描述

1 JVM運行時數據區

什麼是運行時數據區(就是我們java運行時的東西是放在那裏的)
在這裏插入圖片描述

2 解析JVM運行時數據區

2.1 方法區(Method Area)

  1. 方法區是所有線程共享的內存區域,它用於存儲已被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
  2. 它有個別命叫Non-Heap(非堆)。當方法區無法滿足內存分配需求時,拋出OutOfMemoryError異常。

2.2 Java堆(Java Heap)

  1. java堆是java虛擬機所管理的內存中最大的一塊,是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例。
  2. 在Java虛擬機規範中的描述是:所有的對象實例以及數組都要在堆上分配。
  3. java堆是垃圾收集器管理的主要區域,因此也被成爲“GC堆”。
  4. 從內存回收角度來看java堆可分爲:新生代和老生代。
  5. 從內存分配的角度看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區。
  6. 無論怎麼劃分,都與存放內容無關,無論哪個區域,存儲的都是對象實例,進一步的劃分都是爲了更好的回收內存,或者更快的分配內存。
  7. 根據Java虛擬機規範的規定,java堆可以處於物理上不連續的內存空間中。當前主流的虛擬機都是可擴展的(通過 -Xmx 和 -Xms 控制)。如果堆中沒有內存可以完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。

2.3 程序計數器(Program Counter Register)

  1. 程序計數器是一塊較小的內存空間,它可以看作是:保存當前線程所正在執行的字節碼指令的地址(行號)
  2. 由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,一個處理器都只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都有一個獨立的程序計數器,各個線程之間計數器互不影響,獨立存儲。稱之爲“線程私有”的內存。程序計數器內存區域是虛擬機中唯一沒有規定OutOfMemoryError情況的區域。

總結:也可以把它叫做線程計數器

例子:在java中最小的執行單位是線程,線程是要執行指令的,執行的指令最終操作的就是我們的電腦,就是 CPU。在CPU上面去運行,有個非常不穩定的因素,叫做調度策略,這個調度策略是時基於時間片的,也就是當前的這一納秒是分配給那個指令的。

假如:線程A在看直播
在這裏插入圖片描述
突然,線程B來了一個視頻電話,就會搶奪線程A的時間片,就會打斷了線程A,線程A就會掛起
在這裏插入圖片描述
然後,視頻電話結束,這時線程A究竟該幹什麼?
(線程是最小的執行單位,他不具備記憶功能,他只負責去幹,那這個記憶就由:程序計數器來記錄

在這裏插入圖片描述

2.4 Java虛擬機棧(Java Virtual Machine Stacks)

  1. java虛擬機是線程私有的,它的生命週期和線程相同。
  2. 虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。

解釋:每虛擬機棧中是有單位的,單位就是棧幀,一個方法一個棧幀。一個棧幀中他又要存儲,局部變量,操作數棧,動態鏈接,出口等。

在這裏插入圖片描述
解析棧幀:

  1. 局部變量表:是用來存儲我們臨時8個基本數據類型、對象引用地址、returnAddress類型。(returnAddress中保存的是return後要執行的字節碼的指令地址。)
  2. 操作數棧:操作數棧就是用來操作的,例如代碼中有個 i = 6*6,他在一開始的時候就會進行操作,讀取我們的代碼,進行計算後再放入局部變量表中去
  3. 動態鏈接:假如我方法中,有個 service.add()方法,要鏈接到別的方法中去,這就是動態鏈接,存儲鏈接的地方。
  4. 出口:出口是什呢,出口正常的話就是return 不正常的話就是拋出異常落

思考:.
一個方法調用另一個方法,會創建很多棧幀嗎?
答:會創建。如果一個棧中有動態鏈接調用別的方法,就會去創建新的棧幀,棧中是由順序的,一個棧幀調用另一個棧幀,另一個棧幀就會排在調用者下面

棧指向堆是什麼意思?
棧指向堆是什麼意思,就是棧中要使用成員變量怎麼辦,棧中不會存儲成員變量,只會存儲一個應用地址,堆中的數據等下講

遞歸的調用自己會創建很多棧幀嗎?
遞歸的話也會創建多個棧幀,就是一直排下去

2.5 本地方法棧(Native Method Stack)

  1. 本地方法棧很好理解,他很棧很像,只不過方法上帶了 native 關鍵字的棧字
  2. 它是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)的服務
  3. native關鍵字的方法是看不到的,必須要去oracle官網去下載纔可以看的到,而且native關鍵字修飾的大部分源碼都是C和C++的代碼。
  4. 同理可得,本地方法棧中就是C和C++的代碼

3 Java內存結構

在這裏插入圖片描述
上面已經講了運行時數據區,這裏就差幾個小組件了

3.1 JVM字節碼執行引擎

虛擬機核心的組件就是執行引擎,它負責執行虛擬機的字節碼,一般戶先進行編譯成機器碼後執行。

“虛擬機”是一個相對於“物理機”的概念,虛擬機的字節碼是不能直接在物理機上運行的,需要JVM字節碼執行引擎編譯成機器碼後纔可在物理機上執行。

3.2 垃圾收集系統

程序在運行過程中,會產生大量的內存垃圾(一些沒有引用指向的內存對象都屬於內存垃圾,因爲這些對象已經無法訪問,程序用不了它們了,對程序而言它們已經死亡),爲了確保程序運行時的性能,java虛擬機在程序運行的過程中不斷地進行自動的垃圾回收(GC)。

垃圾收集系統是Java的核心,也是不可少的,Java有一套自己進行垃圾清理的機制,開發人員無需手工清理

3.3 直接內存(Direct Memory)

  1. 直接內存不是虛擬機運行時數據區的一部分,也不是java虛擬機規範中定義的內存區域。但是既然是內存,肯定還是受本機總內存(包括RAM以及SWAP區或者分頁文件)大小以及處理器尋址空間的限制。
  2. 在JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O 方式,它可以使用native 函數庫直接分配堆外內存,然後通脫一個存儲在Java堆中的DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在Java堆和Native(本地)堆中來回複製數據。

直接內存與堆內存的區別:
直接內存申請空間耗費很高的性能,堆內存申請空間耗費比較低
直接內存的IO讀寫的性能要優於堆內存,在多次讀寫操作的情況相差非常明顯

代碼示例:(報錯修改time 值)

package com.lijie;

import java.nio.ByteBuffer;

/**
 * 直接內存 與 堆內存的比較
 */
public class ByteBufferCompare {

    public static void main(String[] args) {
        allocateCompare();   //分配比較
        operateCompare();    //讀寫比較
    }

    /**
     * 直接內存 和 堆內存的 分配空間比較
     */
    public static void allocateCompare() {
        int time = 10000000;    //操作次數
        long st = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {

            ByteBuffer buffer = ByteBuffer.allocate(2);      //非直接內存分配申請
        }
        long et = System.currentTimeMillis();
        System.out.println("在進行" + time + "次分配操作時,堆內存:分配耗時:" + (et - st) + "ms");
        long st_heap = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接內存分配申請
        }
        long et_direct = System.currentTimeMillis();
        System.out.println("在進行" + time + "次分配操作時,直接內存:分配耗時:" + (et_direct - st_heap) + "ms");
    }

    /**
     * 直接內存 和 堆內存的 讀寫性能比較
     */
    public static void operateCompare() {
        //如果報錯修改這裏,把數字改小一點
        int time = 1000000000;
        ByteBuffer buffer = ByteBuffer.allocate(2 * time);
        long st = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            buffer.putChar('a');
        }
        buffer.flip();
        for (int i = 0; i < time; i++) {
            buffer.getChar();
        }
        long et = System.currentTimeMillis();
        System.out.println("在進行" + time + "次讀寫操作時,堆內存:讀寫耗時:" + (et - st) + "ms");
        ByteBuffer buffer_d = ByteBuffer.allocateDirect(2 * time);
        long st_direct = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            buffer_d.putChar('a');
        }
        buffer_d.flip();
        for (int i = 0; i < time; i++) {
            buffer_d.getChar();
        }
        long et_direct = System.currentTimeMillis();
        System.out.println("在進行" + time + "次讀寫操作時,直接內存:讀寫耗時:" + (et_direct - st_direct) + "ms");
    }
}

測試結果:

在進行10000000次分配操作時,堆內存:分配耗時:98ms
在進行10000000次分配操作時,直接內存:分配耗時:8895ms
在進行1000000000次讀寫操作時,堆內存:讀寫耗時:5666ms
在進行1000000000次讀寫操作時,直接內存:讀寫耗時:884ms

代碼來源:「獼猴桃0303」
鏈接爲:https://blog.csdn.net/leaf_0303/article/details/78961936

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章