Java之深入JVM的探討之旅

Java之深入JVM的探討之旅

JVM(Java 虛擬機)學習Java語言開發的朋友們都知道jvm是我們進行Java程序開發不可或缺的一個東西。你知道它叫Java虛擬機也應該知道就是因爲有了它才賦予了我們Java語言一個強大的特性——跨平臺的開發語言。就是因爲Java有了跨平臺性的特點所以它纔會在最近十年裏一直稱霸着計算機程序開發語言的王座。
知道有了它之後Java語言就有了跨平臺性的特點,但是你知道它是個怎麼樣的工作原理嗎?所以接下來就由我來帶領大家一起深入探討JVM的工作原理的講解。這也是我在學習JVM當中的一些筆記吧。把它們放到網上希望能幫助到那些學習Java的新生們。講解可能有些暇質大家在看的時候可以發表評論自己的看法。
首先讓我們看看Java概念圖的描述

在這裏插入圖片描述
JDK工具解析:(JDK爲java的開發工具包)java開發者必備
javac:java編輯器(把java源碼文件翻譯成.class的字節碼文件)
javadoc:文檔生成器(把一些自己編寫的java源碼生自己的API文檔類似於java官方API)
jar:打包工具(也是把寫好的資源文件打包壓縮成以.jar結尾的壓縮文件方便後期調用,類似於我們開發時經常導入的第三方jar包)
javap:反編譯工具(就是javac的死對頭,把.class字節碼文件反編譯成.java源碼文件)
javaConsole:監視器(符合JMX的圖形工具,用於監視Java虛擬機。它可以監視本地和遠程JVM。它還可以監視和管理應用程序。)

JRE工具解析:(JRE爲java的運行環境包被包含在JDK中)java運行者必備
核心類庫:
IDL—— Java IDL技術將Java(平臺對象請求代理體系結構)功能添加到Java平臺,提供基於標準的互操作性和連接性。
JDBC—— 提供給Java編程語言對數據庫建立操作關聯的API
JNDI—— Internet上的Java遠程方法調用Inter-ORB協議技術RMI編程模型支持通過RMI API對CORBA服務器和應用程序進行編程。
RMI—— 遠程方法調用(RMI)通過在用Java編程語言編寫的程序之間提供遠程通信來支持分佈式應用程序的開發。
RMI-IIOP—— Internet上的Java遠程方法調用Inter-ORB協議技術RMI編程模型支持通過RMI API對CORBA服務器和應用程序進行編程。
Scripting—— Java SE包括JSR 223:Java™Platform API的腳本,這是Java應用程序可以「託管」腳本引擎的框架。
I / O—— 在java.io和java.nio包管理應用程序的I / O提供了豐富的API集。該功能包括文件和設備I / O,對象序列化,緩衝區管理和字符集支持。
詳細請查看

今天我們主講的是包含在JRE中的JVM
JDK提供Java虛擬機(VM)的一個或多個實現:(這個是java官方解釋)
1、在通常用於客戶端應用程序的平臺上,JDK附帶了一個名爲Java HotSpot Client VM(客戶端VM)的VM實現。調整客戶端VM以減少啓動時間和內存佔用。-client啓動應用程序時,可以使用命令行選項調用它。(大致就是說在我們的計算機上都會由機器生產商安裝好的JVM,在我們打開機器的時候可以啓動它進行相關操作)
2、在所有平臺上,JDK都附帶了一個名爲Java HotSpot Server VM (服務器VM)的Java虛擬機實現。服務器VM旨在實現最高的程序執行速度。-server啓動應用程序時,可以使用命令行選項調用它 。

Java HotSpot技術的一些功能,對於兩種VM實現都是通用的,如下所示。

  1. 自適應編譯器 - 使用標準解釋器啓動應用程序,但在運行時會對代碼進行分析,以檢測性能瓶頸或「熱點」。Java HotSpot VM編譯代碼中性能關鍵部分以提高性能,同時避免不必要的編譯很少使用的代碼(大多數程序)。Java HotSpot VM還使用自適應編譯器來動態決定如何使用內嵌等技術優化編譯代碼。編譯器執行的運行時分析允許它消除猜測,確定哪些優化將產生最大的性能優勢。
  2. 快速內存分配和垃圾收集 - Java HotSpot技術爲對象提供快速內存分配,並提供快速,高效,最先進的垃圾收集器選擇。
  3. 線程同步 - Java編程語言允許使用多個併發的程序執行路徑(稱爲「線程」)。Java HotSpot技術提供了一種線程處理功能,旨在輕鬆擴展以用於大型共享內存多處理器服務器。

