注:“藍色加粗字體”為書本原語
先來一張JVM運行時數據區域圖,再接下來一一分析各區域功能:
程序計數器
程序計數器(program Counter Register)是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。
Java虛擬機棧
虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。下面借用網友的一張圖可能會更清晰:
局部變量表存放了預編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型)和returnAddress類型(指向了一條字節碼指令的地址)。其中64位長度的long和double類型的數據會占用2個局部變量空間(Slot),其余的數據類型只占用1個。通過上述表述(個人理解),局部變量空間是以Slot(32位)為空間單位的。就算原本一字節大小的byte類型在局部變量表也要占一個Slot(32位=4字節)。
在Java虛擬機規范中,Java虛擬機棧規定了兩種異常情況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。換句話說,如果線程棧的大小有1MB,如果當前線程大量使用了遞歸,那么當線程的棧幀總和超過1MB,JVM就會拋出StackOverflowError。另外,創建線程數量是需要分配線程棧內存的,但系統沒有內存可以分配時,就會拋出OutOfMemoryError。說多是廢話,實踐才是真理,接下來嘗試制作以上兩種虛擬機棧錯誤。
測試代碼:
public class StackTest { public static void recursion(int count){ System.out.println("count="+count); recursion(++count); } public static void main(String[] args) { recursion(0); } }
服務器和JVM信息如下:
把以上代碼打成Jar在此環境下運行:
[wc@localhost JAVATEST]$ java -jar stackTest.jar count=0 count=1 …… count=8406 count=8407 count=8408 Exception in thread "main" java.lang.StackOverflowError at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691) at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579) at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271) at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125) at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207) at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129) at java.io.PrintStream.write(PrintStream.java:526) at java.io.PrintStream.print(PrintStream.java:669) at java.io.PrintStream.println(PrintStream.java:806) at StackTest.recursion(StackTest.java:5) at StackTest.recursion(StackTest.java:6)
從以上信息可看出,當recursion方法運行到count=8408的時候,線程棧已經超過了JVM默認的ThreadStackSize大小(1MB),則拋出了StackOverflowError異常。
接下來我們嘗試通過加大設置線程棧大小(ThreadStackSize)值來消耗盡量多的系統內存,導致無法再分配線程棧內存而拋出OutOfMemoryError異常:
[wc@localhost JAVATEST]$ java -jar -Xss2048M stackTest.jar Error occurred during initialization of VM java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Thread.java:714) at java.lang.ref.Reference.<clinit>(Reference.java:187)
JVM線程鎖消耗的是Java進程的內存,但不消耗給JVM的內存,一個進程開啟的最大線程數大概可以用以下公式進行理解:
線程數 = (進程最大分配內存數-JVM內存-保留的操作系統內存)/線程棧大小
進程最大分配內存大小跟操作系統有關,由於本人虛擬服務器的總內存大小為2GB,所以我嘗試通過-Xss參數分配2GB的內存大小給線程棧,讓虛擬機拋出異常。
本地方法棧
本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。
Java堆
Java堆是Java虛擬機所管理的內存中最大的一塊數據區域,在虛擬機啟動時創建並被所有線程共享。此內存區域唯一的目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存,例如對象實例和數組。但隨着其他技術的成熟(如JIT),對象分配在堆上慢慢地變得又沒那么“絕對”了。Java堆同樣是垃圾收集器管理的主要區域,由於現在的收集器基本都采用分代收集算法,所以Java堆中還可以細分為新生代和老年代。當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制)。如果堆中沒有內存完成實例分配,並且對也無法再擴展時,將會拋出OutOfMemoryError異常。繼續上代碼制造內存溢出:
public class HeapTest { public static void main(String[] args) { List<byte[]> byteList = new ArrayList<byte[]>(); for(int i=0;i<10000;i++){ byteList.add(new byte[1024000]); System.out.println("count="+i); } } }
打包為heapTest.jar進行測試:
[wc@localhost JAVATEST]$ java -jar heapTest.jar count=0 count=1 …… count=405 count=406 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at HeapTest.main(HeapTest.java:9)
通過打印信息得知,當程序創建第406個byte數組(每個數組1MB)時,堆就產生溢出了。
方法區
方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。方法區也稱Non-Heap(非堆),目的是與Java堆區分開來,可通過-XX:MaxPermSize設置內存大小。從JVM運行時區域內存模型來看(本文第一張圖),堆和方法區是兩塊獨立的內存塊。但從垃圾收集器來看,HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區,所以很多人都更願意把方法區稱為“永久代”,如下圖所示:
運行時常量池(Runtime Constant Pool)是方法區的一部分,用於存放Class文件在編譯期生成的各種字面量和符號引用,因為Class文件除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table)。這部分內容將在類加載后進入方法區的運行時常量池中存放。同時運行時常量池具備動態性,並非預置入Class文件中常量池的內存才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,例如String類的inter()方法。既然運行時常量池是方法區的一部分,自然受到方法區內存限制,當常量池無法再申請到內存時會拋出OutOfMemoryError異常。繼續上代碼制造內存溢出:
public class MethodAreaTest { public static void main(String[] args) { List<String> list = new ArrayList<String>(); int i = 0; while(true){ System.out.println(i); list.add(("MethodAreaTest"+String.valueOf(i++)).intern()); } } }
由於JDK1.8 HotSpot虛擬機去掉了永久代,所以要回歸JDK1.6測試:
打包為MethodAreaTest.jar進行測試:
wc@WC01:~/JAVATEST> java -jar MethodAreaTest.jar 1
2 …… 815311
815312
815313 Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at MethodAreaTest.main(MethodAreaTest.java:11)
通過打印信息可以看到,當intern了815313次之后,出現了方法區內存溢出(畢竟常量池是屬於方法區的)。