阿里架構師帶你深刻淺出jvm

本文跟你們聊聊JVM的內部結構,從組件中的多線程處理,JVM系統線程,局部變量數組等方面進行解析java

JVMweb

JVM = 類加載器(classloader) + 執行引擎(execution engine) + 運行時數據區域(runtime data area)數據庫

下面這幅圖展現了一個典型的JVM(符合JVM Specification Java SE 7 Edition)所具有的關鍵內部組件。編程

阿里架構師帶你深刻淺出jvm

組件中的多線程處理bootstrap

多線程處理」或「自由線程處理」指的是一個程序同時執行多個操做線程的能力。 做爲多線程應用程序的一個示例,某個程序在一個線程上接收用戶輸入,在另外一個線程上執行多種複雜的計算,並在第三個線程上更新數據庫。 在單線程應用程序中,用戶可能會花費時間等待計算或數據庫更新完成。 而在多線程應用程序中,這些進程能夠在後臺進行,所以不會浪費用戶時間。 多線程處理能夠是組件編程中的一個很是強大的工具。經過編寫多線程組件,您能夠建立在後臺執行復雜計算的組件,它們容許用戶界面 (UI) 在計算的過程當中自由地響應用戶輸入。 雖然多線程處理是一個強大的工具,可是要將其正確應用卻比較困難。 未能正確實現的多線程代碼可能下降應用程序性能,或甚至致使應用程序凍結。 下列主題將向您介紹多線程編程的一些注意事項和最佳作法。.NET Framework 提供幾個在組件中進行多線程處理的選項。 System.Threading 命名空間中的功能是一個選項。 基於事件的異步模式是另外一個選項。 BackgroundWorker 組件是對異步模式的實現;它提供封裝在組件中以便於使用的高級功能。數組

JVM系統線程緩存

若是你用jconsole或者任何其餘的debug工具查看,可能會看到有許多線程在後臺運行。這些運行着的後臺線程不包含主線程,主線程是基於執行publicstatic void main(String[]) 的須要而被建立的。而這些後臺線程都是被主線程所建立。在HotspotJVM中主要的後臺系統線程,見下表:安全

VM 線程 該線程用於等待執行一系列可以使得JVM到達一個「safe-point」的操做。

而這些操做不得不發生在一個獨立的線程上的緣由是:它們都要求JVM處於一個——沒法修改堆的safepoint。性能優化

被這個線程執行的該類操做都是「stop-the-world」型的垃圾回收、線程棧回收、線程擱置以及有誤差的鎖定撤銷。服務器

週期性的任務線程 該線程用於響應timer事件(例如,中斷),這些事件用於調度執行週期性的操做
GC 線程 這些線程支持在JVM中不一樣類型的垃圾回收
編譯器線程 它們用於在運行時將字節碼編譯爲本地機器碼
信號分發線程 該線程接收發送給JVM的信號,並經過調用JVM合適的方法進行處理

單個線程

每一個線程的一次執行都包含以下的組件

程序計數器(PC)

除非當前指令或者操做碼是原生的,不然當前指令或操做碼的地址都須要依賴於PC來尋址。若是當前方法是原生的,那麼該PC即爲undefined。全部的CPU都有一個PC,一般PC在每一個指令執行後被增長以指向即將執行的下一條指令的地址。JVM使用PC來跟蹤正在執行的指令的位置。事實上,PC被用來指向methodarea的一個內存地址。

原生棧

不是全部的JVM都支持原生方法,但那些支持該特性的JVM一般會對每一個線程建立一個原生方法棧。若是對JVM的JNI(JavaNative Invocation)採用c連接模型的實現,那麼原生棧也將是一個C實現的棧。在這個例子中,原生棧中參數的順序 、返回值都將跟一般的C程序相同。一個原生方法一般會對JVM產生一個回調(這依賴於JVM的實現)並執行一個Java方法。這樣一個原生到Java的調用發生在棧上(一般在Java棧),與此同時線程也將離開原生棧,一般在Java棧上建立一個新的frame。

每一個線程都有屬於它本身的棧,用於存儲在線程上執行的每一個方法的frame。棧是一個後進先出的數據結構,這可使得當前正在執行的方法位於棧的頂部。對於每一個方法的執行,都會有一個新的frame被建立並被入棧到棧的頂部。當方法正常的返回或在方法執行的過程當中遇到未捕獲的異常時frame會被出棧。棧不會被直接進行操做,除了push/ pop frame 對象。所以能夠看出,frame對象可能會被分配在堆上,而且內存也不必是連續的地址空間(請注意區分frame的指針跟frame對象)。