接下來我們看看JVM的內部結構圖:
官方JVM圖解

翻譯版本
文字解釋:源碼.java ——>(javac編輯器)——>字節碼.class ——> 類加載器(JVM)——>運行時數據區(JVM)——>執行引擎(JVM) ——>機器碼 ——>機器識別處理

java虛擬機內存模型中定義的訪問操作與物理計算機處理的基本一致。
引用圖片
JVM定義了控制Java代碼解釋執行和具體實現的五種規格,它們是:

  1. JVM指令系統
  2. JVM寄存器
  3. JVM 棧結構
  4. JVM 碎片回收堆

工作原理:
1、JVM是java的核心和基礎,在java編譯器和os平臺之間的虛擬處理器。它是一種基於下層的操作系統和硬件平臺並利用軟件方法來實現的抽象的計算機,可以在上面執行java的字節碼程序。
2、 java編譯器只需面向JVM,生成JVM能理解的代碼或字節碼文件。Java源文件經編譯器,編譯成字節碼程序,通過JVM將每一條指令翻譯成不同平臺機器碼,通過特定平臺運行。
重點JVM的「內存管理」:內存分配、垃圾回收。

JVM在運行時會把自己所管理的內存劃分成若干個不同 數據區域
線程私有:程序計數器、本地方法棧、虛擬機棧。指令(線程私有,執線程運行時初始化,運行結束銷燬)
線程共享:堆、方法區。數據(線程共享)
引用圖片
在這裏插入圖片描述

