想理解JVM看了這篇文章,就知道了!(一)

前言

​ 本章節屬於Java進階系列,前面關於設計模式講解完了,有興趣的童鞋能夠翻看以前的博文,後面會講解JVM的優化,整個系列會完整的講解整個java體系與生態相關的中間件知識。本次將對jvm有更深刻的學習,咱們不只要讓程序能跑起來,並且是能夠跑的更快!能夠分析解決在生產環境中所遇到的各類「棘手」的問題,好比運行的應用卡住了,日誌不輸出,程序沒有反應,CPU負載忽然升高,多線程應用下,如何分配線程數量等。java

JVM介紹

什麼是JVM

​ 做爲java工程師,對於jvm確定不陌生。JVM是Java Virtual Machine的縮寫,通俗來講也就是運行java代碼的容器。當項目啓動時,會根據jvm相關配置參數,在計算機的內存中開啓一片空間用於運行JVM。以後java相關代碼就會被加載進JVM中運行。c++

百度百科對JVM的定義:程序員

爲何要了解JVM

​ 對於Java程序員來講,在虛擬機自動內存管理機制的幫助下,再也不須要爲每個new操做去寫配對的delete/free代碼,不容易出現內存泄漏和內存溢出問題,看起來由虛擬機管理內存一切都很美好。不過,也正是由於Java程序員把控制內存的權力交給了Java虛擬機,一旦出現內存泄漏和溢出方面的問題,若是不瞭解虛擬機是怎樣使用內存的,那排查錯誤、修正問題將會成爲一項異常艱難的工做。設計模式

JVM內存模型

JVM總體架構

​ 由上面的圖能夠看出,JVM虛擬機中主要是由三部分構成,分別是類加載子系統、運行時數據區、執行引擎。
類加載子系統
​ Java虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型。
運行時數據區
​ Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。這些區域有各自的用途,以及建立和銷燬的時間,有的區域隨着虛擬機進程的啓動而一直存在,有些區域則是依賴用戶線程的啓動和結束而創建和銷燬。
執行引擎
​ 執行引擎用於執行JVM字節碼指令,主要有兩種方式,分別是解釋執行和編譯執行,區別在於,解釋執行是在執行時翻譯成虛擬機指令執行,而編譯執行是在執行以前先進行編譯再執行。解釋執行啓動快,執行效率低。編譯執行,啓動慢,執行效率高。垃圾回收器就是自動管理運行數據區的內存,將無用的內存佔用進行清除,釋放內存資源。
本地方法庫、本地庫接口
​ 在jdk的底層中,有一些實現是須要調用本地方法完成的(使用c或c++寫的方法),就是經過本地庫接口調用完成的。好比:System.currentTimeMillis()方法。緩存

運行時數據區

​ 運行時數據區是jvm中最爲重要的部門。也是咱們在調優時須要重點關注的區域,下面咱們一塊兒瞭解下這個部分的具體內容。服務器

​ 根據《Java虛擬機規範》中的規定,在運行時數據區將內存分爲方法區(Method Area)、Java堆區(Java
Heap)、Java虛擬機棧(Java Virtual Machine Stack)、程序計數器(Program Counter Register)、本地方法
棧(Native Method Stacks)。多線程

程序計數器

​ 程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。架構

​ 因爲Java虛擬機的多線程是經過線程輪流切換、分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。app

java虛擬機棧

​ 與程序計數器同樣,Java虛擬機棧也是線程私有的,它的生命週期與線程相同。Java虛擬機棧描述的是Java方法執行的線程內存模型:每一個方法被執行的時候,Java虛擬機都會同步建立一個棧幀,用於存儲局部變量表、操做數棧、動態鏈接、方法出口等信息。每個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。jvm

局部變量表

  • 局部變量表是一組變量值的存儲空間,用於存放方法參數和方法內部定義的局部變量。
  • 在Class文件中,方法的Code屬性的max_locals數據項中肯定了該方法所需分配的局部變量表的最大容量。
  • 該表以變量槽(Variable Slot)爲最小單位,一個slot能夠存放32位之內的數據,好比:boolean、byte、
    char、short、int、float等數據,若是存儲long、double類型數據,須要佔用2個solt。
  • 虛擬機經過索引定位的方式使用局部變量表,索引值的範圍是從0開始至局部變量表最大的變量槽數量。
  • 若是訪問的是32位數據類型的變量,索引N就表明了使用第N個變量槽,若是訪問的是64位數據類型的變量,則說明會同時使用第N和N+1兩個變量槽。
  • 局部變量表中第0位索引的變量槽默認是用於傳遞方法所屬對象實例的引用,在方法中能夠經過關鍵字「this」來訪問到這個隱含的參數。其他參數則按照參數表順序排列,佔用從1開始的局部變量槽,參數表分配完畢後,再根據方法體內部定義的變量順序和做用域分配其他的變量槽。

操做數棧

  • 操做數棧也常被稱爲操做棧,它是一個先進後出棧。
  • 操做數棧的最大深度也在編譯的時候被寫入到Code屬性的max_stacks數據項之中。
  • 操做數棧的每個元素均可以是包括long和double在內的任意Java數據類型。32位數據類型所佔的棧容量爲1,64位數據類型所佔的棧容量爲2。
  • 方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法的執行過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,也就是出棧和入棧操做。
  • 操做數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,例如iadd指令,不能出現一個long和一個float使用iadd命令相加的狀況。