棧的限制

一個棧能夠是動態的或者是有合適大小的。若是一個線程要求更大的棧,那麼將拋出StackOverflowError異常;若是一個線程要求新建立一個frame,又沒有足夠的內存空間來分配,將會拋出OutOfMemoryError異常。

Frame

對於每個方法的執行,一個新frame會被建立並被入棧到棧頂。當方法正常返回或在方法執行的過程當中遇到未捕獲的異常,frame會被出棧。

局部變量數組

局部變量數組包含了在方法執行期間所用到的全部的變量。包含一個對this的引用,全部的方法參數,以及其餘局部定義的變量。對於類方法(好比靜態方法),方法參數的存儲索引從0開始;而對於實例方法,索引爲0的槽都爲存儲this指針而保留。

操做數棧

操做數棧在字節碼指令被執行的過程當中使用。它跟原生CPU使用的通用目的的寄存器相似。大部分的字節碼都把時間花費在跟操做數棧打交道上,經過入棧、出棧、複製、交換或者執行那些生產/消費值的操做。對字節碼而言,那些在局部變量數組和操做數棧之間移動值的指令是很是頻繁的。

動態連接

每一個frame都包含一個對運行時常量池的引用。該引用指向將要被執行的方法所屬的類的常量池。該引用也用於輔助動態連接。

當一個Java類被編譯時,全部對存儲在類的常量池中的變量以及方法的引用都被當作符號引用。一個符號引用僅僅只是一個邏輯引用而不是最終指向物理內存地址的引用。JVM的實現能夠選擇解析符號引用的時機,該時機能夠發生在當類文件被驗證後、被加載後,這稱之eager或靜態分析;不一樣的是它也能夠發生在當符號引用被首次使用的時候,稱之爲lazy或延遲分析。但JVM必須保證:解析發生在每一個引用被首次使用前,同時在該時間點,若是遇到分析錯誤可以拋出異常。綁定是一個處理過程,它將被符號引用標識的字段、方法或類替換爲一個直接引用。這個處理過程只發生一次,由於符號引用須要被徹底替換。若是一個符號引用關聯着一個類,而該類尚未被解析,那麼該類也會被當即加載。每一個直接引用都被以偏移的方式存儲,該存儲結構關聯着變量或方法的運行時位置。

線程之間共享

  • 堆中某個節點的值老是不大於或不小於其父節點的值;

  • 堆老是一棵徹底二叉樹。

將根節點最大的堆叫作最大堆或大根堆,根節點最小的堆叫作最小堆或小根堆。常見的堆有二叉堆、斐波那契堆等。

堆的定義以下:n個元素的序列{k1,k2,ki,…,kn}當且僅當知足下關係時,稱之爲堆。

(ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4...n/2)

若將和這次序列對應的一維數組(即以一維數組做此序列的存儲結構)當作是一個徹底二叉樹,則堆的含義代表,徹底二叉樹中全部非終端結點的值均不大於(或不小於)其左、右孩子結點的值。由此,若序列{k1,k2,…,kn}是堆,則堆頂元素(或徹底二叉樹的根)必爲序列中n個元素的最小值(或最大值)

非堆式內存

有些對象並不會建立在堆中,這些對象在邏輯上被認爲是JVM機制的一部分。

非堆式的內存包括:

  • 永久代中包含:

  • 方法區

  • 內部字符串

  • 代碼緩存:用於編譯以及存儲方法,這些方法已經被JIT編譯成本地代碼

內存管理

對象和數組永遠都不會被顯式釋放,所以只能依靠垃圾回收器來自動地回收它們。

一般,以以下的步驟進行:

  1. 新對象和數組被建立在年輕代

  2. 次垃圾回收器將在年輕代上執行。那些仍然存活着的對象,將被從eden區移動到survivor區

  3. 主垃圾回收器將會把對象在代與代之間進行移動,主垃圾回收器一般會致使應用程序的線程暫停。那些仍然存活着的對象將被從年輕代移動到老年代

  4. 永久代會在每次老年代被回收的時候同時進行,它們在二者中其一滿了以後都會被回收

JIT編譯

JIT具體的作法是這樣的:當載入一個類型時,CLR爲該類型建立一個內部數據結構和相應的函數,當函數第一被調用時,JIT將該函數編譯成機器語言.當再次遇到該函數時則直接從cache中執行已編譯好的機器語言.

方法區