操作數棧結構
——>程序計數器(Program Counter Register):指向當前線程正在執行的字節碼指令的地址(指令行號),由於java是多線程的,在CPU進行線程切換的時候確保程序中的線程在恢復自己的CPU執行能夠回線程到切換前的執行位置,所以用程序計數器來確定CPU到底是執行到那個線程了並且賦予了CPU時間片(可以佔用CPU的時間段)
由於Java 虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域爲「線程私有」的內存。
如果線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則爲空(Undefined)。此內存區域是唯一一個在Java 虛擬機規範中沒有規定任何OutOfMemoryError 情況的區域。
——>本地方法棧(Native Method Stacks):執行過程跟虛擬機棧差不多,(本地方法棧執行native方法、虛擬機棧執行java方法)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧爲虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native 方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError 和OutOfMemoryError異常。
——>虛擬機棧(Java Virtual Machine Stacks):存儲特點(先進後出),存儲當前線程運行方法(棧幀:局部變量表、操作數棧、動態連接、返回地址,主要由這四個部分構成)所需的數據、指令、返回地址(對象引用)。
當類中的方法被調用時都會產生一個結構都一樣的「棧幀」
VM Args: -Xss128k 表示虛擬機棧的內存大小,當虛擬機棧的所有棧幀的內存總和超過了128k就會產生棧內存溢出(會報StackOverFlowError異常),當每個棧幀增添變量時會造成虛擬機棧能容納棧幀的數量減少(-Xss / 單個棧幀內存 = 棧幀數量),爲棧幀增添變量會增加單個棧幀的內存。
程序優化:(虛擬機棧默認內存空間爲1M)

  1. 儘量合理分配每個棧幀的內存,讓虛擬機棧儘可能可以容納更多的棧幀。
    高併發、多線程的項目:
    默認:是按虛擬機棧的內存空間(-Xss)默認1M,可是我們的程序有1000個線程同時運行(1M * 1000 = 1G)這時是非常消耗我們計算機有限的內存空間的。
    優化:把虛擬機棧內存空間(-Xss)修改爲200k,同樣程序有1000個線程同時運行(200k * 1000 = 200M)這時比較很明顯了嘛。
  2. 合理利用棧幀的內存空間,儘量少創建一個無關緊要的變量以減少棧幀的內存。
    高併發、多線程的項目:
    虛擬機棧都是1M內存,每個棧幀有10000個int(4個字節)變量,20000個char(2個字節)變量,10000個float(4個字節)變量,10000個long(8個字節)變量,其中有一半爲無用的變量。
    默認:
    10000 x 4bytes + 20000 x 2bytes + 10000 x 4bytes + 10000 x 8bytes = 200000bytes
    1M = 2^20bytes
    2^20bytes / 200000bytes = 100000 00000 00000個棧幀(每個棧幀只計算變量內存空間)
    優化:
    5000 x 4bytes + 10000 x 2bytes + 5000 x 4bytes + 5000 x 8bytes = 100000bytes
    1M = 2^20bytes
    2^20bytes / 100000bytes = 200000 00000 00000個棧幀(每個棧幀只計算變量內存空間)
    局部變量表:(首位存放的是this對象代表本棧幀)存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)的變量名字、對象引用(reference 類型,它不等同於對象本身,根據不同的虛擬機實現,它可能是一個指向對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress 類型(指向了一條字節碼指令的地址)。
    操作數棧:存放的是各種基本類型的變量值和引用類型的引用地址。
    動態連接:這裏與java的多態性區別不大,就是說在我們類中的操作方法可以有很多種形態。動態連接就是此時產生操作的多態操作方法與對象間的連接。
    返回地址:就是操作方法最後的return操作。

在這裏插入圖片描述

——>方法區
方法區(Method Area)與Java 堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java 堆區分開來。對於習慣在HotSpot 虛擬機上開發和部署程序的開發者來說,很多人願意把方法區稱爲「永久代」(Permanent Generation),本質上兩者並不等價,僅僅是因爲HotSpot 虛擬機的設計團隊選擇把GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對於其他虛擬機(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。即使是HotSpot 虛擬機本身,根據官方發佈的路線圖信息,現在也有放棄永久代並「搬家」至Native Memory 來實現方法區的規劃了。Java 虛擬機規範對這個區域的限制非常寬鬆,除了和Java 堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集。相對而言,垃圾收集行爲在這個區域是比較少出現的,但並非數據進入了方法區就如永久代的名字一樣「永久」存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載,一般來說這個區域的回收「成績」比較難以令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分區域的回收確實是有必要的。在Sun 公司的BUG 列表中,曾出現過的若干個嚴重的BUG 就是由於低版本的HotSpot 虛擬機對此區域未完全回收而導致內存泄漏。根據Java 虛擬機規範的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError 異常。

類信息:類被加載後的信息
常量:在類中被定義爲常量的數據變量
靜態變量:在類中被static修飾的變量(隨着類的加載而加載)
即時編譯期譯後代碼
在這裏插入圖片描述
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant PoolTable),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。Java 虛擬機對Class 文件的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用於存儲哪種數據都必須符合規範上的要求,這樣纔會被虛擬機認可、裝載和執行。但對於運行時常量池,Java 虛擬機規範沒有做任何細節的要求,不同的提供商實現的虛擬機可以按照自己的需要來實現這個內存區域。不過,一般來說,除了保存Class 文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中①。運行時常量池相對於Class 文件常量池的另外一個重要特徵是具備動態性,Java 語言並不要求常量一定只能在編譯期產生,也就是並非預置入Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String 類的intern() 方法。既然運行時常量池是方法區的一部分,自然會受到方法區內存的限制,當常量池無法再申請到內存時會拋出OutOfMemoryError 異常

直接內存直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError 異常出現,所以我們放到這裏一起講解。在JDK 1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O 方式,它可以使用Native 函數庫直接分配堆外內存,然後通過一個存儲在Java 堆裏面的DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在Java 堆和Native 堆中來回複製數據。顯然,本機直接內存的分配不會受到Java 堆大小的限制,但是,既然是內存,則肯定還是會受到本機總內存(包括RAM 及SWAP 區或者分頁文件)的大小及處理器尋址空間的限制。服務器管理員配置虛擬機參數時,一般會根據實際內存設置-Xmx等參數信息,但經常會忽略掉直接內存,使得各個內存區域的總和大於物理內存限制(包括物理上的和操作系統級的限制),從而導致動態擴展時出現OutOfMemoryError異常。邏輯內存模型我們已經看到了,那當我們建立一個對象的時候是怎麼進行訪問的呢?在Java 語言中,對象訪問是如何進行的?對象訪問在Java 語言中無處不在,是最普通的程序行爲,但即使是最簡單的訪問,也會卻涉及Java 棧、Java 堆、方法區這三個最重要內存區域之間的關聯關係,如下面的這句代碼:Object obj = new Object();假設這句代碼出現在方法體中,那「Object obj」這部分的語義將會反映到Java 棧的本地變量表中,作爲一個reference 類型數據出現。而「new Object()」這部分的語義將會反映到Java 堆中,形成一塊存儲了Object 類型所有實例數據值(Instance Data,對象中各個實例字段的數據)的結構化內存,根據具體類型以及虛擬機實現的對象內存佈局(Object Memory Layout)的不同,這塊內存的長度是不固定的。另外,在Java 堆中還必須包含能查找到此對象類型數據(如對象類型、父類、實現的接口、方法等)的地址信息,這些類型數據則存儲在方法區中。由於reference 類型在Java 虛擬機規範裏面只規定了一個指向對象的引用,並沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java 堆中的對象的具體位置,因此不同虛擬機實現的對象訪問方式會有所不同,主流的訪問方式有兩種:使用句柄和直接指針。如果使用句柄訪問方式,Java 堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息,如下圖所示。

在這裏插入圖片描述

如果使用直接指針訪問方式,Java 堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,reference 中直接存儲的就是對象地址,如下圖所示
在這裏插入圖片描述
這兩種對象的訪問方式各有優勢,使用句柄訪問方式的最大好處就是reference 中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而reference 本身不需要被修改。使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象的訪問在Java 中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。就本書討論的主要虛擬機Sun HotSpot 而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發的範圍來看,各種語言和框架使用句柄來訪問的情況也十分常見。

——>Java堆
對於大多數應用來說,Java 堆(Java Heap)是Java 虛擬機所管理的內存中最大的一塊。Java 堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。這一點在Java 虛擬機規範中的描述是:所有的對象實例以及數組都要在堆上分配①,但是隨着JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換②優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那麼「絕對」了。Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做「GC 堆」(GarbageCollected Heap,幸好國內沒翻譯成「垃圾堆」)。如果從內存回收的角度看,由於現在收集器基本都是採用的分代收集算法,所以Java 堆中還可以細分爲:新生代和老年代;再細緻一點的有Eden 空間、From Survivor 空間、To Survivor 空間等。如果從內存分配的角度看,線程共享的Java 堆中可能劃分出多個線程私有的分配緩衝區(Thread LocalAllocation Buffer,TLAB)。不過,無論如何劃分,都與存放內容無關,無論哪個區域,存儲的都仍然是對象實例,進一步劃分的目的是爲了更好地回收內存,或者更快地分配內存。在本章中,我們僅僅針對內存區域的作用進行討論,Java 堆中的上述各個區域的分配和回收等細節將會是下一章的主題。根據Java 虛擬機規範/的規定,Java 堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實現時,既可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms 控制)。如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError 異常。
對象實例
數組
——>Java堆的大小參數設置
-Xmx 堆區內存可被分配的最大上限
-Xms 堆區內存初始化分配的大小

