摘要:作為本系列的第一章就從內(nèi)存模型開始說起。這這塊內(nèi)存區(qū)域有可能發(fā)生兩種異常。新生代的一塊內(nèi)存空間,它是新小對象出生的地方,當(dāng)沒有足夠的空間進行分配的時候,發(fā)生一次。
一、前言
手上的這本《深入理解Java虛擬機》這本書買來已接近2年,期間也是看看停停,現(xiàn)如今也才只看到前10章(來回倒騰的看)。寫這個專題的目的:1、作一個專題復(fù)習(xí),老話說的好:好記性不如爛筆頭,正好也可以把自己的一些理解記錄;2、我買的這本書大部分是基于1.6、1.7的,而現(xiàn)在都java11了,一些內(nèi)容做了改變,但我還是以JDK8作為講解(畢竟高版本我也不太熟)。
作為本系列的第一章:就從內(nèi)存模型開始說起。
我想大家剛畢業(yè)找工作面試的時候都被問過這種問題:Java的內(nèi)存區(qū)域是如何劃分的?由此可見這塊還是挺重要都。總的來說,Java虛擬機內(nèi)存區(qū)域共分為:程序計數(shù)器、虛擬機棧、本地方法棧、堆、方法區(qū)、直接內(nèi)存、運行時常量池七6塊區(qū)域。下面將會一一講解。
2.1、程序計數(shù)器其實從名字就可以看出來,它是計數(shù)用的,我們在程序中在執(zhí)行if、while、try/catch的時候都是依賴于這個計數(shù)器。要知道Java是多線程編程語言,為了在切換線程的時候程序計數(shù)器能恢復(fù)到正確的位置,每個線程都會維護一個程序計數(shù)器,也就是說:程序計數(shù)器是線程私有的,同時它還是內(nèi)存區(qū)域唯一一個在Java虛擬機規(guī)范中沒有規(guī)范任何OOM情況的區(qū)域。
特點:
線程私有
不會發(fā)生OOM
2.2、虛擬機棧這里之所以稱虛擬機棧是因為后面還有一個本地方法棧。這里的虛擬機棧指的就是我們平時說的堆棧中的棧,在數(shù)據(jù)結(jié)構(gòu)中我們知道棧的特點是先進后出的,虛擬機棧描述的Java方法的執(zhí)行模型。這里我舉一個例子(為了簡單,這里就用js舉例):
function a(){ b(); } function b(){ c(); } function c(){ } a();
從上面可以看出:a調(diào)用b,b調(diào)用c。執(zhí)行開始的順序是:a>b>c。執(zhí)行結(jié)束的順序是:c>b>a。正好符合棧的特性。
在我們調(diào)用一個Java方法的時候:每個方法都會創(chuàng)建一個棧幀(Stack Frame)。這里的棧幀你就把它理解成C語言的結(jié)構(gòu)體,只是一個數(shù)據(jù)結(jié)構(gòu)而已。它存放的是:*局部變量表、操作數(shù)棧、動態(tài)鏈接等。
這里又多了4個名詞,下面分別對這三個名詞作解釋。
a、局部變量表
由名字可以知道它存放的是變量:局部變量和方法參數(shù),它存放于方法的Code屬性的max_locals數(shù)據(jù)項。至于這個Code屬性是什么,后續(xù)會有專門的文章介紹。我們需要知道的是:系統(tǒng)不會為局部變量賦予初始值(實例變量和類變量都會被賦予初始值)
b、操作數(shù)棧
Java虛擬機的解釋執(zhí)行引擎被稱為"基于棧的執(zhí)行引擎",其中所指的棧就是指-操作數(shù)棧。
操作數(shù)棧是一個基于字節(jié)的數(shù)組,但是它不是基于數(shù)組的角標(biāo)來索引,而是通過壓棧和出棧來訪問,這里舉一個小例子:
// int a = 1 ; b = 2; c = a + b ; iload_0 // 將局部變量表中索引為0的操作數(shù)壓入棧 iload_1 // 將局部變量表中索引為1的操作數(shù)壓入棧 iadd // 將相加結(jié)果壓入棧 istore_2 // 從操作數(shù)棧中彈出結(jié)果然后放入局部變量表中索引為2的位置
動態(tài)鏈接
這個我會有一個后續(xù)將會有一篇文章來介紹。
這這塊內(nèi)存區(qū)域有可能發(fā)生兩種異常:StackOverflowError、OOM。這兩種異常都很好演示:
// StackOverflowError異常 function a(){ a(); } a(); // 演示OOM的話,則最好設(shè)置下堆內(nèi)存 //-Xms=10M -Xmx=10M function a(){ int[] = new int[1024*10]; a(); }
特點:
線程私有,生命周期和線程相同
拋SOE和OOM兩種異常
2.3、本地方法棧本地方法棧和虛擬機方法棧很類似,區(qū)別就是虛擬機為的是Java方法服務(wù),而本地方法棧則為虛擬機使用的Native服務(wù)。這里就涉及了一個概念:本地方法。那什么是本地方法呢?
簡單地講,一個Native Method就是一個java調(diào)用非java代碼的接口
這個非Java代碼的接口可能是c,也有可能是c++。更多關(guān)于本地方法的內(nèi)容就不過多展開。
2.4、堆對于大部分應(yīng)用來說,Java堆是虛擬機管理內(nèi)存中最大的一塊,它存放的內(nèi)容是對象實例。根據(jù)Java虛擬機規(guī)范:絕大多數(shù)對象實例以及數(shù)組都都在堆上分配。(Class對象除外,它是存放在方法區(qū))堆是垃圾回收器管理的主要區(qū)域,我們知道現(xiàn)代收集器是基于分代收集算法,因此我們可以對Java對進行劃分:新生代、老年代。然后對新生代可以再劃分:Eden空間、From Survivor、To Survivor空間。下面對這幾塊內(nèi)存空間作介紹。
Eden
新生代的一塊內(nèi)存空間,它是新小對象“出生”的地方,當(dāng)Eden沒有足夠的空間進行分配的時候,發(fā)生一次Minor GC。
From、To
Survivor之所以會劃分兩塊區(qū)域,是由于新生代的回收算法決定的。From、To這個不是固定的,而且To區(qū)域永遠是空的,Eden:Survior它們的默認比例是8:1,也就是說新生代的可用內(nèi)存大小是8+1=9。當(dāng)對象從Eden進入到From之后它的年齡設(shè)置為1,每熬過一次Minor GC,那它的年齡就+1,當(dāng)年齡到達一定的值(默認15)就會進入到老年代。
注意: 虛擬機并并不是永遠要求對象的年齡達到我們設(shè)置的值或者默認值15才能進入到老年代,如果Survivor中相同年齡所有對象大小總和大于Survivor空間的一半,那么年齡大于或者等于該年齡對象的就可以直接進入到老年代。
老年代
老年代存放的是長期存活的對象和大對象,這里的大對象可能是大字符串和大數(shù)組。
MinorGC
將已經(jīng)死亡的對象消除,將依然存活的對象移動到From空間
當(dāng)From空間已滿的時候,將已經(jīng)死亡的對象消除,將依然存活的對象移動到From空間;此后Eden執(zhí)行MinorGC時將依然存活的對象移動到To空間(From和To對調(diào))
當(dāng)執(zhí)行了n次對象還未死亡,將會進入到老年代。
Minor GC:發(fā)生在新生代的垃圾回收動作,因為大多數(shù)Java對象都是朝生夕死,因此這次回收會很頻繁,速度也會很快。2.5、方法區(qū)
Major GC(Full GC):老年代的垃圾回收動作,Full GC的速度一般比Minor GC慢10倍以上。
永久代
其實一開始我一直理不清方法區(qū)和永久代之間的概念,最近才整明白。方法區(qū)是Java虛擬機規(guī)范的叫法,而永久代是Hotspot的叫法,我們可以將永久代當(dāng)成對方法區(qū)的一種實現(xiàn),而且Java8已經(jīng)去永久代。永久代是一片連續(xù)的堆空間,可以通過-XX:MaxPermSize來設(shè)置。永久代的垃圾回收是和老年代捆綁在一起的,因此不論那個滿了都會觸發(fā)兩者的垃圾回收。
JDK1.7中,存儲在永久代的部分數(shù)據(jù)就已經(jīng)轉(zhuǎn)移到了Java Heap或者是 Native Heap,譬如符號引用(Symbols)轉(zhuǎn)移到了native heap;字面量(interned strings)轉(zhuǎn)移到了java heap;類的靜態(tài)變量(class statics)轉(zhuǎn)移到了java heap。
元數(shù)據(jù)
元數(shù)據(jù)是jdk8出來的,它和永久代類似,最大的區(qū)別是元空間并不在虛擬機中,而是使用本地內(nèi)存。
-XX:MetaspaceSize // 初始空間大小 -XX:MaxMetaspaceSize // 最大空間
至于為什么要作永久代到元數(shù)據(jù)之間的轉(zhuǎn)換,我想主要有兩個原因:
字符串存放到永久代代容易出現(xiàn)性能問題和內(nèi)存溢出
為了JRocket和HotSpot的合并
2.6、運行時常量池運行時常量池,屬于方法區(qū)的一部分,用于存放編譯期生成的各種字面量和符號引用。對于運行時常量池,不同產(chǎn)商有不同實現(xiàn)。還有兩個個類似的名詞叫:字符串常量池、class文件常量池,下面來分別介紹這三者。
字符串常量池
符串池里的內(nèi)容是在類加載完成,經(jīng)過驗證,準(zhǔn)備階段之后在堆中生成字符串對象實例,然后將該字符串對象實例的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的實例對象,具體的實例對象是在堆中開辟的一塊空間存放的)
在HotSpot VM里實現(xiàn)的string pool功能的是一個HashTable類,它是一個哈希表,里面存的是駐留字符串(也就是我們常說的用雙引號括起來的)的引用(而不是駐留字符串實例本身),也就是說在堆中的某些字符串實例被這個StringTable引用之后就等同被賦予了”駐留字符串”的身份。這個StringTable在每個HotSpot VM的實例只有一份,被所有的類共享。
class文件常量池
我們知道class類結(jié)構(gòu)最前面除了魔數(shù)、主次版本號之后就是常量池了,這個常量池就是我們說的class文件常量池,它存放的是我們編譯生成的各種字面量和符號引用。
符號引用:符號引用是一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)即可(它與直接引用區(qū)分一下,直接引用一般是指向方法區(qū)的本地指針,相對偏移量或是一個能間接定位到目標(biāo)的句柄)。一般包括下面三類常量:類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符。
運行時常量池
當(dāng)java文件被編譯成class文件之后,也就是會生成我上面所說的class常量池。當(dāng)jvm在執(zhí)行某個類的時候,必須經(jīng)過加載、連接、初始化,而連接又包括驗證、準(zhǔn)備、解析三個階段。而當(dāng)類加載到內(nèi)存中后,jvm就會將class常量池中的內(nèi)容存放到運行時常量池中,由此可知,運行時常量池也是每個類都有一個。在上面我也說了,class常量池中存的是字面量和符號引用,也就是說他們存的并不是對象的實例,而是對象的符號引用值。而經(jīng)過解析(resolve)之后,也就是把符號引用替換為直接引用,解析的過程會去查詢?nèi)肿址兀簿褪俏覀兩厦嫠f的HashTable,以保證運行時常量池所引用的字符串與全局字符串池中所引用的是一致的。
小結(jié)
1.全局常量池在每個VM中只有一份,存放的是字符串常量的引用值
2.class常量池是在編譯的時候每個class都有的,在編譯階段,存放的是常量的符號引用
3.運行時常量池是在類加載完成之后,將每個class常量池中的符號引用值轉(zhuǎn)存到運行時常量池中,也就是說,每個class都有一個運行時常量池,類在解析之后,將符號引用替換成直接引用,與全局常量池中的引用值保持一致
直接內(nèi)存不屬于虛擬機運行時數(shù)據(jù)區(qū)的一部分,它不是Java虛擬機規(guī)范定義的區(qū)域,在JDK1.4加入了NIO我們可以直接操作堆外內(nèi)存,在RPC通信中我們經(jīng)常會使用到NIO,著名框架Netty就是基于此。直接內(nèi)存不受Java堆的限制,但是收到本機的內(nèi)存限制。
三、總結(jié)本篇主要就JVM的內(nèi)存模型作了介紹,主要介紹了虛擬機棧、堆、常量池,這三個也是我們平時用的比較多的,當(dāng)然也不代表其它不重要。
一些嘮叨:剛畢業(yè)那會也經(jīng)常好奇為什么現(xiàn)在公司都喜歡面試造飛機、工作擰螺絲,現(xiàn)在也逐漸慢慢有體會,因為螺絲絕大部分人都會擰啊。花通樣的薪資肯定人家不愿意就招個擰螺絲的。而且,一些原理性東西對長遠的工作確實有益,反正學(xué)到手總不是壞事。
歡迎關(guān)注公眾號:碼農(nóng)有道
參考《深入理解Java虛擬機》,Java中幾種常量池的區(qū)分
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/74404.html
摘要:作為一個程序員,不了解內(nèi)存模型就不能寫出能夠充分利用內(nèi)存的代碼。程序計數(shù)器是在電腦處理器中的一個寄存器,用來指示電腦下一步要運行的指令序列。在虛擬機中,本地方法棧和虛擬機棧是共用同一塊內(nèi)存的,不做具體區(qū)分。 作為一個 Java 程序員,不了解 Java 內(nèi)存模型就不能寫出能夠充分利用內(nèi)存的代碼。本文通過對 Java 內(nèi)存模型的介紹,讓讀者能夠了解 Java 的內(nèi)存的分配情況,適合 Ja...
摘要:內(nèi)存模型和運行時數(shù)據(jù)區(qū)域的關(guān)系主內(nèi)存對應(yīng)著堆,工作內(nèi)存對應(yīng)著棧。在的單例模式中有運用到二運行時數(shù)據(jù)區(qū)域內(nèi)存區(qū)域因為的運行時數(shù)據(jù)區(qū)域一直在改善,所以不同版本之間會有不同。 一、java內(nèi)存模型 showImg(https://segmentfault.com/img/remote/1460000016694250?w=1810&h=941); java定義內(nèi)存模型的目的是:為了屏蔽各種...
摘要:內(nèi)存模型首先介紹下程序具體執(zhí)行的過程源代碼文件后綴會被編譯器編譯為字節(jié)碼文件后綴由中的類加載器加載各個類的字節(jié)碼文件,加載完畢之后,交由執(zhí)行引擎執(zhí)行在整個程序執(zhí)行過程中,會用一段空間來存儲程序執(zhí)行期間需要用到的數(shù)據(jù)和相關(guān)信息,這段空間一般被 [TOC] JVM內(nèi)存模型 首先介紹下Java程序具體執(zhí)行的過程: Java源代碼文件(.java后綴)會被Java編譯器編譯為字節(jié)碼文件(....
摘要:的內(nèi)存模型概述虛擬機在執(zhí)行程序的過程中,會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域。程序計數(shù)器這是一塊較小的內(nèi)存,它可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。 JVM的內(nèi)存模型 概述 Java虛擬機在執(zhí)行java程序的過程中,會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域。這些區(qū)域都有各自的用途,以及創(chuàng)建和銷毀的時間,有的區(qū)域隨著虛擬機進程的啟動而存在,有些區(qū)域則依賴用戶線程的啟動...
摘要:是描述方法執(zhí)行的內(nèi)存模型每個方法執(zhí)行的時候會同時創(chuàng)建一個棧幀,用于存儲局部變量表操作數(shù)棧動態(tài)連接返回地址方法出口等信息。虛擬機是使用局部變量表完成參數(shù)值到參數(shù)變量表的傳遞過程。堆內(nèi)存管理最大的一塊。 showImg(https://segmentfault.com/img/bVLqsv?w=475&h=398); 1. 虛擬機棧 VM Stack 線程私有,生命周期與線程相同。VM S...
閱讀 638·2021-11-25 09:43
閱讀 1906·2021-11-17 09:33
閱讀 824·2021-09-07 09:58
閱讀 2062·2021-08-16 10:52
閱讀 482·2019-08-30 15:52
閱讀 1722·2019-08-30 15:43
閱讀 971·2019-08-30 15:43
閱讀 2922·2019-08-29 16:41