深入理解Java虛擬機:Java內(nèi)存區(qū)域透徹分析

前言

Java是目前用戶最多、使用范圍最廣的軟件開發(fā)技術(shù),Java 的技術(shù)體系主要由支撐Java程序運行的虛擬機。為各開發(fā)領(lǐng)域提供接口支持的Java API, Java編程語言及許許多多的第三方Java框架( 如Spring和Struts等)構(gòu)成。在國內(nèi),有關(guān)Java API、Java 語言及第三方框架的技術(shù)資料和書籍非常豐富,相比之下,有關(guān)Java虛擬機的資料卻顯得異常貧乏。

創(chuàng)新互聯(lián)公司主營嘉峪關(guān)網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營網(wǎng)站建設(shè)方案,成都App定制開發(fā),嘉峪關(guān)h5微信小程序搭建,嘉峪關(guān)網(wǎng)站營銷推廣歡迎嘉峪關(guān)等地區(qū)企業(yè)咨詢

這種狀況很大程度上是由Java開發(fā)技術(shù)本身的一個重要優(yōu)點導(dǎo)致的:

在虛擬機層面隱藏了底層技術(shù)的復(fù)雜性以及機器與操作系統(tǒng)的差異性。運行程序的物理機器情況千差萬別,而Java虛擬機則在千差萬別的物理機上面建立了統(tǒng)一的運行平臺,實現(xiàn)了在任意一臺虛擬機上編譯的程序都能在任何一臺虛擬機上正常運行。這一極大的優(yōu)勢使得Java應(yīng)用的開發(fā)比傳統(tǒng)C/C++應(yīng)用的開發(fā)更高效和快捷,程序員可以把主要精力集中在具體業(yè)務(wù)邏輯上,而不是物理硬件的兼容性上。

一般情況下,一個程序員只要了解了必要的Java API, Java語法井學(xué)習(xí)適當(dāng)?shù)牡谌介_發(fā)框架,就已經(jīng)基本能滿足日常開發(fā)的需要了,虛擬機會在用戶不知不覺中完成對硬件平臺的兼容以及對內(nèi)存等資源的管理工作。因此,了解虛擬機的運作并不是一般開發(fā)人員必須掌握的知識。然而,凡事都具備兩面性。隨著Java技術(shù)的不斷發(fā)展,它被應(yīng)用于越來越多的領(lǐng)域之中。其中一些領(lǐng)域,如電力、金融、通信等,對程序的性能、穩(wěn)定性和可擴展性方面都有極高的要求。

一個程序很可能在10個人同時使用時完全正常,但是在10000個人同時使用時就會變慢、死鎖甚至崩潰。毫無疑問,要滿足10000個人同時使用需要更高性能的物理硬件,但是在絕大多數(shù)情況下,提升硬件效能無法等比例地提升程序的性能和并發(fā)能力,有時甚至可能對程序的性能沒有任何改善作用。

這里面有Java虛擬機的原因:為了達到為所有硬件提供一致的虛擬平臺的目的,犧牲了一些硬件相關(guān)的性能特性。

更重要的是人為原因:開發(fā)人員如果不了解虛擬機的一些技術(shù)特性的運行原理,就無法寫出最適合虛擬機運行和可自優(yōu)化的代碼。

其實,目前商用的高性能Java虛擬機都提供了相當(dāng)多的優(yōu)化特性和調(diào)節(jié)手段,用于滿足應(yīng)用程序在實際生產(chǎn)環(huán)境中對性能和穩(wěn)定性的要求。如果只是為了入門學(xué)習(xí),讓程序在自己的機器上正常運行,那么這些特性可以說是可有可無的;如果用于生產(chǎn)環(huán)境,尤其是企業(yè)級應(yīng)用開發(fā)中,就迫切需要開發(fā)人員中至少有一部分人對虛擬機的特性及調(diào)節(jié)方法具有很清晰的認識,所以在Java開發(fā)體系中,對架構(gòu)師、系統(tǒng)調(diào)優(yōu)師、高級程序員等角色的需求一直都非常大。

