JVM有關的知識點(一)

JVM有關的知識點(一)

1. 你瞭解JAVA虛擬機嗎?能解釋一下底層的模塊嗎?

JVM在整個jdk(java 運行環境)中處於最底層,負責與操作系統的交互,用來屏蔽操作系統環境,提供一個完整的Java運行環境,因此也就虛擬計算機. 操作系統裝入JVM是通過jdk中Java.exe來完成,通過下面4步來完成JVM環境.

(1)創建JVM裝載環境和配置
(2)裝載JVM.dll
(3)初始化JVM.dll並掛界到JNIENV(JNI調用接口)實例
(4) 調用JNIEnv實例裝載並處理class類。

主要由【程序計數器,虛擬機棧,本地方法棧,堆,方法區】五個主要模塊構成
在這裏插入圖片描述

程序計數器

主要是當前線程執行字節碼的行號指示器,是線程私有的,如果執行的是java方法,則計數器記錄的是正在執行虛擬機字節碼的指令地址;如果執行的是native方法,則這個計數器爲空。
此內存區域是整個jvm唯一沒有規定內存溢出【OutOfMemoryError】的區域。

虛擬機棧(又稱java棧)
虛擬機棧其實就是棧內存主要指代的地方,這一部分內存用來存儲基礎數據類型的對象,以及爲對內存中動態變量的引用提供一個地址,也就是java的指針。
這一部分也是【線程私有】,每個方法在執行的同時創建一個棧幀,實際上,Java虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有局部變量表、操作數棧、動態鏈接、方法出口信息
隨着方法執行的結束,這個棧幀也在jvm中進行了入棧與出棧的過程。

這一塊可以拋出兩個異常:
【stackOverFlowError】:線程請求的棧的深度大於虛擬機預設的深度
【OutOfMemoryError】:jvm無法擴展足夠內存

主要分三個區域:局部變量區、運行環境區、操作數區。

  • 局部變量區:

    每個Java方法使用一個固定大小的局部變量集。它們按照與vars寄存器的字偏移量來尋址。局部變量都是32位的。長整數和雙精度浮點數佔據了兩個局部變量的空間,卻按照第一個局部變量的索引來尋址。虛擬機規範並不要求在局部變量中的64位的值是64位對齊的。虛擬機提供了把局部變量中的值裝載到操作數棧的指令,也提供了把操作數棧中的值寫入局部變量的指令。

  • 運行環境區

    在運行環境中包含的信息用於動態鏈接,正常的方法返回以及異常捕捉。

  • 操作數區

    機器指令只從操作數棧中取操作數,對它們進行操作,並把結果返回到棧中。選擇棧結構的原因是:在只有少量寄存器或非通用寄存器的機器(如Intel486)上,也能夠高效地模擬虛擬機的行爲。操作數棧是32位的。它用於給方法傳遞參數,並從方法接收結果,也用於支持操作的參數,並保存操作的結果。例如,iadd指令將兩個整數相加。相加的兩個整數應該是操作數棧頂的兩個字。這兩個字是由先前的指令壓進堆棧的。這兩個整數將從堆棧彈出、相加,並把結果壓回到操作數棧中。

    每個原始數據類型都有專門的指令對它們進行必須的操作。每個操作數在棧中需要一個存儲位置,除了long和double型,它們需要兩個位置。操作數只能被適用於其類型的操作符所操作。例如,壓入兩個int類型的數,如果把它們當作是一個long類型的數則是非法的。在Sun的虛擬機實現中,這個限制由字節碼驗證器強制實行。但是,有少數操作(操作符dupe和swap),用於對運行時數據區進行操作時是不考慮類型的。

本地方法棧:

與虛擬機棧很相似,但是虛擬機棧主要爲java方法(也就是字節碼)提供內存服務;
而本地方法棧則爲虛擬機使用到的 Native 方法服務。
因爲native的原因,本地方法棧對棧中方法的語言,使用方式,數據結構內都沒有要求,比較自由,在 HotSpot 虛擬機中將虛擬機棧與本地方法棧和二爲一。
拋出異常與虛擬機棧相同。

堆:

佔用虛擬機內存最大的一塊,被所有【線程共享】因此在其上進行對象內存的分配均需要進行加鎖,這也導致了new對象的開銷是比較大的,啓動虛擬機就會創建這一塊內存區域。這一塊內存區域的唯一作用就是存放對象實例。
java堆是jvm垃圾管理收集的主要區域,因此很多時候也被稱爲GC堆
【OutOfMemoryError】:jvm無法擴展足夠內存

方法區:
【線程共享】區域,此點與堆相同,但是方法區主要存儲的是已經被jvm加載的類的信息,常量,靜態變量等。jvm規範是把方法區歸爲堆的一個邏輯部分,
但是卻仍然有區別:根據分代收集算法方法區算是永久代
【OutOfMemoryError】:jvm無法擴展足夠內存