全部的線程共享相同的方法區。因此,對於方法區數據的訪問以及對動態連接的處理必須是線程安全的。若是兩個線程企圖訪問一個尚未被載入的類(該類必須只能被加載一次)的字段或者方法,直到該類被加載完成,這兩個線程才能繼續執行。

類的文件結構

一個被編譯過的類文件包含以下的結構:

ClassFile
 { u4magic; u2minor_version; u2major_version; u2constant_pool_count; 
cp_infocontant_pool[constant_pool_count – 1]; u2access_flags; 
u2this_class; u2super_class; u2interfaces_count; 
u2interfaces[interfaces_count]; u2fields_count; 
field_infofields[fields_count]; u2methods_count; 
method_infomethods[methods_count]; u2attributes_count; 
attribute_infoattributes[attributes_count];}
magic,

minor_version,

major_version

指定一些信息:

當前類的版本、編譯當前類的JDK版本

constant_pool 跟符號表類似,但它包含更多的數據
access_flags 爲該類提供一組修改器
this_class 爲該類提供徹底限定名在常量池中的索引,例如:org/jamesdbloom/foo/Bar
super_class 提供對其父類的符號引用在常量池中的索引,例如:java/lang/Object
interface 常量池中的數組索引,該數組提供對全部被實現的接口的符號引用
fields 常量池中的數組索引,該數組提供對每一個字段的完整描述
methods 常量池中的數組索引,該數組提供對每一個方法簽名的完整描述,若是該方法不是抽象的或者native的,

那麼也會包含字節碼

attributes 不一樣值的數組,提供關於類的額外信息,包括註解:RetentionPolicy.CLASS以及RetentionPolicy.RUNTIME

可使用javap命令查看被編譯後的java類的字節碼。

下面列出了在該類文件中,使用到的操做碼:

aload_0 該操做碼是形如aload_<n>格式的一組操做碼中其中的一個。

它們都是用來加載一個對象引用到操做數棧。

而「<n>」用於指示要被訪問的對象引用在局部變量數組中的位置,但n的值只能是0,1,2或3。

也有其餘類似的操做碼用來加載非對象引用,如:iload_<n>,lload_<n>,fload_<n>以及dload_<n>

(其中,i表示int,l表示long,f表示float,而d表示double,上面n的取值範圍對這些*load_<n>一樣適用)。

局部變量的索引若是大於3,可使用iload,lload,float,dload和aload加載。

這些操做碼都攜帶要被加載的局部變量在數組中的索引。

ldc 該操做碼用來從運行時常量池取出一個常量壓入操做數棧
getstatic 該操做碼用來從運行時常量池的靜態字段列表入棧一個靜態值到操做數棧
invokespecial

invokevirtual

這些操做碼是一組用來執行方法的操做碼

(總共有:invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual這幾種)。

其中,本例中出現的invokevirtual用來執行類的實例方法;

而invokespecial用於執行實例的初始化方法,同時也用於執行私有方法以及屬於超類但被當前類繼承的方法

(超類方法動態綁定到子類)。

return 該操做碼是一組操做碼(ireturn,lreturn,freturn,dreturn,areturn以及return)中的其中一個。

每一個操做碼,都是類型相關的返回語句。

其中i表明int,l表示long,f表示float,d表示double而a表示一個對象的引用。

沒有標識符做爲首字母的return語句,僅會返回void

就像在其餘通用的字節碼中那樣,以上這些操做碼主要用於跟本地變量、操做數棧以及運行時常量池打交道。

構造器有兩個指令,第一個將「this」壓入到操做數棧,接下來該構造器的父構造器被執行,這一操做將致使this被「消費」,所以this將從操做數棧出棧。

阿里架構師帶你深刻淺出jvm

而對於sayHello()方法,它的執行將更爲複雜。由於它不得不經過運行時常量池,解析符號引用到真實的引用。第一個操做數getstatic,用來入棧一個指向System類的靜態字段out的引用到操做數棧。接下來的操做數ldc,入棧一個字符串字面量「Hello」到操做數棧。最後,invokevirtual操做數,執行System.out的println方法,這將使得「Hello」做爲一個參數從操做數棧出棧,併爲當前線程建立一個新的frame。

阿里架構師帶你深刻淺出jvm

在此我向你們推薦一個架構學習交流羣。交流學習羣號: 744642380, 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良

類加載器

JVM的啓動是經過bootstrap類加載器來加載一個用於初始化的類。在publicstatic void main(String[])被執行前,該類會被連接以及實例化。main方法的執行,將順序經歷加載,連接,以及對額外必要的類跟接口的初始化。

