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