運行時常量池:

運行時常量池是方法區的一部分。Class文件中存在類的版本,屬性,字段,方法,接口等以及運行時常量池(用於存放編譯期生成的各種字面量和符號引用),主要存儲的是類加載編譯時期生成的各種字面變量和符號引用。
既然運行時常量池時方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出 OutOfMemoryError 異常。
JDK1.7及之後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。

在這裏插入圖片描述

Java虛擬機從啓動到結束的生命週期,當Java虛擬機啓動後,在如下幾種情況下,Java虛擬機將結束生命週期:

(1)執行了System.exit()方法
(2)程序正常執行結束
(3)程序在執行過程中遇到了異常或錯誤而異常終止
(4)由於操作系統出現錯誤而導致Java虛擬機進程終止

2. JRE/JDK/JVM是什麼關係

JRE(JavaRuntimeEnvironment,Java運行環境) 也就是Java平臺。所有的Java 程序都要在JRE下才能運行。普通用戶只需要運行已開發好的java程序,安裝JRE即可。

JDK(Java Development Kit) 是程序開發者用來來編譯、調試java程序用的開發工具包。JDK的工具也是Java程序,也需要JRE才能運行。爲了保持JDK的獨立性和完整性,在JDK的安裝過程中,JRE也是 安裝的一部分。所以,在JDK的安裝目錄下有一個名爲jre的目錄,用於存放JRE文件。

JVM(JavaVirtualMachine,Java虛擬機) 是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。JVM有自己完善的硬件架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。Java語言最重要的特點就是跨平臺運行。使用JVM就是爲了支持與操作系統無關,實現跨平臺。

3. JVM原理

JVM是java的核心和基礎,在java編譯器和os平臺之間的虛擬處理器。它是一種利用軟件方法實現的抽象的計算機基於下層的操作系統和硬件平臺,可以在上面執行java的字節碼程序。

java程序運行的一個全過程圖例:
在這裏插入圖片描述
java編譯器只要面向JVM,生成JVM能理解的代碼或字節碼文件。Java源文件經編譯成字節碼程序,通過JVM將每一條指令翻譯成不同平臺機器碼,通過特定平臺運行。

4. JVM內存模型及調優

4.1 JVM內存模型

JDK1.7內存區域劃分
在這裏插入圖片描述
JDK1.8內存區域劃分
在這裏插入圖片描述
上面提到並介紹過的不在講,重點講解直接內存和元數據區

  • 直接內存

    直接內存並不是JVM運行時數據區的一部分, 但也會被頻繁的使用: 在JDK 1.4引入的NIO提供了基於Channel與Buffer的IO方式, 它可以使用Native函數庫直接分配堆外內存, 然後使用DirectByteBuffer對象作爲這塊內存的引用進行操作, 這樣就避免了在Java堆和Native堆中來回複製數據, 因此在一些場景中可以顯著提高性能.
    本機直接內存的分配不會受到Java堆大小的限制(即不會遵守-Xms、-Xmx等設置), 但既然是內存, 則肯定還是會受到本機總內存大小及處理器尋址空間的限制, 因此動態擴展時也會出現OutOfMemoryError異常.

  • 元數據區域
    元數據區域取代了1.7版本及以前的永久代。元數據和永久代本質上都時方法區的實現。
    參數設置:-XX:MetaspaceSize=18m
    -XX:MaxMetaspaceSize=60m

4.2. JVM內存調優

首先需要注意的是在對JVM內存調優的時候不能只看操作系統級別Java進程所佔用的內存,這個數值不能準確的反應堆內存的真實佔用情況,因爲GC過後這個值是不會變化的,因此內存調優的時候要更多地使用JDK提供的內存查看工具,比如JConsole和Java VisualVM。

對JVM內存的系統級的調優主要的目的是減少GC的頻率和Full GC的次數,過多的GC和Full GC是會佔用很多的系統資源(主要是CPU),影響系統的吞吐量。特別要關注Full GC,因爲它會對整個堆進行整理,導致Full GC一般由於以下幾種情況:

  1. 舊生代(年老代)空間不足
    調優時儘量讓對象在新生代GC時被回收、讓對象在新生代多存活一段時間和不要創建過大的對象及數組避免直接在舊生代創建對象

  2. 持久代Pemanet Generation空間不足
    增大Perm Gen空間,避免太多靜態對象,控制好新生代和舊生代的比例

  3. 統計得到的GC後晉升到舊生代的平均大小大於舊生代剩餘空間
    控制好新生代和舊生代的比例

  4. System.gc()被顯示調用
    垃圾回收不要手動觸發,儘量依靠JVM自身的機制

調優手段主要是通過控制堆內存的各個部分的比例和GC策略來實現,下面來看看各部分比例不良設置會導致什麼後果

1)新生代設置過小

一是新生代GC次數非常頻繁,增大系統消耗;二是導致大對象直接進入舊生代,佔據了舊生代剩餘空間,誘發Full GC

