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

前言

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

JVM介紹

什麼是JVM

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

百度百科對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虛擬機的多線程是通過線程輪流切換、分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)都只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。

java虛擬機棧

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

局部變量表

  • 局部變量表是一組變量值的存儲空間,用於存放方法參數和方法內部定義的局部變量。
  • 在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虛擬機採用的是指針訪問方式實現。

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