關(guān)于JVM

JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用于計算設(shè)備的規(guī)范,它是一個虛構(gòu)出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現(xiàn)的。

引入Java語言虛擬機后,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用Java虛擬機屏蔽了與具體平臺相關(guān)的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標(biāo)代碼(字節(jié)碼),就可以在多種平臺上不加修改地運行。

Java內(nèi)存區(qū)域透徹分析

這篇文章主要介紹Java內(nèi)存區(qū)域,也是作為Java虛擬機的一些最基本的知識,理解了這些知識之后,才能更好的進行Jvm調(diào)優(yōu)或者更加深入的學(xué)習(xí),本來這些知識是晦澀難懂的,所以希望能夠講解的透徹且形象。

運行時數(shù)據(jù)區(qū)域

JVM載執(zhí)行Java程序的過程中會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域。

Java 虛擬機所管理的內(nèi)存一共分為Method Area(方法區(qū))、VM Stack(虛擬機棧)、Native Method Stack(本地方法棧)、Heap(堆)、Program Counter Register(程序計數(shù)器)五個區(qū)域。

這些區(qū)域都有各自的用途,以及創(chuàng)建和銷毀的時間,有的區(qū)域隨著虛擬機進程的啟動而存在,有些區(qū)域則是依賴用戶線程的啟動和結(jié)束而建立和銷毀。具體如下圖所示:

深入理解 Java 虛擬機:Java 內(nèi)存區(qū)域透徹分析

上圖介紹的是JDK1.8 JVM運行時內(nèi)存數(shù)據(jù)區(qū)域劃分。1.8同1.7比,最大的差別就是:元數(shù)據(jù)區(qū)取代了永久代。元空間的本質(zhì)和永久代類似,都是對JVM規(guī)范中方法區(qū)的實現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元數(shù)據(jù)空間并不在虛擬機中,而是使用本地內(nèi)存。

程序計數(shù)器(Program Counter Register)

程序計數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。在虛擬機概念模型中,字節(jié)碼解釋器工作時就是通過改變計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成。

程序計數(shù)器是一塊?“線程私有”?的內(nèi)存,每條線程都有一個獨立的程序計數(shù)器,能夠?qū)⑶袚Q后的線程恢復(fù)到正確的執(zhí)行位置。

  • 執(zhí)行的是一個Java方法

計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址。

  • 執(zhí)行的是Native方法

計數(shù)器為空(Undefined),因為native方法是java通過JNI直接調(diào)用本地C/C++庫,可以近似的認為native方法相當(dāng)于C/C++暴露給java的一個接口,java通過調(diào)用這個接口從而調(diào)用到C/C++方法。由于該方法是通過C/C++而不是java進行實現(xiàn)。那么自然無法產(chǎn)生相應(yīng)的字節(jié)碼,并且C/C++執(zhí)行時的內(nèi)存分配是由自己語言決定的,而不是由JVM決定的。

  • 程序計數(shù)器也是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的內(nèi)存區(qū)域。

其實,我感覺這塊區(qū)域,作為我們開發(fā)人員來說是不能過多的干預(yù)的,我們只需要了解有這個區(qū)域的存在就可以,并且也沒有虛擬機相應(yīng)的參數(shù)可以進行設(shè)置及控制。

Java虛擬機棧(Java Virtual Machine Stacks)

深入理解 Java 虛擬機:Java 內(nèi)存區(qū)域透徹分析

Java虛擬機棧(Java Virtual Machine Stacks)描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame),從上圖中可以看出,棧幀中存儲著局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每一個方法從調(diào)用直至執(zhí)行完成的過程,會對應(yīng)一個棧幀在虛擬機棧中入棧到出棧的過程。

與程序計數(shù)器一樣,Java虛擬機棧也是線程私有的。