加載: 加載是這樣一個過程:查找表示該類或接口類型的類文件,並把它讀到一個字節數組中。接着,這些字節會被解析以確認它們是否表示一個Class對象以及是否有正確的主、次版本號。任何被當作直接superclass的類或接口也一同被加載。一旦這些工做完成,一個類或接口對象將會從二進制表示中建立。

連接: 連接包含了對該類或接口的驗證,準備類型以及該類的直接父類跟父接口。簡而言之,連接包含三個步驟:驗證、準備以及解析(optional)

驗證:該階段會確認類以及接口的表示形式在結構上的正確性,同時知足Java編程語言以及JVM語義上的要求。

在驗證階段執行這些檢查意味着在運行時能夠免去在連接階段進行這些動做,雖然拖慢了類的加載速度,然而它避免了在執行字節碼的時候執行這些檢查。

準備:包含了對靜態存儲的內存分配以及JVM所使用的任何數據結構(好比方法表)。靜態字段都被建立以及實例化爲它們的默認值。然而,沒有任何實例化器或代碼在這個階段被執行,由於這些任務將會發生在實例化階段。

解析:是一個可選的階段。該階段經過加載引用的類或接口來檢查符號引用是否正確。若是在這個點這些檢查沒發生,那麼對符號引用的解析會被推遲到直到它們被字節碼指令使用以前。

實例化 類或接口,包含執行類或接口的實例化方法:<clinit>

阿里架構師帶你深刻淺出jvm

在JVM中存在多個不一樣職責的類加載器。每個類加載器都代理其已被加載的父加載器(除了bootstrap類加載器,由於它是根加載器)。

Bootstrap類加載器:當java程序運行時,java虛擬機須要裝載java類,這個過程須要一個類裝載器來完成。而類裝載器自己也是一個java類,這就出現了相似人類的第一位母親是如何產生出來的問題。

其實,java虛擬機中內嵌了一個稱爲Bootstrap的類裝載器,它是用特定於操做系統的本地代碼實現的,屬於java虛擬機的內核,這個Bootstrap類裝載器不用專門的類裝載器去裝載。Bootstrap類裝載器負責加載java核心包中的類。

Extension 類加載器:從標準的Java擴展API中加載類。例如,安全的擴展功能集。

System 類加載器:這是應用程序默認的類加載器。它從classpath中加載應用程序類。

用戶定義的類加載器:能夠額外得定義類加載器來加載應用程序類。用戶定義的類加載器可用於一些特殊的場景,好比:在運行時從新加載類或將一些特殊的類隔離爲多個不一樣的分組(一般web服務器中都會有這樣的需求,好比Tomcat)。

更快的類加載

一個稱之爲類數據共享(CDS)的特性自HotspotJVM 5.0開始被引進。在安裝JVM期間,安裝器加載一系列的Java核心類(如rt.jar)到一個通過映射過的內存區進行共享存檔。CDS減小了加載這些類的時間從而提高了JVM的啓動速度,同時容許這些類在不一樣的JVM實例之間共享。這大大減小了內存碎片。

方法區的位置

JVM Specification Java SE 7 Edition清楚地聲明:儘管方法區是堆的一個邏輯組成部分,但最簡單的實現多是既不對它進行垃圾回收也不壓縮它。然而矛盾的是利用jconsole查看Oracle的JVM的方法區(以及CodeCache)是非堆形式的。OpenJDK代碼顯示CodeCache相對ObjectHeap而言是VM中一個獨立的域。

類加載器引用

類一般是按需加載,即第一次使用該類時才加載。因爲有了類加載器,Java運行時系統不須要知道文件與文件系統。

運行時常量池

JVM對每一個類型維護着一個常量池,它是一個跟符號表類似的運行時數據結構,但它包含了更多的數據。Java的字節碼須要一些數據,一般這些數據會由於太大而難以直接存儲在字節碼中。取而代之的一種作法是將其存儲在常量池中,字節碼包含一個對常量池的引用。運行時常量池主要用來進行動態連接。

幾種類型的數據會存儲在常量池中,它們是:

  • 數值字面量

  • 字符串字面量

  • 類的引用

  • 字段的引用

  • 方法的引用

若是你編譯下面的這個簡單的類:

package org.jvminternals;public class SimpleClass { public void sayHello() {System.out.println("Hello");}}

生成的類文件的常量池,看起來會像下圖所示:

