面試必問:說一下 Java 虛擬機的內存布局?
我們通常所說的 Java 虛擬機(JVM)的內存布局,一般是指 Java 虛擬機的運行時數據區(Runtime Data Area),也就是當字節碼被類加載器加載之后的執行區域劃分。當然它通常是 JVM 模塊的第一個面試問題,所以,接下來我們一起來看它里面包含了哪些內容。
官方定義
《Java虛擬機規范》中將 JVM 運行時數據區域劃分為以下 5 部分:
- 程序計數器(Program Counter Register)
- Java虛擬機棧(Java Virtual Machine Stacks)
- 本地方法棧(Native Method Stack)
- Java 堆(Java Heap)
- 方法區(Methed Area)
如下圖所示:
接下來,我們分別來看每個模塊的作用及詳細介紹。
一、程序計數器
《Java虛擬機規范》中對程序計數器的定義如下:
The Java Virtual Machine can support many threads of execution at once (JLS §17). Each Java Virtual Machine thread has its own pc (program counter) register. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method (§2.6) for that thread. If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined. The Java Virtual Machine's pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.
以上內容翻譯成中文,簡單來說它的含義是:JVM 中可以有多個執行線程,每個線程都有自己的程序計數器,在程序計數器中包含的是正在執行線程的指令地址。
也就是說,程序計數器(Program Counter Register)是線程獨有一塊很小的內存區域,保存當前線程所執行字節碼的位置,包括正在執行的指令、跳轉、分支、循環、異常處理等。
1、作用
我們知道,CPU 核數是比較少的,而任務(線程)是比較多的,所以真實的情況是,CPU 會不停的切換線程以執行所有的程序,當然因為(CPU)切換的速度比較快,所以我們是感知不到的,我們感覺好像所有的程序都是一直在執行,其實從微觀的層面來看,所有的程序都是切換執行的。
那么問題來了,CPU 一直在切換線程執行任務,那 CPU 再次切換到某個線程時,它是怎么知道當前的線程上次知道到哪了?
這就是程序計數器的作用,程序計數器里面保存了當前線程執行的行號,這樣當 CPU 切換到當前線程時,才能接著上次執行的位置,繼續執行。
PS:程序計數器中真實記錄的是下一行任務的執行指令。程序計數器也是 JVM 運行時數據區中執行最快的一塊區域。
2、線程共享
程序計數器記錄的是每個線程的執行行號,所以每個線程都擁有自己的程序計數器,所以此區域不是線程共享的,而是線程私有的。
3、GC
GC 是 Garbage Collection 的縮寫,譯為垃圾收集。
此區域不存在 GC。
4、OOM
OOM 是 Out of Memory 的縮寫,譯為內存溢出。
此區域不存在 OOM 的問題。
二、Java 虛擬機棧
Java 虛擬機棧(Java Virtual Machine Stack)也叫做 JVM 棧,《Java虛擬機規范》對此區域的說明如下:
Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames (§2.6). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous.
In the First Edition of The Java? Virtual Machine Specification, the Java Virtual Machine stack was known as the Java stack.
This specification permits Java Virtual Machine stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the Java Virtual Machine stacks are of a fixed size, the size of each Java Virtual Machine stack may be chosen independently when that stack is created.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of Java Virtual Machine stacks, as well as, in the case of dynamically expanding or contracting Java Virtual Machine stacks, control over the maximum and minimum sizes.
The following exceptional conditions are associated with Java Virtual Machine stacks:
- If the computation in a thread requires a larger Java Virtual Machine stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
- If Java Virtual Machine stacks can be dynamically expanded, and expansion is attempted but insufficient memory can be made available to effect the expansion, or if insufficient memory can be made available to create the initial Java Virtual Machine stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.
以上內容翻譯成中文的含義如下:
Java 虛擬機棧是線程私有的區域,它隨著線程的創建而創建。它里面保存的是局部變量表(基礎數據類型和對象引用地址)和計算過程中的中間結果。Java 虛擬機的內存不需要連續,它只有兩個操作:入棧和出棧。
Java 虛擬機棧要么大小固定,要么根據計算動態的擴展和收縮。程序員可以對 Java 虛擬機棧進行初始值的大小設置和最大值的設置。
Java 虛擬機棧出現的異常有兩種:
- 當 Java 虛擬機棧大小固定時,如果程序中的棧分配超過了最大虛擬機棧就會出現 StackOverflowError 異常。
- 如果 Java 虛擬機棧是動態擴展的,那么當內存不足時,就會引發 OutOfMemoryError 的異常。
1、作用
Java 虛擬機棧主要是管 Java 程序運行的,它保存的是方法的局部變量、方法執行中的部分結果,并參與方法的調用和返回。
簡單來說,棧是運行時單位,而堆是存儲單位。也就是說:棧解決的是程序運行的問題,即程序如何執行?或者說如何處理數據。堆解決的是數據存儲的問題,即數據怎么放?放在哪兒。
2、線程共享
Java 虛擬機棧是線程私有的,它的生命周期和線程的生命周期一致。
3、GC
Java 虛擬機棧因為只有入棧和出棧兩個操作,所以它是不涉及垃圾回收的。
4、OOM
此區域雖然沒有 GC,但存在兩種異常:
- 當 Java 虛擬機棧大小固定時,如果程序中的棧分配超過了最大虛擬機棧就會出現 StackOverflowError 異常。
- 如果 Java 虛擬機棧是動態擴展的,那么當內存不足時,就會引發 OutOfMemoryError 的異常。
也就是,Java 虛擬機棧是可能存在 OOM 的。
5、常見參數設置
設置 Java 虛擬機棧大小:-Xss<size>。
如設置:-Xss128k,表示設置每個線程的棧大小為 128k,此設置等價于 -XX:ThreadStackSize=128k。
6、常見問題演示
最簡單的錯誤示例就是死循環,方法自己調自己,這樣 Java 虛擬機棧就會只入棧不出棧,當到達 Java 虛擬機棧的最大數之后就會出現 StackOverflowError 異常,如下圖所示:
三、本地方法棧
本地方法棧(Native Method Stacks),《Java 虛擬機規范》對此區域的說明如下:
An implementation of the Java Virtual Machine may use conventional stacks, colloquially called "C stacks," to support native methods (methods written in a language other than the Java programming language). Native method stacks may also be used by the implementation of an interpreter for the Java Virtual Machine's instruction set in a language such as C. Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks. If supplied, native method stacks are typically allocated per thread when each thread is created.
This specification permits native method stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the native method stacks are of a fixed size, the size of each native method stack may be chosen independently when that stack is created.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the native method stacks, as well as, in the case of varying-size native method stacks, control over the maximum and minimum method stack sizes.
The following exceptional conditions are associated with native method stacks:
- If the computation in a thread requires a larger native method stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
- If native method stacks can be dynamically expanded and native method stack expansion is attempted but insufficient memory can be made available, or if insufficient memory can be made available to create the initial native method stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.
以上內容,挑重點簡單翻譯一下:本地方法棧俗稱“C?!保?Native(本地)方法(用 Java 編程語言以外的語言編寫的方法),此區域和 Java 虛擬機棧類似,這不過諸如 C 語言等使用的??臻g。它也是存在兩種異常:StackOverflowError 和 OutOfMemoryError。
PS:因為此區域是非 Java 語言實現和使用的,所以本文就不做過多的贅述,總之,記得一句話:此區域和 Java 虛擬機棧類似,不過是給 C/C++ 語言使用的。
四、堆
堆(Heap)《Java 虛擬機規范》對此區域的說明如下:
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor's system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the heap, as well as, if the heap can be dynamically expanded or contracted, control over the maximum and minimum heap size.
The following exceptional condition is associated with the heap:
- If a computation requires more heap than can be made available by the automatic storage management system, the Java Virtual Machine throws an OutOfMemoryError.
以上內容,挑重點簡單翻譯一下:
堆是線程共享的,程序中所有類實例和數組的內存都存儲在此區域,它在 Java 虛擬機啟動時就會創建。對象不會被顯式釋放,只會在垃圾收集時釋放。堆的大小可以是固定的,也可以動態擴展或收縮。堆的內存在物理層面不需要是連續的。
程序員可以對堆進行初始大小控制,或者設置最大、最小堆的容量。
堆可能會出現 OutOfMemoryError 異常。
1、作用
堆是 Java 虛擬機的主要存儲單位,Java 中所有的對象和數組都是保存在此區域的。
2、線程共享
堆是線程共享的,堆上的對象可能被多個線程同時訪問。
3、GC
堆是 JVM 最大的一塊區域,也是垃圾回收器進行垃圾回收最頻繁的一塊區域。
4、OOM
當堆空間不足時,會發生 OutOfMemoryError 異常。
5、常見參數設置
- -Xms<size>:設置初始 Java 堆大小,比如:-Xms10m,表示設置堆的初始大小為 10MB。
- -Xmx<size>:設置最大 Java 堆大小,比如:-Xmx10m,表示設置堆的最大空間為 10MB。
6、常見問題演示
接下來,我們來演示一下堆空間 OOM 的問題,我們先使用“-Xmx50m”的參數來設置一下 Idea,它表示將程序運行的最大內存設置為 50m,如果程序的運行超過這個值就會出現內存溢出的問題,設置方法如下:
設置后的最終效果這樣的:
PS:因為我使用的 Idea 是社區版,所以可能和你的界面不一樣,你只需要點擊“Edit Configurations...”找到“VM options”選項,設置上“-Xmx50m”參數就可以了。
配置完 Idea 之后,接下來我們來實現一下業務代碼。在代碼中我們會創建一個大對象,這個對象中會有一個 10m 大的數組,然后我們將這個大對象存儲在 ThreadLocal 中,再使用線程池執行大于 5 次添加任務,因為設置了最大運行內存是 50m,所以理想的情況是執行 5 次添加操作之后,就會出現內存溢出的問題,實現代碼如下:
以上程序的執行結果如下:
從上述圖片可看出,當程序執行到第 5 次添加對象時就出現內存溢出的問題了,這是因為設置了最大的運行內存是 50m,每次循環會占用 10m 的內存,加上程序啟動會占用一定的內存,因此在執行到第 5 次添加任務時,就會出現內存溢出的問題。
五、方法區
方法區(Method Area)《Java 虛擬機規范》對此區域的說明如下:
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.
The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.
The following exceptional condition is associated with the method area:
- If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.
以上內容,挑重點簡單翻譯一下:
方法區是線程共享的,方法區類似于傳統語言的編譯代碼的存儲區,或者類似于操作系統進程中的“文本”段。它存儲每個類的結構,如運行時常量池、字段和方法數據,以及方法和構造函數的代碼。
方法區域是在 Java 虛擬機啟動時創建的,盡管方法區域在邏輯上是堆的一部分,但簡單的實現可能選擇不進行垃圾收集或壓縮。方法區域可以是固定的大小,也可以動態擴展。方法區的(物理)內存不需要連續。
Java 虛擬機實現可以為程序員或用戶提供對方法區域初始大小的控制,以及在可變大小的方法區域的情況下,對最大和最小方法區域大小的控制。
如果方法區中的內存無法滿足分配請求,Java 虛擬機將拋出一個 OutOfMemoryError。
1、作用
用于存儲每個類的結構,包括運行時常量池、靜態變量、字段和方法數據。
2、HotSpot 方法區實現
HotSpot 虛擬機是 Sun JDK 和 Open JDK 中自帶的虛擬機,也是目前使用范圍最廣的 Java 虛擬機。作為官方 Java 虛擬機的化身,目前所講的所有知識,幾乎都是針對此虛擬機的,所以我們要看 HotSpot 虛擬機對《Java 虛擬機規范》中方法區的實現。
?對于 HotSpot 虛擬機來說,不同的 JDK 方法區的實現是不同的,在 JDK 1.7 之前,HotSpot 技術團隊使用的是永久代來實現方法區的,但這種實現有一個致命的問題,這樣設計更容易造成內存溢出。因為永久代有 -XX:MaxPermSize(方法區分配的最大內存)的上限,即使不設置也會有默認的大小。例如,32 位操作系統中的 4GB 內存限制等,并且這樣設計導致了部分的方法在不同類型的 Java 虛擬機下的表現也不同,比如 String::intern() 方法。所以在 JDK 1.7 時 HotSpot 虛擬機已經把原本放在永久代的字符串常量池和靜態變量等移出了方法區,并且在 JDK 1.8 中完全廢棄了永久代的概念。
JDK 1.8 之后,HotSpot 虛擬機開始使用元空間(Meta Space)來實現方法區了。?
3、線程共享
方法區是線程共享的。
4、GC
《Java 虛擬機規范》中規定方法區可以沒有 GC(垃圾回收),但 HotSpot 中對此區域實現了 GC 操作。
5、OOM
方法區是存在 OOM 情況的,比如在 JDK 1.8 中,如果元空間設置空間過小,而類信息產生的過多就會產生 OOM,如下示例所示:
以上程序的執行結果如下圖所示:
以上代碼是通過 CGLIB 不斷的產生動態代理類將方法區填滿,從而就導致 OOM 的問題。
PS:在使用 CGLIB 之前,需要現在當前項目中導入 CGLIB 才可以正常使用。
6、常用參數設置
永久代(HotSpot 虛擬機,JDK 1.7 之前設置有效):
- -XX:PermSize=100m:設置永久代初始值為 100MB。
- -XX:MaxPermSize=100m:設置永久代最大值為 100MB。
元空間(HotSpot 虛擬機,JDK 1.8 之后設置有效):
- -XX:MetaspaceSize=100m:設置元空間初始大小為 100MB。
- -XX:MaxMetaspaceSize=100m:設置元空間最大容量為 100MB,默認不設置,則沒有限制。
- -XX:CompressedClassSpaceSize=100m:設置 Class Metaspace 的大小為 100MB,默認值為 1G。
直接內存(HotSpot 虛擬機,JDK 1.8 之后設置有效):
-XX:MaxDirectMemorySize=100m:指定直接內存的最大容量為 100MB。
總結
《Java虛擬機規范》中將 JVM 運行時數據區域劃分為以下 5 部分:
- 程序計數器(Program Counter Register);
- Java 虛擬機棧(Java Virtual Machine Stacks);
- 本地方法棧(Native Method Stack);
- Java 堆(Java Heap);
- 方法區(Methed Area)。
其中線程私有的區域是:程序計數器、Java 虛擬機棧、本地方法棧;而線程共享的是:Java 堆和方法區。
而除了程序計數器,其他區域都是可以會出現 OOM 的。