而局部變量表中存放了編譯期可知的各種:

  • 基本數(shù)據(jù)類型(boolen、byte、char、short、int、 float、 long、double)

  • 對象引用(reference類型,它不等于對象本身,可能是一個指向?qū)ο笃鹗嫉刂返闹羔槪部赡苁侵赶蛞粋€代表對象的句柄或其他與此對象相關(guān)的位置)

  • returnAddress類型(指向了一條字節(jié)碼指令的地址)

其中64位長度的long和double類型的數(shù)據(jù)會占用2個局部變量空間(Slot),其余數(shù)據(jù)類型只占用1個。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

Java虛擬機規(guī)范中對這個區(qū)域規(guī)定了兩種異常狀況:

  • StackOverflowError:線程請求的棧深度大于虛擬機所允許的深度,將會拋出此異常。

  • OutOfMemoryError:當(dāng)可動態(tài)擴展的虛擬機棧在擴展時無法申請到足夠的內(nèi)存,就會拋出該異常。

一直覺得上面的概念性的知識還是比較抽象的,下面我們通過JVM參數(shù)的方式來控制棧的內(nèi)存容量,模擬StackOverflowError異?,F(xiàn)象。

本地方法棧(Native Method Stack)

本地方法棧(Native Method Stack)?與Java虛擬機棧作用很相似,它們的區(qū)別在于虛擬機棧為虛擬機執(zhí)行Java方法(即字節(jié)碼)服務(wù),而本地方法棧則為虛擬機使用到的Native方法服務(wù)。

在虛擬機規(guī)范中對本地方法棧中使用的語言、方式和數(shù)據(jù)結(jié)構(gòu)并無強制規(guī)定,因此具體的虛擬機可實現(xiàn)它。甚至有的虛擬機(Sun HotSpot虛擬機)直接把本地方法棧和虛擬機棧合二為一。與虛擬機一樣,本地方法棧會拋出StackOverflowError和OutOfMemoryError異常。

  • 使用-Xss參數(shù)減少棧內(nèi)存容量(更多的JVM參數(shù)可以參考這篇文章:深入理解Java虛擬機-常用vm參數(shù)分析)

這個例子中,我們將棧內(nèi)存的容量設(shè)置為256K(默認1M),并且再定義一個變量查看棧遞歸的深度。