Constant
 pool: #1 = Methodref #6.#17 // java/lang/Object."<init>":()V#2 = 
Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;#3 = 
String #20 // "Hello"#4 = Methodref #21.#22 // 
java/io/PrintStream.println:(Ljava/lang/String;)V#5 = Class #23 // 
org/jvminternals/SimpleClass#6 = Class #24 // java/lang/Object#7 = Utf8 
<init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11
 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 
Lorg/jvminternals/SimpleClass; #14 = Utf8 sayHello #15 = Utf8 SourceFile
 #16 = Utf8 SimpleClass.java #17 = NameAndType #7:#8 // 
"<init>":()V#18 = Class #25 // java/lang/System#19 = NameAndType 
#26:#27 // out:Ljava/io/PrintStream;#20 = Utf8 Hello #21 = Class #28 // 
java/io/PrintStream#22 = NameAndType #29:#30 // 
println:(Ljava/lang/String;)V#23 = Utf8 org/jvminternals/SimpleClass #24
 = Utf8 java/lang/Object#25 = Utf8 java/lang/System #26 = Utf8 out#27 = 
Utf8 Ljava/io/PrintStream; #28 = Utf8 java/io/PrintStream #29 = Utf8 
println #30 = Utf8 (Ljava/lang/String;)V

常量池中包含了下面的這些類型:

Integer 一個4字節的int常量
Long 一個8字節的long常量
Float 一個4字節的float常量
Double 一個8字節的double常量
String 一個String字面值常量指向常量池中另外一個包含最終字節的UTF8記錄
Utf8 一個字節流表示一個Utf8編碼的字串序列
Class 一個Class字面值常量指向常量池中的另外一個Utf8記錄,它包含JVM內部格式的徹底限定名

(它用於動態連接)

NameAndType 用一個冒號區分一對值,每一個值都指向常量池中得其餘記錄。

冒號前的第一個值指向一個utf8字符串字面量表示方法名或者字段名。

第二個值指向一個utf8字符串字面量表示類型。

舉一個字段的例子是徹底限定的類名;

舉一個方法的例子是: 它是一個列表,該列表中每一個參數都是徹底限定的類名

Fieldref,

Methodref,

InterfaceMethodref

用點來分隔的一對值,每一個值指向常量池中的另外一個記錄。

點前的第一個值指向一個Class記錄。第二個值指向一個NameAndType記錄

異常表

異常表存儲了每一個異常處理器的信息:

  • 起始點

  • 終止點

  • 處理代碼的PC偏移量

  • 被捕獲的異常類的常量池索引

若是一個方法定義了try-catch或try-finally異常處理器,那麼一個異常表將會被建立。它包含了每一個異常處理器的信息或者finally塊以及正在被處理的異常類型跟處理器代碼的位置。

當一個異常被拋出,JVM會爲當前方法尋找一個匹配的處理器。若是沒有找到,那麼該方法最終會唐突地出棧當前stackframe而異常會被從新拋出到調用鏈(新的frame)。若是在全部的frame都出棧以前仍是沒有找到異常處理器,那麼當前線程將會被終止。固然這也可能會致使JVM被終止,若是異常被拋出到最後一個非後臺線程的話,好比該線程就是主線程。

最終異常處理器會匹配全部的異常類型而且不管何時該類型的異常被拋出老是會獲得執行。在沒有異常拋出的例子中,finally塊仍然會在方法的最後被執行。一旦return語句被執行就會當即跳轉到finally代碼塊繼續執行。

字符比較

字符比較(character comparison)是指按照字典次序對單個字符或字符串進行比較大小的操做,通常都是以ASCII碼值的大小做爲字符比較的標準。

符號表

符號表在編譯程序工做的過程當中須要不斷收集、記錄和使用源程序中一些語法符號的類型和特徵等相關信息。這些信息通常以表格形式存儲於系統中。如常數表、變量名錶、數組名錶、過程名錶、標號表等等,統稱爲符號表。對於符號表組織、構造和管理方法的好壞會直接影響編譯系統的運行效率。

在JVM中,內部字符串被存儲在字符串表中。字符串表是一個hashtable映射對象指針到符號(好比:Hashtable<oop,Symbol>),它被存儲在永久代裏。

當類被加載時,字符串字面量會被編譯器自動「內部化」而且被加入到字符表。另外字符串類的實例能夠經過調用String.intern()來明確地內部化。當String.intern()被調用,若是符號表裏已經包含該字符串,那麼指向該字符串的引用將被返回。若是該字符串沒有包含在字符表,則會被加入到字符串表同時返回其引用。

在此我向你們推薦一個架構學習交流羣。交流學習羣號: 744642380, 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良

相關文章
相關標籤/搜索