Java 虛擬機內存模型是 Java 程序運行的基礎。爲了能使 Java 應用程序正常運行,JVM 虛擬機將其內存數據分爲程序計數器、虛擬機棧、本地方法棧、Java 堆和方法區等部分。html
程序計數器 (Program Counter Register)java
程序計數器 (Program Counter Register) 是一塊很小內存空間,因爲 Java 是支持線程的語言,當線程數量超過 CPU 數量時,線程之間根據時間片輪詢搶奪 CPU 資源。對於單核 CPU 而言,每一時刻只能有一個線程在運行,而其餘線程必須被切換出去。爲此,每個線程都必須用一個獨立的程序計數器,用於記錄下一條要運行的指令。各個線程之間的計數器互不影響,獨立工做,是一塊線程獨有的內存空間。若是當前線程正在執行一個 Java 方法,則程序計數器記錄正在執行的 Java 字節碼地址,若是當前線程正在執行一個 Native 方法,則程序計數器爲空。程序員
虛擬機棧數組
虛擬機棧用於存放函數調用堆棧信息。Java 虛擬機棧也是線程私有的內存空間,它和 Java 線程在同一時間建立,它保存方法的局部變量、部分結果,並參與方法的調用和返回。性能優化
Java 虛擬機規範容許 Java 棧的大小是動態的或者是固定的。在 Java 虛擬機規範中定義了兩種異常與棧空間有關:StackOverflowError 和 OutOfMemoryError。若是線程在計算過程當中,請求的棧深度大於最大可用的棧深度,則拋出 StackOverflowError;若是 Java 棧能夠動態擴展,而在擴展棧的過程當中沒有足夠的內存空間來支持棧的發展,則拋出 OutOfMemeoryError。可使用-Xss 參數來設置棧的大小,棧的大小直接決定了函數調用的可達深度。數據結構
下面的例子展現了一個遞歸調用的應用。計數器 count 記錄了遞歸的層次,這個沒有出口的遞歸函數必定會致使棧溢出。程序則在棧溢出時,打印出棧的當前深度。併發
清單 1. 遞歸調用顯示棧的最大深度ide
public class TestStack { private int count = 0; //沒有出口的遞歸函數 public void recursion(){ count++;//每次調用深度加 1 recursion();//遞歸 } public void testStack(){ try{ recursion(); }catch(Throwable e){ System.out.println("deep of stack is "+count);//打印棧溢出的深度 e.printStackTrace(); } } public static void main(String[] args){ TestStack ts = new TestStack(); ts.testStack(); } }
清單 2. 清單 1 運行結果函數
java.lang.StackOverflowError at TestStack.recursion(TestStack.java:7) at TestStack.recursion(TestStack.java:7) at TestStack.recursion(TestStack.java:7) at TestStack.recursion(TestStack.java:7) at TestStack.recursion(TestStack.java:7) at TestStack.recursion(TestStack.java:7) at TestStack.recursion(TestStack.java:7)deep of stack is 9013
虛擬機棧在運行時使用一種叫作棧幀的數據結構保存上下文數據。在棧幀中,存放了方法的局部變量表、操做數棧、動態鏈接方法和返回地址等信息。每個方法的調用都伴隨着棧幀的入棧操做。相應地,方法的返回則表示棧幀的出棧操做。若是方法調用時,方法的參數和局部變量相對較多,那麼棧幀中的局部變量表就會比較大,棧幀會膨脹以知足方法調用所需傳遞的信息。所以,單個方法調用所需的棧空間大小也會比較多。工具
函數嵌套調用的次數由棧的大小決定。棧越大,函數嵌套調用次數越多。對一個函數而言,它的參數越多,內部局部變量越多,它的棧幀就越大,其嵌套調用次數就會減小。
本地方法棧
本地方法棧和 Java 虛擬機棧的功能很類似,本地方法棧用於存放函數調用堆棧信息。Java 虛擬機棧用於管理 Java 函數的調用,而本地方法棧用於管理本地方法的調用。本地方法並非用 Java 實現的,而是使用 C 實現的。在 SUN 的 HotSpot 虛擬機中,不區分本地方法棧和虛擬機棧。所以,和虛擬機棧同樣,它也會拋出 StackOverflowError 和 OutofMemoryError。
Java 堆
堆用於存放 Java 程序運行時所需的對象等數據。幾乎全部的對象和數組都是在堆中分配空間的。Java 堆分爲新生代和老生代兩個部分,新生代用於存放剛剛產生的對象和年輕的對象,若是對象一直沒有被回收,生存得足夠長,老年對象就被移入老年代。新生代又可進一步細分爲 eden、survivor space0 和 survivor space1。eden 即對象的出生地,大部分對象剛剛創建時都會被存放在這裏。survivor 空間是存放其中的對象至少經歷了一次垃圾回收,並得以倖存下來的。若是在倖存區的對象到了指定年齡仍未被回收,則有機會進入老年代 (tenured)。下面例子演示了對象在內存中的分配方式。
清單 3. 進行一次新生代 GC
public class TestHeapGC { public static void main(String[] args){ byte[] b1 = new byte[1024*1024/2]; byte[] b2 = new byte[1024*1024*8]; b2 = null; b2 = new byte[1024*1024*8];//進行一次新生代 GC System.gc(); } }
清單 4. 清單 3 的配置
-XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -Xms40M -Xmx40M -Xmn20M
清單 5. 清單 3 的輸出
[GC [DefNew: 9031K->661K(18432K), 0.0022784 secs] 9031K->661K(38912K), 0.0023178 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] Heap def new generation total 18432K, used 9508K [0x34810000, 0x35c10000, 0x35c10000) eden space 16384K, 54% used [0x34810000, 0x350b3e58, 0x35810000) from space 2048K, 32% used [0x35a10000, 0x35ab5490, 0x35c10000) to space 2048K, 0% used [0x35810000, 0x35810000, 0x35a10000) tenured generation total 20480K, used 0K [0x35c10000, 0x37010000, 0x37010000) the space 20480K, 0% used [0x35c10000, 0x35c10000, 0x35c10200, 0x37010000) compacting perm gen total 12288K, used 374K [0x37010000, 0x37c10000, 0x3b010000) the space 12288K, 3% used [0x37010000, 0x3706db10, 0x3706dc00, 0x37c10000) ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000) rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
上述輸出顯示 JVM 在進行屢次內存分配的過程當中,觸發了一次新生代 GC。在此次 GC 中,本來分配在 eden 段的變量 b1 被移動到 from 空間段 (s0)。最後分配的 8MB 內存被分配在 eden 新生代。
方法區
方法區用於存放程序的類元數據信息。方法區與堆空間相似,它也是被 JVM 中全部的線程共享的。方法區主要保存的信息是類的元數據。方法區中最爲重要的是類的類型信息、常量池、域信息、方法信息。類型信息包括類的完整名稱、父類的完整名稱、類型修飾符和類型的直接接口類表;常量池包括這個類方法、域等信息所引用的常量信息;域信息包括域名稱、域類型和域修飾符;方法信息包括方法名稱、返回類型、方法參數、方法修飾符、方法字節碼、操做數棧和方法棧幀的局部變量區大小以及異常表。總之,方法區內保持的信息大部分來自於 class 文件,是 Java 應用程序運行必不可少的重要數據。
在 Hot Spot 虛擬機中,方法區也稱爲永久區,是一塊獨立於 Java 堆的內存空間。雖然叫作永久區,可是在永久區中的對象一樣也能夠被 GC 回收的。只是對於 GC 的表現也和 Java 堆空間略有不一樣。對永久區 GC 的回收,一般主要從兩個方面分析:一是 GC 對永久區常量池的回收;二是永久區對類元數據的回收。Hot Spot 虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就能夠被回收。
清單 6 所示代碼會生成大量 String 對象,並將其加入常量池中。String.intern() 方法的含義是若是常量池中已經存在當前 String,則返回池中的對象,若是常量池中不存在當前 String 對象,則先將 String 加入常量池,並返回池中的對象引用。所以,不停地將 String 對象加入常量池會致使永久區飽和。若是 GC 不能回收永久區的這些常量數據,那麼就會拋出 OutofMemoryError 錯誤。
清單.6 GC 收集永久區
public class permGenGC { public static void main(String[] args){ for(int i=0;i<Integer.MAX_VALUE;i++){ String t = String.valueOf(i).intern();//加入常量池 } } }
清單 7. 清單 6 的配置
-XX:PermSize=2M -XX:MaxPermSize=4M -XX:+PrintGCDetails
清單 8. 清單 6 的輸出
[Full GC [Tenured: 0K->149K(10944K), 0.0177107 secs] 3990K->149K(15872K), [Perm : 4096K->374K(4096K)], 0.0181540 secs] [Times: user=0.02 sys=0.02, real=0.03 secs] [Full GC [Tenured: 149K->149K(10944K), 0.0165517 secs] 3994K->149K(15936K), [Perm : 4096K->374K(4096K)], 0.0169260 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] [Full GC [Tenured: 149K->149K(10944K), 0.0166528 secs] 3876K->149K(15936K), [Perm : 4096K->374K(4096K)], 0.0170333 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
每當常量池飽和時,FULL GC 總能順利回收常量池數據,確保程序穩定持續進行。
因爲 Java 字節碼是運行在 JVM 虛擬機上的,一樣的字節碼使用不一樣的 JVM 虛擬機參數運行,其性能表現可能各不同。爲了能使系統性能最優,就須要選擇使用合適的 JVM 參數運行 Java 應用程序。
設置最大堆內存
JVM 內存結構分配對 Java 應用程序的性能有較大的影響。
Java 應用程序可使用的最大堆能夠用-Xmx 參數指定。最大堆指的是新生代和老生代的大小之和的最大值,它是 Java 應用程序的堆上限。清單 9 所示代碼是在堆上分配空間直到內存溢出。-Xmx 參數的大小不一樣,將直接決定程序可以走過幾個循環,本例配置爲-Xmx5M,設置最大堆上限爲 5MB。
清單 9 .Java 堆分配空間
import java.util.Vector; public class maxHeapTest { public static void main(String[] args){ Vector v = new Vector(); for(int i=0;i<=10;i++){ byte[] b = new byte[1024*1024]; v.add(b); System.out.println(i+"M is allocated"); } System.out.println("Max memory:"+Runtime.getRuntime().maxMemory()); } }
清單 10. 運行輸出
0M is allocated 1M is allocated 2M is allocated 3M is allocated Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at maxHeapTest.main(maxHeapTest.java:8)
此時代表在完成 4MB 數據分配後系統空閒的堆內存大小已經不足 1MB 了。
設置 GC 新生代區大小
參數-Xmn 或者用於 Hot Spot 虛擬機中的參數-XX:NewSize(新生代初始大小)、-XX:MaxNewSize 用於設置新生代的大小。設置一個較大的新生代會減少老生代的大小,這個參數對系統性能以及 GC 行爲有很大的影響。新生代的大小通常設置爲整個堆空間的 1/4 到 1/3 左右。
以清單 9 的代碼爲例,若使用 JVM 參數-XX:+PrintGCDetails -Xmx11M -XX:NewSize=2M -XX:MaxNewSize=2M -verbose:gc 運行程序,將新生代的大小減少爲 2MB,那麼 MinorGC 次數將從 4 次增長到 9 次 (默認狀況下是 3.5MB 左右)。
清單 11. 運行輸出
[GC [DefNew: 1272K->150K(1856K), 0.0028101 secs] 1272K->1174K(11072K), 0.0028504 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew: 1174K->0K(1856K), 0.0018805 secs] 2198K->2198K(11072K), 0.0019097 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] clearing.... [GC [DefNew: 1076K->0K(1856K), 0.0004046 secs] 3274K->2198K(11072K), 0.0004382 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew: 1024K->0K(1856K), 0.0011834 secs] 3222K->3222K(11072K), 0.0013508 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew: 1024K->0K(1856K), 0.0012983 secs] 4246K->4246K(11072K), 0.0013299 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] clearing.... [GC [DefNew: 1024K->0K(1856K), 0.0001441 secs] 5270K->4246K(11072K), 0.0001686 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew: 1024K->0K(1856K), 0.0012028 secs] 5270K->5270K(11072K), 0.0012328 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew: 1024K->0K(1856K), 0.0012553 secs] 6294K->6294K(11072K), 0.0012845 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] clearing.... [GC [DefNew: 1024K->0K(1856K), 0.0001524 secs] 7318K->6294K(11072K), 0.0001780 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 1856K, used 1057K [0x36410000, 0x36610000, 0x36610000) eden space 1664K, 63% used [0x36410000, 0x365185a0, 0x365b0000) from space 192K, 0% used [0x365e0000, 0x365e0088, 0x36610000) to space 192K, 0% used [0x365b0000, 0x365b0000, 0x365e0000) tenured generation total 9216K, used 6294K [0x36610000, 0x36f10000, 0x37010000) the space 9216K, 68% used [0x36610000, 0x36c35868, 0x36c35a00, 0x36f10000) compacting perm gen total 12288K, used 375K [0x37010000, 0x37c10000, 0x3b010000) the space 12288K, 3% used [0x37010000, 0x3706dc88, 0x3706de00, 0x37c10000) ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000) rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
設置持久代大小
持久代 (方法區) 不屬於堆的一部分。在 Hot Spot 虛擬機中,使用-XX:MaxPermSize 參數能夠設置持久代的最大值,使用-XX:PermSize 能夠設置持久代的初始大小。持久代的大小直接決定了系統能夠支持多少個類定義和多少常量。對於使用 CGLIB 或者 Javassit 等動態字節碼生成工具的應用程序而言,設置合理的持久代大小有助於維持系統穩定。系統所支持的最大類與 MaxPermSize 成正比。通常來講,MaxPermSize 設置爲 64MB 已經能夠知足絕大部分應用程序正常工做。若是依然出現永久區溢出,能夠設置爲 128MB。這是兩個很經常使用的永久區取值。若是 128MB 依然不能知足應用程序需求,那麼對於大部分應用程序來講,則應該考慮優化系統的設計,減小動態類的產生,或者利用 GC 回收部分駐紮在永久區的無用類信息,以使系統健康運行。
設置線程棧大小
線程棧是線程的一塊私有空間。在 JVM 中可使用-Xss 參數設置線程棧的大小。
在線程中進行局部變量分配,函數調用時都須要在棧中開闢空間。若是棧的空間分配過小,那麼線程在運行時可能沒有足夠的空間分配局部變量或者達不到足夠的函數調用深度,致使程序異常退出;若是棧空間過大,那麼開設線程所需的內存成本就會上升,系統所能支持的線程總數就會降低。因爲 Java 堆也是向操做系統申請內存空間的,所以,若是堆空間過大,就會致使操做系統可用於線程棧的內存減小,從而間接減小程序所能支持的線程數量。
清單 12 所示代碼嘗試開設儘量多的線程,並在線程數量飽和時,打印已經開設的線程數量。
清單 12. 嘗試開啓儘量多的線程
public class TestXss { public static class MyThread extends Thread{ @Override public void run(){ try{ Thread.sleep(10000); }catch(InterruptedException e){ e.printStackTrace(); } } } public static void main(String[] args){ int count=0; try{ for(int i=0;i<10000;i++){ new MyThread().start(); count++; } }catch(OutOfMemoryError e){ System.out.println(count); System.out.println(e.getMessage()); } } }
清單 13. 配置-Xss1M 時的運行輸出
1578 unable to create new native thread
一共容許啓動 1578 個線程。
清單 14. 配置-Xss20M 時的運行輸出
69 unable to create new native thread
實驗證實若是改變系統的最大堆空間設定,能夠發現系統所能支持的線程數量也會相應改變。
Java 堆的分配以 200MB 遞增,當棧大小爲 1MB 時,最大線程數量以 200 遞減。當系統物理內存被堆佔據時,就不能夠被棧使用。當系統因爲內存空間不夠而沒法建立新的線程時會拋出 OOM 異常。這並非因爲堆內存不夠而致使的 OOM,而是由於操做系統內存減去堆內存後剩餘的系統內存不足而沒法建立新的線程。在這種狀況下能夠嘗試減小堆內存以換取更多的系統空間來解決這個問題。綜上所述,若是系統確實須要大量線程併發執行,那麼設置一個較小的堆和較小的棧有助於提升系統所能承受的最大線程數。
設置堆的比例分配
參數-XX:SurvivorRatio 是用來設置新生代中 eden 空間和 s0 空間的比例關係。s0 和 s1 空間又分別稱爲 from 空間和 to 空間。它們的大小是相同的,職能也是相同的,並在 Minor GC 後互換角色。
清單 15. 所示示例演示不斷插入字符時使用的 GC 輸出
import java.util.ArrayList; import java.util.List; public class StringDemo { public static void main(String[] args){ List<String> handler = new ArrayList<String>(); for(int i=0;i<1000;i++){ HugeStr h = new HugeStr(); ImprovedHugeStr h1 = new ImprovedHugeStr(); handler.add(h.getSubString(1, 5)); handler.add(h1.getSubString(1, 5)); } } static class HugeStr{ private String str = new String(new char[800000]); public String getSubString(int begin,int end){ return str.substring(begin, end); } } static class ImprovedHugeStr{ private String str = new String(new char[10000000]); public String getSubString(int begin,int end){ return new String(str.substring(begin, end)); } } }
清單 16. 設置新生代堆爲 10MB,並使 eden 區是 s0 的 8
-XX:+PrintGCDetails -XX:MaxNewSize=10M -XX:SurvivorRatio=8
清單 17. 運行輸出
[Full GC [Tenured: 233756K->233743K(251904K), 0.0524229 secs] 233756K->233743K(261120K), [Perm : 377K->372K(12288K)], 0.0524703 secs] [Times: user=0.06 sys=0.00, real=0.06 secs] def new generation total 9216K, used 170K [0x27010000, 0x27a10000, 0x27a10000) eden space 8192K, 2% used [0x27010000, 0x2703a978, 0x27810000) from space 1024K, 0% used [0x27910000, 0x27910000, 0x27a10000) to space 1024K, 0% used [0x27810000, 0x27810000, 0x27910000) tenured generation total 251904K, used 233743K [0x27a10000, 0x37010000, 0x37010000) the space 251904K, 92% used [0x27a10000, 0x35e53d00, 0x35e53e00, 0x37010000) compacting perm gen total 12288K, used 372K [0x37010000, 0x37c10000, 0x3b010000) the space 12288K, 3% used [0x37010000, 0x3706d310, 0x3706d400, 0x37c10000) ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000) rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
修改參數 SurvivorRatio 爲 2 運行程序,至關於設置 eden 區是 s0 的 2 倍大小,因爲 s1 與 s0 相同,故有 eden=[10MB/(1+1+2)]*2=5MB。
清單 18. 運行輸出
[Full GC [Tenured: 233756K->233743K(251904K), 0.0546689 secs] 233756K->233743K(259584K), [Perm : 377K->372K(12288K)],0.0547257 secs] [Times: user=0.05 sys=0.00, real=0.05 secs] def new generation total 7680K, used 108K [0x27010000, 0x27a10000, 0x27a10000) eden space 5120K, 2% used [0x27010000, 0x2702b3b0, 0x27510000) from space 2560K, 0% used [0x27510000, 0x27510000, 0x27790000) to space 2560K, 0% used [0x27790000, 0x27790000, 0x27a10000) tenured generation total 251904K, used 233743K [0x27a10000, 0x37010000, 0x37010000) the space 251904K, 92% used [0x27a10000, 0x35e53d00, 0x35e53e00, 0x37010000) compacting perm gen total 12288K, used 372K [0x37010000, 0x37c10000, 0x3b010000) the space 12288K, 3% used [0x37010000, 0x3706d310, 0x3706d400, 0x37c10000) ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000) rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)
Java 堆參數總結
Java 堆操做是主要的數據存儲操做,總結的主要參數配置以下。
與 Java 應用程序堆內存相關的 JVM 參數有:
-Xms:設置 Java 應用程序啓動時的初始堆大小;
-Xmx:設置 Java 應用程序能得到的最大堆大小;
-Xss:設置線程棧的大小;
-XX:MinHeapFreeRatio:設置堆空間最小空閒比例。當堆空間的空閒內存小於這個數值時,JVM 便會擴展堆空間;
-XX:MaxHeapFreeRatio:設置堆空間的最大空閒比例。當堆空間的空閒內存大於這個數值時,便會壓縮堆空間,獲得一個較小的堆;
-XX:NewSize:設置新生代的大小;
-XX:NewRatio:設置老年代與新生代的比例,它等於老年代大小除以新生代大小;
-XX:SurvivorRatio:新生代中 eden 區與 survivor 區的比例;
-XX:MaxPermSize:設置最大的持久區大小;
-XX:TargetSurvivorRatio: 設置 survivor 區的可以使用率。當 survivor 區的空間使用率達到這個數值時,會將對象送入老年代。
從全部這些參數描述信息和代碼示例能夠看到,沒有哪一條固定的規則能夠供程序員參考。性能優化須要根據您應用的實際狀況來有選擇性地挑選參數及配製值,沒有徹底絕對的最優方案,最優方案是基於您對 JVM 數據存儲方式及本身代碼的瞭解程度來做出的最佳選擇。