?/**
?*?@ClassName?Test_02
?*?@Description?設(shè)置Jvm參數(shù):-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();
2?}?catch?(Throwable?e)?{
23?e.printStackTrace();
24?}
25?}
26}

運行時設(shè)置JVM參數(shù)

深入理解 Java 虛擬機:Java 內(nèi)存區(qū)域透徹分析

輸出結(jié)果:

深入理解 Java 虛擬機:Java 內(nèi)存區(qū)域透徹分析

?Java堆(Heap)

對于大多數(shù)應(yīng)用而言,Java堆(Heap)是Java虛擬機所管理的內(nèi)存中最大的一塊,它被所有線程共享的,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域唯一的目的是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存,且每次分配的空間是不定長的。在Heap 中分配一定的內(nèi)存來保存對象實例,實際上只是保存對象實例的屬性值,屬性的類型和對象本身的類型標(biāo)記等,并不保存對象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的內(nèi)存保存對象實例和對象的序列化比較類似。

Java堆是垃圾收集器管理的主要區(qū)域,因此也被稱為?“GC堆(Garbage Collected Heap)”?。從內(nèi)存回收的角度看內(nèi)存空間可如下劃分:

深入理解 Java 虛擬機:Java 內(nèi)存區(qū)域透徹分析

圖片摘自https://blog.csdn.net/bruce128/article/details/79357870

  • 新生代(Young):新生成的對象優(yōu)先存放在新生代中,新生代對象朝生夕死,存活率很低。在新生代中,常規(guī)應(yīng)用進行一次垃圾收集一般可以回收70% ~ 95% 的空間,回收效率很高。

如果把新生代再分的細致一點,新生代又可細分為Eden空間、From Survivor空間、To Survivor空間,默認比例為8:1:1。

  • 老年代(Tenured/Old):在新生代中經(jīng)歷了多次(具體看虛擬機配置的閥值)GC后仍然存活下來的對象會進入老年代中。老年代中的對象生命周期較長,存活率比較高,在老年代中進行GC的頻率相對而言較低,而且回收的速度也比較慢。

  • 永久代(Perm):永久代存儲類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù),對這一區(qū)域而言,Java虛擬機規(guī)范指出可以不進行垃圾收集,一般而言不會進行垃圾回收。

其中新生代和老年代組成了Java堆的全部內(nèi)存區(qū)域,而永久代不屬于堆空間,它在JDK 1.8以前被Sun HotSpot虛擬機用作方法區(qū)的實現(xiàn)

另外,再強調(diào)一下堆空間內(nèi)存分配的大體情況,這對于后面一些Jvm優(yōu)化的技巧還是有幫助的。

  • 老年代 :三分之二的堆空間

  • 年輕代 :三分之一的堆空間
    eden區(qū):8/10 的年輕代空間
    survivor0 : 1/10 的年輕代空間
    survivor1 : 1/10 的年輕代空間

最后,我們再通過一個簡單的例子更加形象化的展示一下堆溢出的情況。

  • JVM參數(shù)設(shè)置:-Xms10m -Xmx10m

這里將堆的最小值和最大值都設(shè)置為10m,如果不了解這些參數(shù)的含義,可以參考這篇文章:深入理解Java虛擬機-常用vm參數(shù)分析

?/**
?*?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());
?}
?}
}

輸出結(jié)果:

深入理解 Java 虛擬機:Java 內(nèi)存區(qū)域透徹分析

圖中出現(xiàn)了java.lang.OutOfMemoryError,并且提示了Java heap space,這就說明是Java堆內(nèi)存溢出的情況。

堆的Dump文件分析

我的使用的是VisualVM工具進行分析,關(guān)于如何使用這個工具查看這篇文章(深入理解Java虛擬機-如何利用VisualVM對高并發(fā)項目進行性能分析 )。在運行程序之后,會同時打開VisualVM工具,查看堆內(nèi)存的變化情況。

深入理解 Java 虛擬機:Java 內(nèi)存區(qū)域透徹分析

在上圖中,可以看到,堆的最大值是30m,但是使用的堆的容量也快接近30m了,所以很容易發(fā)生堆內(nèi)存溢出的情況。

接著查看dump文件。

深入理解 Java 虛擬機:Java 內(nèi)存區(qū)域透徹分析

如上圖,堆中的大部分的對象都是HeapObject,所以,就是因為這個對象的一直產(chǎn)生,所以導(dǎo)致堆內(nèi)存不夠分配,所以出現(xiàn)內(nèi)存溢出。

我們再看GC情況。

深入理解 Java 虛擬機:Java 內(nèi)存區(qū)域透徹分析

如上圖,Eden新生代總共48次minor gc,耗時1.168s,基本滿足要求,但是survivor卻沒有,這不正常,同時Old Gen老年代總共27次full gc,耗時4.266s,耗時長,gc多,這正是因為大量的大對象進入到老年代導(dǎo)致的,所以,導(dǎo)致full gc頻繁。

方法區(qū)(Method Area)

方法區(qū)(Method Area)?與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域。它用于存儲一杯虛擬機加載的類信息、常量、靜態(tài)變量、及時編譯器編譯后的代碼等數(shù)據(jù)。正因為方法區(qū)所存儲的數(shù)據(jù)與堆有一種類比關(guān)系,所以它還被稱為?Non-Heap。

運行時常量池(Runtime Constant Pool)

運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池存放。

Java虛擬機對Class文件每一部分(自然包括常量池)的格式有嚴格規(guī)定,每一個字節(jié)用于存儲那種數(shù)據(jù)都必須符合規(guī)范上的要求才會被虛擬機認可、裝載和執(zhí)行。但對于運行時常量池,Java虛擬機規(guī)范沒有做任何有關(guān)細節(jié)的要求,不同的提供商實現(xiàn)的虛擬機可以按照自己的需求來實現(xiàn)此內(nèi)存區(qū)域。不過一般而言,除了保存Class文件中的描述符號引用外,還會把翻譯出的直接引用也存儲在運行時常量池中。

運行時常量池相對于Class文件常量池的另外一個重要特征是具備動態(tài)性,Java語言并不要求常量一定只有編譯器才能產(chǎn)生,也就是并非置入Class文件中的常量池的內(nèi)容才能進入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中。

運行時常量池舉例

上面的動態(tài)性在開發(fā)中用的比較多的便是String類的intern() 方法。所以,我們以intern() 方法舉例,講解一下運行時常量池。

String.intern()是一個native方法,作用是:如果字符串常量池中已經(jīng)包含有一個等于此String對象的字符串,則直接返回池中的字符串;否則,加入到池中,并返回。

?/**
?*?@ClassName?MethodTest
?*?@Description?vm參數(shù)設(shè)置:-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參數(shù)介紹:

-Xms512m -Xmx512m -Xmn128m -XX:PermSize=10M -XX:MaxPermSize=10M -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:-HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
開始堆內(nèi)存和最大堆內(nèi)存都是512m,永久代大小10m,新生代和老年代1:4,E:S1:S2=8:1:1,最大經(jīng)過15次survivor進入老年代,使用的,垃圾收集器是新生代ParNew,老年代CMS。

通過這樣的設(shè)置之后,查看運行結(jié)果:

深入理解 Java 虛擬機:Java 內(nèi)存區(qū)域透徹分析

首先堆內(nèi)存耗完,然后看看GC情況,設(shè)置這些參數(shù)之后,GC情況應(yīng)該會不錯,拭目以待。

深入理解 Java 虛擬機:Java 內(nèi)存區(qū)域透徹分析

上圖是GC情況,我們可以看到新生代?21 次minor gc,用了1.179秒,平均不到50ms一次,性能不錯,老年代?117 次full gc,用了45.308s,平均一次不到1s,性能也不錯,說明jvm運行是不錯的。

注意:?在JDK1.6及以前的版本中運行以上代碼,因為我們通過-XX:PermSize=10M -XX:MaxPermSize=10M設(shè)置了方法區(qū)的大小,所以也就是設(shè)置了常量池的容量,所以運行之后,會報錯:java.lang.OutOfMemoryError:PermGen space,這說明常量池溢出;在JDK1.7及以后的版本中,將會一直運行下去,不會報錯,在前面也說到,JDK1.7及以后,去掉了永久代。

直接內(nèi)存

直接內(nèi)存(Direct Memory)并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機規(guī)范中定義的內(nèi)存區(qū)域。但這部分內(nèi)存也被頻繁運用,而卻可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。

這個我們實際中主要接觸到的就是NIO,在NIO中,我們?yōu)榱四軌蚣涌霫O操作,采用了一種直接內(nèi)存的方式,使得相比于傳統(tǒng)的IO快了很多。

在NIO引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作。這樣能避免在Java堆和Native堆中來回復(fù)制數(shù)據(jù),在一些場景里顯著提高性能。

在配置虛擬機參數(shù)時,會根據(jù)實際內(nèi)存設(shè)置-Xmx等參數(shù)信息,但經(jīng)常忽略直接內(nèi)存,使得各個內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)的限制),從而導(dǎo)致動態(tài)擴展時出現(xiàn)OutOfMemoryError異常。

文章名稱:深入理解Java虛擬機:Java內(nèi)存區(qū)域透徹分析
標(biāo)題來源:http://muchs.cn/article0/pdgeoo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供App開發(fā)、外貿(mào)網(wǎng)站建設(shè)移動網(wǎng)站建設(shè)、靜態(tài)網(wǎng)站、響應(yīng)式網(wǎng)站

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

網(wǎng)站托管運營