JVM 全稱 Java Virtual Machine 是Java語言實現與平臺的無關性的關鍵。我們所說的 JVM,狹義上指的就 HotSpot(因為JVM有很多版本,但是使用最多的是HotSpot)。如非特殊說明,我們都以 HotSpot 為準。一般的高級語言如果要在不同的平臺上運行,至少需要編譯成不同的目標代碼。而引入Java語言虛擬機后,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用模式Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。
跨平臺:
JVM在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。相當于現實生活中的翻譯。
跨語言:
JVM只識別字節碼,所以JVM其實跟語言是解耦的,也就是沒有直接關聯,并不是它翻譯Java文件,而是識別class文件,這個一般稱之為字節碼。還有像Groovy 、Kotlin、Jruby等等語言,它們其實也是編譯成字節碼,所以也可以在JVM上面跑,這個就是JVM的跨語言特征。
JVM、JRE、JDK的關系
JVM只是一個翻譯,把Class翻譯成機器識別的代碼,但是需要注意,JVM 不會自己生成代碼,需要大家編寫代碼,同時需要很多依賴類庫,這個時候就需要用到JRE。
JRE是什么,它除了包含JVM之外,提供了很多的類庫(就是我們說的jar包,它可以提供一些即插即用的功能,比如讀取或者操作文件,連接網絡,使用I/O等等之類的)這些東西就是JRE提供的基礎類庫。JVM 標準加上實現的一大堆基礎類庫,就組成了 Java 的運行時環境,也就是我們常說的 JRE(Java Runtime Environment)。
但對于程序員來說,JRE還不夠。我寫完要編譯代碼,還需要調試代碼,還需要打包代碼、有時候還需要反編譯代碼。所以我們會使用JDK,因為JDK還提供了一些非常好用的小工具,比如 javac(編譯代碼)、java、jar (打包代碼)、javap(反編譯<反匯編>)等。這個就是JDK。
Java程序的運行過程
Java程序的運行過程是javac工具先將.Java文件編譯成.class文件,然后執行的過程中通過Java類加載器ClassLoader將class文件加載到JVM 運行時數據區 ,通過解釋執行和JIT等執行引擎調用操作系統的接口。如圖所示:
運行時數據區域
了解Java虛擬機主要是理解其運行時數據區原理。Java 引以為豪的就是它的自動內存管理機制。相比于 C++的手動內存管理、復雜難以理解的指針等,Java 程序寫起來就方便的多。在 Java 中,JVM 內存主要分為堆、程序計數器、方法區、虛擬機棧和本地方法棧。運行時數據區結構如圖所示
線程獨享的區域
程序計數器
小的內存空間,當前線程執行的字節碼的行號指示器;各線程之間獨立存儲,互不影響。程序計數器是一塊很小的內存空間,主要用來記錄各個線程執行的字節碼的地址,例如,分支、循環、跳轉、異常、線程恢復等都依賴于計數器。由于 Java 是多線程語言,當執行的線程數量超過 CPU 核數時,線程之間會根據時間片輪詢爭奪 CPU 資源。如果一個線程的時間片用完了,或者是其它原因導致這個線程的 CPU 資源被提前搶奪,那么這個退出的線程就需要單獨的一個程序計數器,來記錄下一條運行的指令。
程序計數器也是JVM中唯一不會OOM(OutOfMemory)的內存區域
虛擬機棧
虛擬機棧在JVM運行過程中存儲當前線程運行方法所需的數據,指令、返回地址。Java 虛擬機棧是基于線程的。哪怕你只有一個 main() 方法,也是以線程的方式運行的。在線程的生命周期中,參與計算的數據會頻繁地入棧和出棧,棧的生命周期是和線程一樣的。棧里的每條數據,就是棧幀。在每個 Java 方法被調用的時候,都會創建一個棧幀,并入棧。一旦完成相應的調用,則出棧。所有的棧幀都出棧后,線程也就結束了。
棧幀
棧幀,虛擬機棧的存儲單元,虛擬機棧中存放著一個或多個棧幀。每個棧幀,都包含四個區域:(局部變量表、操作數棧、動態連接、返回地址)。棧的大小缺省為1M,可用參數 –Xss調整大小,例如-Xss256k
局部變量表
用于存放我們的局部變量的。首先它是一個32位的長度,主要存放我們的Java的八大基礎數據類型,一般32位就可以存放下,如果是64位的就使用高低位占用兩個也可以存放下,如果是局部的一些對象,比如我們的Object對象,我們只需要存放它的一個引用地址即可。
操作數棧
它就是一個棧,先進后出的棧結構,操作數棧,就是用來操作的,操作的的元素可以是任意的java數據類型,所以我們知道一個方法剛剛開始的時候,這個方法的操作數棧就是空的,操作數棧運行方法就是JVM一直運行入棧/出棧的操作。在JVM中,基于解釋執行的這種方式是基于棧的引擎,這個說的棧,就是操作數棧。
動態連接
Java語言特性多態(需要類運行時才能確定具體的方法)。
返回地址
正常返回(調用程序計數器中的地址作為返回)、異常的話(通過異常處理器表<非棧幀中的>來確定)
一個簡單的work方法對應于在運行時數據區的字節碼
想了解更多字節碼,可以通過這個網站學習字節碼
https://cloud.tencent.com/developer/article/1333540
本地方法棧
本地方法棧跟 Java 虛擬機棧的功能類似,Java 虛擬機棧用于管理 Java 函數的調用,而本地方法棧則用于管理本地方法的調用。但本地方法并不是用 Java 實現的,而是由 C 語言實現的。本地方法棧是和虛擬機棧非常相似的一個區域,它服務的對象是 native 方法。你甚至可以認為虛擬機棧和本地方法棧是同一個區域。虛擬機規范無強制規定,各版本虛擬機自由實現 ,HotSpot直接把本地方法棧和虛擬機棧合二為一
線程共享的區域
方法區/永久代
很多開發者都習慣將方法區稱為“永久代”,其實這兩者并不是等價的。HotSpot 虛擬機使用永久代來實現方法區,但在其它虛擬機中,例如,Oracle 的 JRockit、IBM 的 J9 就不存在永久代一說。因此,方法區只是 JVM 中規范的一部分,可以說,在 HotSpot 虛擬機中,設計人員使用了永久代來實現了 JVM 規范的方法區。
方法區主要是用來存放已被虛擬機加載的類相關信息,包括類信息、靜態變量、常量、運行時常量池、字符串常量池。
JVM 在執行某個類的時候,必須先加載。在加載類(加載、驗證、準備、解析、初始化)的時候,JVM 會先加載 class 文件,而在 class 文件中除了有類的版本、字段、方法和接口等描述信息外,還有一項信息是常量池 (Constant Pool Table),用于存放編譯期間生成的各種字面量和符號引用。
字面量包括字符串(String a=“b”)、基本類型的常量(final 修飾的變量),符號引用則包括類和方法的全限定名(例如 String 這個類,它的全限定名就是 Java/lang/String)、字段的名稱和描述符以及方法的名稱和描述符。而當類加載到內存中后,JVM 就會將 class 文件常量池中的內容存放到運行時的常量池中;在解析階段JVM 會把符號引用替換為直接引用(對象的索引值)。例如,類中的一個字符串常量在 class 文件中時,存放在 class 文件常量池中的;在 JVM 加載完類之后,JVM 會將這個字符串常量放到運行時常量池中,并在解析階段,指定該字符串對象的索引值。運行時常量池是全局共享的,多個類共用一個運行時常量池,class 文件中常量池多個相同的字符串在運行時常量池只會存在一份。
方法區與堆空間類似,也是一個共享內存區,所以方法區是線程共享的。假如兩個線程都試圖訪問方法區中的同一個類信息,而這個類還沒有裝入 JVM,那么此時就只允許一個線程去加載它,另一個線程必須等待。在 HotSpot 虛擬機、Java7 版本中已經將永久代的靜態變量和運行時常量池轉移到了堆中,其余部分則存儲在 JVM 的非堆內存中,而 Java8 版本已經將方法區中實現的永久代去掉了,并用元空間(class metadata)代替了之前的永久代,并且元空間的存儲位置是本地。
元空間大小參數:
jdk1.7及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
jdk1.8以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8以后大小就只受本機總內存的限制(如果不設置參數的話)
Java8 為什么使用元空間替代永久代,這樣做有什么好處呢?
官方給出的解釋是:移除永久代是為了融合 HotSpot JVM 與 JRockit VM 而做出的努力,因為 JRockit 沒有永久代,所以不需要配置永久代。永久代內存經常不夠用或發生內存溢出,拋出異常 java.lang.OutOfMemoryError: PermGen。這是因為在 JDK1.7 版本中,指定的 PermGen 區大小為 8M,由于 PermGen 中類的元數據信息在每次 FullGC 的時候都可能被收集,回收率都偏低,成績很難令人滿意;還有,為 PermGen 分配多大的空間很難確定,PermSize 的大小依賴于很多因素,比如,JVM 加載的 class 總數、常量池的大小和方法的大小等。
堆
堆是 JVM 上最大的內存區域,我們申請的幾乎所有的對象,都是在這里存儲的。我們常說的垃圾回收,操作的對象就是堆。堆空間一般是程序啟動時,就申請了,但是并不一定會全部使用。
隨著對象的頻繁創建,堆空間占用的越來越多,就需要不定期的對不再使用的對象進行回收。這個在 Java 中,就叫作 GC(Garbage Collection)。那一個對象創建的時候,到底是在堆上分配,還是在棧上分配呢?這和兩個方面有關:對象的類型和在 Java 類中存在的位置。
在常見的虛擬機中,將堆分為Eden區,From區,To區和老年代四塊區域。
Eden區,From區,To區和老年代內存大小比例為:8:1:1:20
堆大小參數:
-Xms:堆的最小值;
-Xmx:堆的最大值;
-Xmn:新生代的大小;
-XX:NewSize;新生代最小值;
-XX:MaxNewSize:新生代最大值;
例如- Xmx256m
虛擬機配置查看官網:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BABHDABI
可以了解更多虛擬機配置
直接內存
直接內存不是虛擬機運行時數據區的一部分,也不是java虛擬機規范中定義的內存區域;如果使用了NIO,這塊區域會被頻繁使用,在java堆內可以用directByteBuffer對象直接引用并操作;這塊內存不受java堆大小限制,但受本機總內存的限制,可以通過-XX:MaxDirectMemorySize來設置(默認與堆內存最大值一樣),所以也會出現OOM異常。
從底層深入理解運行時數據區
開啟HSDB工具
Jdk1.8啟動JHSDB的時候必須將sawindbg.dll復制到對應目錄的jre下
C:\Program Files\Java\jdk1.8.0_101\lib
執行 java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
當我們通過 Java 運行以上代碼時,JVM 的整個處理過程如下:
- JVM 向操作系統申請內存,JVM 第一步就是通過配置參數或者默認配置參數向操作系統申請內存空間。
- JVM 獲得內存空間后,會根據配置參數分配堆、棧以及方法區的內存大小。
- 完成上一個步驟后, JVM 首先會執行構造器,編譯器會在.java 文件被編譯成.class 文件時,收集所有類的初始化代碼,包括靜態變量賦值語句、靜態代碼塊、靜態方法,靜態變量和常量放入方法區
- 執行方法。啟動 main 線程,執行 main 方法,開始執行第一行代碼。此時堆內存中會創建一個 Teacher 對象,對象引用 student 就存放在棧中。具體的操作如上面的字節碼操作過程。
深入辨析堆和棧
功能
1.以棧幀的方式存儲方法調用的過程,并存儲方法調用過程中基本數據類型的變量(int、short、long、byte、float、double、boolean、char等)以及對象的引用變量,其內存分配在棧上,變量出了作用域就會自動釋放;
2.而堆內存用來存儲Java中的對象。無論是成員變量,局部變量,還是類變量,它們指向的對象都存儲在堆內存中;
線程獨享還是共享
1.棧內存歸屬于單個線程,每個線程都會有一個棧內存,其存儲的變量只能在其所屬線程中可見,即棧內存可以理解成線程的私有內存。
2.堆內存中的對象對所有線程可見。堆內存中的對象可以被所有線程訪問。
空間大小
棧的內存要遠遠小于堆內存
內存溢出
棧溢出
HotSpot版本中棧的大小是固定的,是不支持拓展的。
java.lang.StackOverflowError 一般的方法調用是很難出現的,如果出現了可能會是無限遞歸。
虛擬機棧帶給我們的啟示:方法的執行因為要打包成棧楨,所以天生要比實現同樣功能的循環慢,所以樹的遍歷算法中:遞歸和非遞歸(循環來實現)都有存在的意義。遞歸代碼簡潔,非遞歸代碼復雜但是速度較快。
OutOfMemoryError:不斷建立線程,JVM申請棧內存,機器沒有足夠的內存。
堆溢出
內存溢出:申請內存空間,超出最大堆內存空間。
方法區溢出
1.運行時常量池溢出
2.方法區中保存的Class對象沒有被及時回收掉或者Class信息占用的內存超過了我們配置。
注意Class要被回收,條件比較苛刻(僅僅是可以,不代表必然,因為還有一些參數可以進行控制):
(1)該類所有的實例都已經被回收,也就是堆中不存在該類的任何實例。
(2)加載該類的ClassLoader已經被回收。
(3)該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
本機直接內存溢出
接內存的容量可以通過MaxDirectMemorySize來設置(默認與堆內存最大值一樣),所以也會出現OOM異常;由直接內存導致的內存溢出,一個比較明顯的特征是在HeapDump文件中不會看見有什么明顯的異常情況,如果發生了OOM,同時Dump文件很小,可以考慮重點排查下直接內存方面的原因。
虛擬機優化技術
編譯優化技術——方法內聯
方法內聯的優化行為,就是把目標方法的代碼原封不動的“復制”到調用的方法中,避免真實的方法調用而已。
棧的優化技術——棧幀之間數據的共享
在一般的模型中,兩個不同的棧幀的內存區域是獨立的,但是大部分的JVM在實現中會進行一些優化,使得兩個棧幀出現一部分重疊。(主要體現在方法中有參數傳遞的情況),讓下面棧幀的操作數棧和上面棧幀的部分局部變量重疊在一起,這樣做不但節約了一部分空間,更加重要的是在進行方法調用時就可以直接公用一部分數據,無需進行額外的參數復制傳遞了。