您好,登錄后才能下訂單哦!
Java虛擬機深入理解系列全部文章更新中...
這篇文章主要介紹Java內存區域,也是作為Java虛擬機的一些最基本的知識,理解了這些知識之后,才能更好的進行Jvm調優或者更加深入的學習,本來這些知識是晦澀難懂的,所以希望能夠講解的透徹且形象。
JVM載執行Java程序的過程中會把它所管理的內存劃分為若干個不同的數據區域。
Java 虛擬機所管理的內存一共分為Method Area(方法區)、VM Stack(虛擬機棧)、Native Method Stack(本地方法棧)、Heap(堆)、Program Counter Register(程序計數器)五個區域。
這些區域都有各自的用途,以及創建和銷毀的時間,有的區域隨著虛擬機進程的啟動而存在,有些區域則是依賴用戶線程的啟動和結束而建立和銷毀。具體如下圖所示:
cdn.xitu.io/2019/12/10/16eeeba1315b179a?w=593&h=479&f=png&s=18655">
上圖介紹的是JDK1.8 JVM運行時內存數據區域劃分。1.8同1.7比,最大的差別就是:元數據區取代了永久代。元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代之間最大的區別在于:元數據空間并不在虛擬機中,而是使用本地內存。
程序計數器(Program Counter Register)是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。在虛擬機概念模型中,字節碼解釋器工作時就是通過改變計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
程序計數器是一塊 “線程私有” 的內存,每條線程都有一個獨立的程序計數器,能夠將切換后的線程恢復到正確的執行位置。
計數器記錄的是正在執行的虛擬機字節碼指令的地址。
計數器為空(Undefined),因為native方法是java通過JNI直接調用本地C/C++庫,可以近似的認為native方法相當于C/C++暴露給java的一個接口,java通過調用這個接口從而調用到C/C++方法。由于該方法是通過C/C++而不是java進行實現。那么自然無法產生相應的字節碼,并且C/C++執行時的內存分配是由自己語言決定的,而不是由JVM決定的。
其實,我感覺這塊區域,作為我們開發人員來說是不能過多的干預的,我們只需要了解有這個區域的存在就可以,并且也沒有虛擬機相應的參數可以進行設置及控制。
Java虛擬機棧(Java Virtual Machine Stacks)描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame),從上圖中可以看出,棧幀中存儲著局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,會對應一個棧幀在虛擬機棧中入棧到出棧的過程。
與程序計數器一樣,Java虛擬機棧也是線程私有的。
而局部變量表中存放了編譯期可知的各種:
其中64位長度的long和double類型的數據會占用2個局部變量空間(Slot),其余數據類型只占用1個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
Java虛擬機規范中對這個區域規定了兩種異常狀況:
一直覺得上面的概念性的知識還是比較抽象的,下面我們通過JVM參數的方式來控制棧的內存容量,模擬StackOverflowError異常現象。
本地方法棧(Native Method Stack) 與Java虛擬機棧作用很相似,它們的區別在于虛擬機棧為虛擬機執行Java方法(即字節碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。
在虛擬機規范中對本地方法棧中使用的語言、方式和數據結構并無強制規定,因此具體的虛擬機可實現它。甚至有的虛擬機(Sun HotSpot虛擬機)直接把本地方法棧和虛擬機棧合二為一。與虛擬機一樣,本地方法棧會拋出StackOverflowError和OutOfMemoryError異常。
這個例子中,我們將棧內存的容量設置為256K
(默認1M),并且再定義一個變量查看棧遞歸的深度。
/**
* @ClassName Test_02
* @Description 設置Jvm參數:-Xss256k
* @Author 歐陽思海
* @Date 2019/9/30 11:05
* @Version 1.0
**/
public class Test_02 {
private int len = 1;
public void stackTest() {
len++;
System.out.println("stack len:" + len);
stackTest();
}
public static void main(String[] args) {
Test_02 test = new Test_02();
try {
test.stackTest();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
運行時設置JVM參數
輸出結果:
對于大多數應用而言,Java堆(Heap)是Java虛擬機所管理的內存中最大的一塊,它被所有線程共享的,在虛擬機啟動時創建。此內存區域唯一的目的是存放對象實例,幾乎所有的對象實例都在這里分配內存,且每次分配的空間是不定長的。在Heap 中分配一定的內存來保存對象實例,實際上只是保存對象實例的屬性值,屬性的類型和對象本身的類型標記等,并不保存對象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的內存保存對象實例和對象的序列化比較類似。
Java堆是垃圾收集器管理的主要區域,因此也被稱為 “GC堆(Garbage Collected Heap)” 。從內存回收的角度看內存空間可如下劃分:
如果把新生代再分的細致一點,新生代又可細分為Eden空間、From Survivor空間、To Survivor空間,默認比例為8:1:1。
其中新生代和老年代組成了Java堆的全部內存區域,而永久代不屬于堆空間,它在JDK 1.8以前被Sun HotSpot虛擬機用作方法區的實現
另外,再強調一下堆空間內存分配的大體情況,這對于后面一些Jvm優化的技巧還是有幫助的。
最后,我們再通過一個簡單的例子更加形象化的展示一下堆溢出的情況。
這里將堆的最小值和最大值都設置為10m,如果不了解這些參數的含義,可以參考這篇文章:深入理解Java虛擬機-常用vm參數分析
/**
* VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
* @author zzm
*/
public class HeapTest {
static class HeapObject {
}
public static void main(String[] args) {
List<HeapObject> list = new ArrayList<HeapObject>();
//不斷的向堆中添加對象
while (true) {
list.add(new HeapObject());
}
}
}
輸出結果:
圖中出現了java.lang.OutOfMemoryError
,并且提示了Java heap space
,這就說明是Java堆內存溢出的情況。
堆的Dump文件分析
我的使用的是VisualVM工具進行分析,關于如何使用這個工具查看這篇文章(深入理解Java虛擬機-如何利用VisualVM對高并發項目進行性能分析 )。在運行程序之后,會同時打開VisualVM工具,查看堆內存的變化情況。
在上圖中,可以看到,堆的最大值是30m,但是使用的堆的容量也快接近30m了,所以很容易發生堆內存溢出的情況。
接著查看dump文件。
如上圖,堆中的大部分的對象都是HeapObject,所以,就是因為這個對象的一直產生,所以導致堆內存不夠分配,所以出現內存溢出。
我們再看GC情況。
如上圖,Eden新生代總共48次minor gc,耗時1.168s,基本滿足要求,但是survivor卻沒有,這不正常,同時Old Gen老年代總共27次full gc,耗時4.266s,耗時長,gc多,這正是因為大量的大對象進入到老年代導致的,所以,導致full gc頻繁。
方法區(Method Area) 與Java堆一樣,是各個線程共享的內存區域。它用于存儲一杯虛擬機加載
的類信息、常量、靜態變量、及時編譯器編譯后的代碼等數據。正因為方法區所存儲的數據與堆有一種類比關系,所以它還被稱為 Non-Heap。
<big>運行時常量池(Runtime Constant Pool)</big>
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池存放。
Java虛擬機對Class文件每一部分(自然包括常量池)的格式有嚴格規定,每一個字節用于存儲那種數據都必須符合規范上的要求才會被虛擬機認可、裝載和執行。但對于運行時常量池,Java虛擬機規范沒有做任何有關細節的要求,不同的提供商實現的虛擬機可以按照自己的需求來實現此內存區域。不過一般而言,除了保存Class文件中的描述符號引用外,還會把翻譯出的直接引用也存儲在運行時常量池中。
運行時常量池相對于Class文件常量池的另外一個重要特征是具備動態性,Java語言并不要求常量一定只有編譯器才能產生,也就是并非置入Class文件中的常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中。
上面的動態性在開發中用的比較多的便是String類的intern()
方法。所以,我們以intern()
方法舉例,講解一下運行時常量池。
String.intern()
是一個native
方法,作用是:如果字符串常量池中已經包含有一個等于此String對象的字符串,則直接返回池中的字符串;否則,加入到池中,并返回。
/**
* @ClassName MethodTest
* @Description vm參數設置:-Xms512m -Xmx512m -Xmn128m -XX:PermSize=10M -XX:MaxPermSize=10M -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:-HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
* @Author 歐陽思海
* @Date 2019/11/25 20:06
* @Version 1.0
**/
public class MethodTest {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
long i = 0;
while (i < 1000000000) {
System.out.println(i);
list.add(String.valueOf(i++).intern());
}
}
}
vm參數介紹:
-Xms512m -Xmx512m -Xmn128m -XX:PermSize=10M -XX:MaxPermSize=10M -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:-HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
開始堆內存和最大堆內存都是512m,永久代大小10m,新生代和老年代1:4,E:S1:S2=8:1:1,最大經過15次survivor進入老年代,使用的,垃圾收集器是新生代ParNew,老年代CMS。
通過這樣的設置之后,查看運行結果:
首先堆內存耗完,然后看看GC情況,設置這些參數之后,GC情況應該會不錯,拭目以待。
上圖是GC情況,我們可以看到新生代 21 次minor gc,用了1.179秒,平均不到50ms一次,性能不錯,老年代 117 次full gc,用了45.308s,平均一次不到1s,性能也不錯,說明jvm運行是不錯的。
注意: 在JDK1.6及以前的版本中運行以上代碼,因為我們通過
-XX:PermSize=10M -XX:MaxPermSize=10M
設置了方法區的大小,所以也就是設置了常量池的容量,所以運行之后,會報錯:java.lang.OutOfMemoryError:PermGen space
,這說明常量池溢出;在JDK1.7及以后的版本中,將會一直運行下去,不會報錯,在前面也說到,JDK1.7及以后,去掉了永久代。
直接內存(Direct Memory)并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域。但這部分內存也被頻繁運用,而卻可能導致OutOfMemoryError異常出現。
這個我們實際中主要接觸到的就是NIO,在NIO中,我們為了能夠加快IO操作,采用了一種直接內存的方式,使得相比于傳統的IO快了很多。
在NIO引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java堆中的DirectByteBuffer
對象作為這塊內存的引用進行操作。這樣能避免在Java堆和Native堆中來回復制數據,在一些場景里顯著提高性能。
在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但經常忽略直接內存,使得各個內存區域總和大于物理內存限制(包括物理的和操作系統的限制),從而導致動態擴展時出現OutOfMemoryError異常。
1、原創不易,老鐵,文章需要你的點贊讓更多的人看到,希望能夠幫助到大家!
2、文章有不當之處,歡迎指正,如果喜歡微信閱讀,你也可以關注我的微信公眾號:
好好學java
,公眾號已有 6W 粉絲,回復:1024,獲取公眾號的大禮包,公眾號長期發布 Java 優質系列文章,關注我們一定會讓你收獲很多!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。