動態鏈接

  • 每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中
    的動態鏈接。
  • Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池裏指向方法的符號引用做爲
    參數。這些符號引用一部分會在類加載階段或者第一次使用的時候就被轉化爲直接引用,這種轉化被稱爲靜
    態解析。另一部分將在每一次運行期間都轉化爲直接引用,這部分就稱爲動態鏈接。

方法出口

  • 當一個方法開始執行後,只有兩種方式退出這個方法。
  • 第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用
    者,方法是否有返回值以及返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱爲「正
    常調用完成」。
  • 另一種退出方式是在方法執行的過程當中遇到了異常,而且這個異常沒有在方法體內獲得妥善處理。不管是
    Java虛擬機內部產生的異常,仍是代碼中使用throw字節碼指令產生的異常,只要在本方法的異常表中沒有搜
    索到匹配的異常處理器,就會致使方法退出,這種退出方法的方式稱爲「異常調用完成」。這種方法的返回是不
    會給它的上層調用者提供任何返回值的。
  • 不管採用何種退出方式,在方法退出以後,都必須返回到最初方法被調用時的位置,程序才能繼續執行,方
    法返回時可能須要在棧幀中保存一些信息,用來幫助恢復它的上層主調方法的執行狀態。
  • 方法退出的過程實際上等同於把當前棧幀出棧,所以退出時可能執行的操做有:恢復上層方法的局部變量表
    和操做數棧,把返回值(若是有的話)壓入調用者棧幀的操做數棧中,調整PC計數器的值以指向方法調用指
    令後面的一條指令等。

圖解

​ 以 int i = 1; 這樣代碼爲例,看看虛擬機棧的執行

本地方法棧

​ 本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,其區別只是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的本地(Native)方法服務。

Java堆區

​ Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,Java世界裏「幾乎」全部的對象實例都在這裏分配內存。
​ 須要注意的是,《Java虛擬機規範》並無對堆進行細緻的劃分,因此對於堆的講解要基於具體的虛擬機,咱們以使用最多的HotSpot虛擬機爲例進行講解。
​ Java堆是垃圾收集器管理的內存區域,所以它也被稱做「GC堆」,這就是咱們作JVM調優的重點區域部分。

jdk1.7中堆內存的劃分

  • Young 年輕區(代)
    Young區被劃分爲三部分,Eden區和兩個大小嚴格相同的Survivor區,其中,Survivor區間中,某一時刻只有其中一個是被使用的,另一個留作垃圾收集時複製對象用,在Eden區間變滿的時候,GC就會將存活的對象移到空閒的Survivor區間中,根據JVM的策略,在通過幾回垃圾收集後,任然存活於Survivor的對象將被移動到Tenured區間。
  • Tenured 年老區
    Tenured區主要保存生命週期長的對象,通常是一些老的對象,當一些對象在Young複製轉移必定的次數之後,對象就會被轉移到Tenured區,通常若是系統中用了application級別的緩存,緩存中的對象每每會被轉移到這一區間。
  • Perm 永久區
    Perm代主要保存class,method,filed對象,這部份的空間通常不會溢出,除非一次性加載了不少的類,不過在涉及到熱部署的應用服務器的時候,有時候會遇到java.lang. OutOfMemoryError : PermGen space 的誤,形成這個錯誤的很大緣由就有多是每次都從新部署,可是從新部署後,類的class沒有被卸載掉,這樣就形成了大量的class對象保存在了perm中,這種狀況下,通常從新啓動應用服務器能夠解決問題。
  • Virtual區:
    最大內存和初始內存的差值,就是Virtual區。

jdk1.8中堆內存的劃分

由上圖能夠看出,jdk1.8的內存模型是由2部分組成,年輕代+ 年老代。
年輕代:Eden + 2*Survivor
年老代:OldGen
在jdk1.8中變化最大的Perm區,用Metaspace(元數據空間)進行了替換。
須要特別說明的是:Metaspace所佔用的內存空間不是在虛擬機內部,而是在本地內存空間中,這也是與1.7的永
久代最大的區別所在。

空間分配

若是沒有指定堆內存大小,默認初始堆內存爲物理內存的1/64,最大不超過物理內存的1/4或1G。注意的是元空間會自動擴容,默認狀況下不收限制。

爲何廢棄1.7中的永久區

官方給出的解釋是:移除永久代是爲融合HotSpot JVM與 JRockit VM而作出的努力,由於JRockit沒有永久代,不須要配置永久代。

方法區

  • 方法區(Method Area)與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、
    常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。
  • 《Java虛擬機規範》中把方法區描述爲堆的一個邏輯部分,它卻有一個別名叫做「非堆」(Non-Heap),目的
    是與Java堆區分開來。
  • JDK8以前將HotSpot虛擬機把收集器的分代設計擴展至方法區,因此能夠將永久代看作是方法區,JDK8以後
    廢棄永久代,用元空間來代替。

對象的訪問

  • Java程序會經過棧上的reference數據來操做堆上的具體對象。

  • 主流的訪問方式主要有使用句柄和直接指針兩種:

  • 句柄訪問
    Java堆中將可能會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自具體的地址信息.使用直接指針訪問Java堆中對象的內存佈局就必須考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象地址,若是隻是訪問對象自己的話,就不須要多一次間接訪問的開銷

  • 指針訪問

    使用句柄來訪問的最大好處就是reference中存儲的是穩定句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而reference自己不須要被修改。使用直接指針來訪問最大的好處就是速度更快,它節省了一次指針定位的時間開銷。HotSpot虛擬機採用的是指針訪問方式實現。

相關文章
相關標籤/搜索