JVM虛擬機學習二、虛擬機內存模型(JDK1.8)

JVM虛擬機學習二、虛擬機內存模型(JDK1.8)

基於JDK1.8,HostPot虛擬機

JVM運行時內存模型(1.8)

在這裏插入圖片描述

先說說與JDK1.7之前的區別

  1. 沒有了方法區,取而帶之的是元空間(這麼說不標準,方法區只是JVM規範的概念,並不實際存在。)
  2. 原來方法區中的運行時常量池中的字符串常量池,靜態變量,都存在於堆中了。(注意是字符串常量池存放於堆中,並不是運行時常量池!)
  3. 方法區中其餘的(例如類的加載信息)存在於元空間中,運行時元空間在存在於本地內存中的。

堆內存模型改變

jdk1.7之前,堆通常被劃分爲:新生代(Young Generation),老年代(Old Gerneration),永久代(Permannet Gerneration)。

在這裏插入圖片描述

​ jdk1.8之後堆空間將存放元數據的永久代從堆內存中移到了本地內存並改名爲元空間(Metaspace)。元數據是數據的數據或者叫做用來描述數據的數據或者叫做信息的信息。(比如原本方法區存儲的類信息、即時編譯器編譯後的代碼等)

這麼做的原因

  1. 字符串存在永久代中,容易出現性能問題和內存溢出。
  2. 不會再有java.lang.OutOfMemoryError: PermGen問題。
  3. 類及方法的信息等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出。
  4. 永久代會爲 GC 帶來不必要的複雜度,並且回收效率偏低。

在這裏插入圖片描述

永久代和方法區的關係:永久代其實就是方法區的實現,方法區只是JVM規範的一種概念,實際並不存在,本文都是以hostpot虛擬機爲例,不同的虛擬機中方法區的實現在不同位置。

程序計數器(Program Counter Register)

​ 程序計數器(Program Counter Register),它保存的是程序當前執行的指令的地址(也可以說保存下一條指令的所在存儲單元的地址),當CPU需要執行指令時,需要從程序計數器中得到當前需要執行的指令所在存儲單元的地址,然後根據得到的地址獲取到指令,在得到指令之後,程序計數器便自動加1或者根據轉移指針得到下一條指令的地址,如此循環,直至執行完所有的指令。

​ 由於在JVM中,多線程是通過線程輪流切換來獲得CPU執行時間的,因此,在任一具體時刻,一個CPU的內核只會執行一條線程中的指令,因此,爲了能夠使得每個線程都在線程切換後能夠恢復在切換之前的程序執行位置,每個線程都需要有自己獨立的程序計數器,並且不能互相被幹擾,否則就會影響到程序的正常執行次序。因此,可以這麼說,程序計數器是每個線程所私有的。

在JVM規範中規定,如果線程執行的是非native方法,則程序計數器中保存的是當前需要執行的指令的地址;如果線程執行的是native方法,則程序計數器中的值是undefined。由於程序計數器中存儲的數據所佔空間的大小不會隨程序的執行而發生改變,因此,對於程序計數器是不會發生內存溢出現象(OutOfMemory)的

虛擬機棧(Vitual Machine Stack)

​ Java棧也稱作虛擬機棧(Java Vitual Machine Stack)事實上,Java棧是Java方法執行的內存模型。爲什麼這麼說呢?下面就來解釋一下其中的原因。

​ 虛擬機中存放的是一個個棧幀,每一個棧幀對應着一個方法。棧幀包含有:局部變量表操作數棧動態鏈接方法的返回地址。**當程序的執行指令執行一個方法時,就會在虛擬機棧中創建一個對應的棧幀,並將其壓入棧中,當方法執行完畢棧幀出棧。**因此可知,線程當前執行的方法所對應的棧幀必定位於Java棧的頂部。講到這裏,大家就應該會明白爲什麼 在 使用 遞歸方法的時候容易導致棧內存溢出的現象了以及爲什麼棧區的空間不用程序員去管理了(當然在Java中,程序員基本不用關係到內存分配和釋放的事情,因爲Java有自己的垃圾回收機制),這部分空間的分配和釋放都是由系統自動實施的。下圖是虛擬機棧的模型:

在這裏插入圖片描述

局部變量表:根據名字就知道其含義:方法中的局部變量存放的位置(包括聲明的非靜態變量和方法的形參)。對於基本數據類型的變量,則直接存儲它的值,對於引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就可以確定其大小了,因此在程序執行期間局部變量表的大小是不會改變的

操作數棧:想棧最典型的一個應用就是用來對表達式求值。想想一個線程執行方法的過程中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。因此可以這麼說,程序中的所有計算過程都是在藉助於操作數棧來完成的

動態鏈接:當方法中需要用到常量池中的常量時,該如何引用?這塊區域就是用來存放常量池中常量的引用

方法返回地址:當一個方法執行完畢之後,要返回之前調用它的地方,因此在棧幀中必須保存一個方法返回地址。

本地方法棧