2)新生代設置過大

一是新生代設置過大會導致舊生代過小(堆總量一定),從而誘發Full GC;二是新生代GC耗時大幅度增加。一般說來新生代佔整個堆1/3比較合適

3)Survivor設置過小

導致對象從eden直接到達舊生代,降低了在新生代的存活時間

4)Survivor設置過大

導致eden過小,增加了GC頻率

另外,通過-XX:MaxTenuringThreshold=n來控制新生代存活時間,儘量讓對象在新生代被回收。

由內存管理和垃圾回收可知新生代和舊生代都有多種GC策略和組合搭配,選擇這些策略對於我們這些開發人員是個難題,JVM提供兩種較爲簡單的GC策略的設置方式

1)吞吐量優先

JVM以吞吐量爲指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,來達到吞吐量指標。這個值可由-XX:GCTimeRatio=n來設置

2)暫停時間優先

JVM以暫停時間爲指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,儘量保證每次GC造成的應用停止時間都在指定的數值範圍內完成。這個值可由-XX:MaxGCPauseRatio=n來設置

調優步驟:

  1. 監控GC的狀態

使用各種JVM工具,查看當前日誌,分析當前JVM參數設置,並且分析當前堆內存快照和gc日誌,根據實際的各區域內存劃分和GC執行時間,覺得是否進行優化。

舉一個例子: 系統崩潰前的一些現象:

  • 每次垃圾回收的時間越來越長,由之前的10ms延長到50ms左右,FullGC的時間也有之前的0.5s延長到4、5s
  • FullGC的次數越來越多,最頻繁時隔不到1分鐘就進行一次FullGC
  • 年老代的內存越來越大並且每次FullGC後年老代沒有內存被釋放

之後系統會無法響應新的請求,逐漸到OutOfMemoryError的臨界值,這個時候就需要分析JVM內存快照dump。

  1. 生成堆的dump文件

通過JMX的MBean生成當前的Heap信息,大小爲一個3G(整個堆的大小)的hprof文件,如果沒有啓動JMX可以通過Java的jmap命令來生成該文件。

  1. 分析dump文件

    幾種工具打開該文件:

  • Visual VM
  • IBM HeapAnalyzer
  • JDK 自帶的Hprof工具
  • Mat(Eclipse專門的靜態內存分析工具)推薦使用
    備註:文件太大,建議使用Eclipse專門的靜態內存分析工具Mat打開分析
  1. 分析結果,判斷是否需要優化

如果各項參數設置合理,系統沒有超時日誌出現,GC頻率不高,GC耗時不高,那麼沒有必要進行GC優化,如果GC時間超過1-3秒,或者頻繁GC,則必須優化。

注:如果滿足下面的指標,則一般不需要進行GC:

  • Minor GC執行時間不到50ms;
  • Minor GC執行不頻繁,約10秒一次;
  • Full GC執行時間不到1s;
  • Full GC執行頻率不算頻繁,不低於10分鐘1次;
  1. 調整GC類型和內存分配

如果內存分配過大或過小,或者採用的GC收集器比較慢,則應該優先調整這些參數,並且先找1臺或幾臺機器進行beta,然後比較優化過的機器和沒有優化的機器的性能對比,並有針對性的做出最後選擇

  1. 不斷的分析和調整

通過不斷的試驗和試錯,分析並找到最合適的參數,如果找到了最合適的參數,則將這些參數應用到所有服務器。
在這裏插入圖片描述
JVM常見配置

【堆設置】

-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:設置年輕代大小
-XX:NewRatio=n:設置年輕代和年老代的比值。如:爲3,表示年輕代與年老代比值爲1:3,年輕代佔整個年輕代年老代和的1/4
-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5
-XX:MaxPermSize=n:設置持久代大小

【收集器設置】

-XX:+UseSerialGC:設置串行收集器
-XX:+UseParallelGC:設置並行收集器
-XX:+UseParalledlOldGC:設置並行年老代收集器
-XX:+UseConcMarkSweepGC:設置併發收集器

【回收統計信息】

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

【並行收集器設置】

-XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。並行收集線程數。
-XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間
-XX:GCTimeRatio=n:設置垃圾回收時間佔程序運行時間的百分比。公式爲1/(1+n)

【併發收集器設置】

-XX:+CMSIncrementalMode:設置爲增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n:設置併發收集器年輕代收集方式爲並行收集時,使用的CPU數。並行收集線程數。

參考 & 拓展

深入理解Java虛擬機 https://book.douban.com/subject/24722612/
JVM內幕:Java虛擬機詳解 (薦) http://www.importnew.com/17770.html
JAVA的內存模型及結構 http://ifeve.com/under-the-hood-runtime-data-areas-javas-memory-model/
JVM內存模型與調優 https://blog.csdn.net/qq_39712188/article/details/84840613

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