JMM(java內存模型)
基於分代的思想
堆:新生代(佔堆內存1/3)、老年代(2/3)
——新生代:Eden區(佔新生區內存8/10)、From Survivor區(1/10)、To urvivor區(1/10)
因爲大廠們做過實驗證明:有90%的對象不用垃圾回收機制的。——只有大概10%的對象要去使用垃圾回收(複製回收算法)
有10%的內存空間作爲預留區。所以纔有8:1:1 = 80% :10% :10% 的比例。
引用圖片
JMM——對象內存分配
1、對象優先在Eden區分配
2、在新生代長期存活的對象會優先進入老年代
3、內存容量超過新生代的Eden區的對象會直接進入老年代
4、動態判斷對象的年齡:同一個年齡段的對象數內存總量超過了新生代的內存容量,就會進入老年代

JMM——對象回收(判斷對象的存活)
1、引用計數算法:根據設置的生命週期次數,每GC(執行)1次,初始次數會自減1,等到初始次數爲0的時候,就會調用此方法銷燬對象
2、可達性分配算法:如下圖所示

在這裏插入圖片描述
Minor GC(新生代)——複製回收算法(優點速度快,缺點是隻有50%的利用率)使用於大部分的對象都不需要垃圾回收的時候
在這裏插入圖片描述

Full GC(老年代)
標記清除算法:會帶來內存碎片(當對象需要的內存大於一個局部內存片的時候)——優先使用
在這裏插入圖片描述
標記整理算法(老年代)當會產生過多碎片時,使用此算法進行內存空間整理(性能低下)因爲老年代會產生很多內存碎片,所以是爲什麼老年代需要兩種算法配合使用。
在這裏插入圖片描述

在這裏插入圖片描述

JVM的相關知識:
1、線程私有區域的生命週期是跟隨線程的
2、堆(JMM新生代,老年代)是垃圾回收的重點區域
3、GC的發展趨勢會使用收器(回收量幾百M或者幾個G)
JDK11——ZGC(TB級別):有色指針,加載屏障

JVM常用問題處理方式

  1. 保存堆棧快照日記
  2. 分析內存泄露——佔用堆棧域對象內存超過了此堆棧初始化的內存空間大小(-Xmx最大分配值)
  3. 整理內存設置——設置堆棧的初始化內存空間大小(-Xms最小分配值、-Xmx最大分配值)
  4. 控制垃圾回收頻率
  5. 選擇合適的垃圾回收器

這篇文章是我本人蔘考多篇大佬級別的博客抽取出來比較好的內容