​ 本地方法棧與Java棧的作用和原理非常相似。區別只不過是Java棧是爲執行Java方法服務的,而本地方法棧則是爲執行本地方法(Native Method)服務的。在JVM規範中,並沒有對本地方發展的具體實現方法以及數據結構作強制規定,虛擬機可以自由實現它。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二爲一

堆(Heap)

  1. JVM中所管理內存中的最大的一塊。在虛擬機啓動時被創建。
  2. 唯一的目的是存放對象實例,幾乎所有的對象實例和數組都是在這裏分配內存。
  3. 堆是垃圾收集管理的主要區域,所以也會被稱爲"GC"。
  4. 原來方法區中的運行時常量池中的字符串常量池,靜態變量,都存在於堆中了

​ Java中的堆是用來存儲對象本身的以及數組(當然,數組引用是存放在Java棧中的)。只不過和C語言中的不同,在Java中,程序員基本不用去關心空間釋放的問題,Java的垃圾回收機制會自動進行處理。因此這部分空間也是Java垃圾收集器管理的主要區域。另外,堆是被所有線程共享的,在JVM中只有一個堆。

方法區(jdk1.8/hostpot)

​ 首先關注一個各個版本的方法區中的一些改變:

  1. 在JDK1.7之前運行時常量池邏輯包含字符串常量池存放在方法區, 此時hotspot虛擬機對方法區的實現爲永久代
  2. 在JDK1.7 字符串常量池被從方法區拿到了堆中, 這裏沒有提到運行時常量池,也就是說字符串常量池被單獨拿到堆,運行時常量池剩下的東西還在方法區, 也就是hotspot中的永久代
  3. 在JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字符串常量池還在堆, 運行時常量池還在方法區, 只不過方法區的實現從永久代變成了元空間(Metaspace)

​ 方法區在JVM中也是一個非常重要的區域,它與堆一樣,是被線程共享的區域。在方法區中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯後的代碼等。

運行時常量池、字符串常量池、靜態常量池

1.靜態常量池(class文件常量池)

​ 我們都知道class文件中除了包含類的版本、字段、方法、接口等信息,還包含有常量池:用於存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)

字面量:就是我們所說的常量概念,如文本字符串、被聲明爲final的常量值等。

符號引用:是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可(它與直接引用區分一下,直接引用一般是指向方法區的本地指針,相對偏移量或是一個能間接定位到目標的句柄)。

2.字符串常量池(String pool)

​ 全局字符串池裏的內容是在類加載完成,經過驗證,準備階段之後在堆中生成字符串對象實例,然後將該字符串對象實例的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的實例對象,具體的實例對象是在堆中開闢的一塊空間存放的。)。
​ 在HotSpot VM裏實現的string pool功能的是一個StringTable類,它是一個哈希表,裏面存的是駐留字符串(也就是我們常說的用雙引號括起來的)的引用(而不是駐留字符串實例本身),也就是說在堆中的某些字符串實例被這個StringTable引用之後就等同被賦予了”駐留字符串”的身份。這個StringTable在每個HotSpot VM的實例只有一份,被所有的類共享。

3.運行時常量池(runtime constant pool)

​ 當java文件被編譯成class文件之後,也就是會生成我上面所說的class常量池,那麼運行時常量池又是什麼時候產生的呢?

​ jvm在執行某個類的時候,必須經過加載、連接、初始化,而連接又包括驗證、準備、解析三個階段。而當類加載到內存中後,jvm就會將class常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每個類都有一個。在上面我也說了,class常量池中存的是字面量和符號引用,也就是說他們存的並不是對象的實例,而是對象的符號引用值。而經過解析(resolve)之後,也就是把符號引用替換爲直接引用,解析的過程會去查詢全局字符串池,也就是我們上面所說的StringTable,以保證運行時常量池所引用的字符串與全局字符串池中所引用的是一致的。運行時常量池邏輯上是包含字符串常量池的

​ 總結:

  • 1.全局常量池在每個VM中只有一份,存放的是字符串常量的引用值。
  • 2.class常量池是在編譯的時候每個class都有的,在編譯階段,存放的是常量的符號引用。
  • 3.運行時常量池是在類加載完成之後,將每個class常量池中的符號引用值轉存到運行時常量池中,也就是說,每個class都有一個運行時常量池,類在解析之後,將符號引用替換成直接引用,與全局常量池中的引用值保持一致。

有關String中的intern()的解讀:https://blog.csdn.net/qq_38238296/article/details/89335678

元空間(metaspace)

​ 在JDK1.8中,永久代已經不存在,存儲的類信息、編譯後的代碼數據等已經移動到了MetaSpace(元空間)中,元空間並沒有處於堆內存上,而是直接佔用的本地內存(NativeMemory)。

元空間的本質和永久代類似,都是對JVM規範中方法區的實現。

不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存

部分轉載自:

http://tangxman.github.io/2015/07/27/the-difference-of-java-string-pool/

http://www.cnblogs.com/dolphin0520/p/3613043.html

https://www.cnblogs.com/cjsblog/p/9850300.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章