JAVA運行時簡述(HotSpot)

本文簡單介紹HotSpot虛擬機運行時子系統,內容來自不一樣的版本,所以可能會與最新版本之間(當前爲JDK12)存在一些偏差。java

  • 1.命令行參數處理

    HotSpot虛擬機中有大量的可影響性能的命令行屬性,可根據他們的消費者進行簡單分類:執行器消費(如-server -client選項),執行器處理並傳遞給JVM,直接由JVM消費(大多)。
    這些選項可分爲三個主要的類別:標準選項,非標準選項,開發者選項。標準選項是指全部的JVM不一樣實現都可以處理且在不一樣版本之間穩定可用的選項(可是也能夠deprecated)。
    以-X開頭的選項是非標準選項(不保證全部JVM虛擬機的實現均支持),後續的JAVA SDK更新也不保證會對它進行通知。使用-XX開頭的爲開發者選項,它通常須要特定的系統環境(以保證明現正確的操做)和足量的權限(以訪問系統配置參數),這些實現應當慎重使用,相應的選項在更新後也並不保證通知到用戶。
    命令行參數控制了JVM內部變量的屬性值,這些參數同時具有"類型"與"值"。對於布爾型的屬性值,以+或-置於參數以前,可分別表示該屬性的值爲true或false。對於須要其餘數據的變量,一樣有許多機制能夠設置其值數據(很遺憾並不統一),一部分參數在格式上要求在屬性名稱後直接跟隨屬性值,有些不須要分隔符,有些又不得不加上分隔符,分隔符又多是":","="等,如-XX:+OptionName, -XX:-OptionName, and -XX:OptionName=。
    大多整型的參數(如內存大小)可接受'k','m','g'分別表明kb,mb,gb這種簡寫形式。c++

  • 2.虛擬機運行週期

    執行器
    HotSpot虛擬機有幾種Java標準版的執行器,在unix系統上即java命令,在windows系統下即java和javaw(javaw實際上是指基於網絡的執行器)。從屬於虛擬機啓動過程的執行器操做有:
    a.解析命令行選項,部分選項直接由執行器自身消費,如-client和-sever屬性被用來決斷加載合適的vm庫,其餘的屬性則做爲虛擬機初始化參數(JavaVMInitArgs)傳遞給vm。
    b.若是未明確指定選項,執行器來肯定堆的大小和編譯器類型(是client仍是server)。
    c.確立如LD_LIBRARY_PATH 和 CLASSPATH等環境變量。
    d.若是未在命令行中明確指定主類,執行器會從jar文件清單中找出主類名稱。
    e.執行器會在一個新建立的線程(非原生線程)中使用JNI_CreateJavaVM來建立虛擬機實例。 注意,在原生線程中建立vm會極大的減小定製vm的可能性,如windows中的棧大小等。
    f.一旦vm建立並初始化成功,加載主類成功,執行器可從主類中獲得main方法的屬性,而後使用CallStaticVoidMethod執行主方法並以命令行參數爲它的方法入參。
    g.當java主方法執行完成時,檢查和清理任何可能已發生的掛起的異常,返回退出狀態。它會使用ExceptionOccurred來清理異常,方法若是執行成功,它會給調用進程返回一個0值,不然爲其餘值。
    h.使用DetachCurrentThread解除主線程的關聯,這樣減小了線程的數量,保證可安全調用DestroyJavaVM,它也能保證線程不在vm中執行操做,棧中再也不有存活的棧楨。程序員

    最重要的兩個階段是JNI_CreateJavaVM以及DestroyJavaVM,下面詳述。
    JNI_CreateJavaVM執行步驟:
    首先保證沒有兩個線程同時調用此方法,從而保證不在同一進程中出現兩個vm實例。當在一個進程空間中達到一個初始化點時,該進程空間中不能再建立vm,該點也被稱爲「不返回的點」(point of no return)。緣由在於此時vm已建立的靜態數據結構不可以從新初始化。
    接下來,要對JNI的版本支持進行檢查,檢測gc日誌的ostream是否初始化。此時會初始化一些操做系統模塊,如隨機數生成器,當前進程號,高分辨率的時間,內存頁大小和保護頁等。
    解析傳入的參數和屬性值,並存放留用。初始化java標準系統屬性。
    基於上一步解析的參數和屬性進一步建立和初始化系統模塊,這一次的初始化是爲同步,棧內存,安全點頁作準備。在此時如libzip,libhpi,libjava,libthread等庫也完成了加載,同時完成信號句柄的初始化和設定,並初始化線程庫。
    接下來初始化輸出流日誌,任何須需的代理庫(hprof jdi等)均於此時完成初始化和開啓。
    完成線程狀態的初始化以及持有了線程操做所需的指定的數據的線程本地存儲(TLS Thread Local Storage)的初始化。
    全局數據的初始化,如事件日誌,操做系統同步,性能內存(perfMemory),內存分配器(chunkPool)等。
    到此時開始建立線程,會建立java版本的主線程並綁定到一個當前操做系統線程上。然而這個線程還不能被Threads線程列表感知,完成java級別的線程初始化和啓用。
    緊接着進行餘下部分的全局模塊的初始化,它們包括啓動類加載器(BootClassLoader),代碼緩存(CodeCache),解釋器(Interpreter),編譯器(Compiler),JNI,系統字典(SystemDictionary),Universe。此時便已到達前述的「不返回的點」,也就是說,咱們此時已不能在進程的地址空間中再建立一個vm實例了。
    主線程會在此時被加入到線程列表中,這一步首先要對Thread_Lock進行加鎖操做。此處會對Universe(戲稱爲小宇宙,即所需的全局數據結構)進行健全檢查。此時建立執行全部重要vm函數的VMThread,建立完成後,即達到一個合適的點,在這個點,可發出適當的JVMTI事件通知當前jvm的狀態。
    加載並初始化一些類,包含java.lang.String,java.lang.System,java.lang.Thread,java.lang.ThreadGroup,java.lang.reflect.Method,java.lang.ref.Finalizer,java.lang.Class,以及系統類中的其餘成員。在這一刻,vm已經完成初始化而且可操做,可是並未具有完整的功能。
    到這一步,信號處理器線程也被開啓,同時也完成了啓動編譯器線程和CompileBroker線程,以及StatSampler和WatcherThreads等輔助線程,此時vm具有了完整的功能,生成JNIEnv信息並返回給調用者,此時的vm已經準備就緒,可服務新的JNI請求。算法

    DestroyJavaVM執行步驟segmentfault

    DestroyJavaVM的調用有兩種狀況:執行器調用它拆解vm,或vm自身在出現嚴重錯誤時調用。拆解虛擬機的基本步聚以下:
    首先,要等待到自身成爲惟一一個正在運行的非守護線程時,在整個等待過程當中,虛擬機仍舊是可工做的。
    調用java.lang.Shutdown.shutdown()方法,它會執行java級別的關閉勾子方法,若是有退出終結器可用,運行相應的終結器(finalizer)。
    調用before_exit()爲vm退出作出準備,運行vm級別的關閉勾子(它們是用JVM_OnExit()註冊的),中止剖析器(Profiler),採樣器(StatSampler),Watcher和GC線程。將相應的事件發送給JVMTI/PI,禁用JVMPI,並終止信號線程。
    調用JavaThread的exit方法,釋放JNI句柄塊,移除棧保護頁,把此線程從線程列表中移除,從這個點起,任何java代碼不可被執行。
    終止vm線程,它會把當前的vm帶到安全點並終止編譯器線程。在安全點,應注意任何可能會在安全點阻塞的功能都不可以使用。
    禁用JNI/JVM/JVMPI屏障的追蹤。
    給native代碼中依舊在運行的線程設置_vm_existed標記。
    刪除這個線程。
    調用exit_globals刪除IO和PerfMemory資源。
    返回調用者。windows

  • 3.虛擬機類加載(class loading)

    虛擬機要負責常量池符號的解析,它須要對有關的類和接口前後進行裝載(loading),連接(linking)而後初始化。通常用「類加載機制」來描述把一個類或接口的名稱映射到一個class對象的過程,相應的,JVMS定義了詳細的裝載,連接和初始化階段的協議。
    類的加載是在字節碼解析過程當中完成的,典型是當一個類文件中的常量池符號須要被解析時。有一些JAVA的api會觸發這個過程,如Class.forName(),classLoader.loadClass(),反射api,以及JNI_FindClass均能初始化類的加載。虛擬機自身也能初始化類加載。虛擬機會在啓動時加載如Object,Thread等核心類。裝載一個類須要裝載全部的超類和超接口。且對於連接階段的類文件驗證過程,可能須要裝載額外的類。
    虛擬機和JAVA SE類加載庫共同承擔了類的加載,虛擬機執行了常量池的解析,類和接口的連接和初始化。加載階段是vm和特定的類加載器(java.lang.ClassLoader)之間的一個協做過程。api

    類加載階段數組

    裝載階段,根據類或接口的名稱,在類文件中找出二進制語義,定義類並建立java.lang.Class對象。若是在類文件中找不到二進制表示,則拋出NoClassDefFound錯誤。此外裝載階段也作了一些類文件的在語法上的格式檢查,檢查不經過會拋出ClassFormatError或UnsupportedClassVersionError。在完成裝載以前,vm必須載入全部的超類和超接口。若是類繼承樹存在問題,如類直接或間接地本身繼承或實現本身,則vm會拋出ClassCircularityError。若vm發現類的直接接口不是接口,或者直接父類是一個接口,則會拋出IncompatibleClassChangeError。
    類加載的連接階段首先作一些校驗,它會檢測類文件的語義,常量池符號以及類型檢測,這個過程可能會拋出VerifyError。連接階段接下來進行一些準備工做,它會爲靜態字段進行建立和初始化標準默認值,並分配方法表。注意,到此步爲止不會進行任何java代碼的執行。以後連接階段還有一個可選的步驟,即符號引用的解析。
    接下來是類的初始化階段,它會運行類的靜態初始化器,初始化類的靜態字段。這是類的java代碼的第一次執行。注意類的初始化須要超類的初始化,但不包含超接口的初始化。
    JAVA虛擬機規範(JVMS)規定了類的初始化發生在類的第一次「活化使用」,java語言規範(JLS)容許連接階段的符號解析過程在不破壞java語義前提下的靈活性,裝載,連接和初始化的每個步驟都要在前一步驟完成後進行。爲了性能考慮,HotSpot虛擬機通常會等到要去初始化一個類時纔會去進行類的裝載和連接。因此舉個簡單的例子,若是類A引用了類B,那麼加載類A將不會必然致使B的加載,除非在驗證階段必需。當執行了第一個引用B的指令時,將會致使B的初始化,而這又須要先對類B進行裝載和連接。緩存

    類加載的委託機制
    當一個加載器被要求查找和加載一個class時,它能夠請求另外一個類加載器去作實際的加載工做。這個機制被稱爲加載委託。第一個類加載器是一個「初始化加載器」,而最終定義了該類的類加載器被稱爲「定義加載器」,在字節碼解析的例子中,初始化加載器負責該類的常量池符號的解析。
    類加載器是分層定義的,每一個類加載器可有委託的雙親。委託機制定義了二進制類表示的檢索順序。JAVASE類加載器按層序檢索啓動類加載器,擴展類加載器和系統類加載器。系統類加載器同時也是默認的應用類加載器,它會運行main方法並從類路徑下加載類。應用類加載器能夠是JAVASE 類加載器庫中的實現,也能夠由應用開發人員實現。JAVASE類庫實現了擴展類加載器,它負責加載jre下lib/ext目錄中的類。
    做者在「54個JAVA官方文檔術語」一文中曾說過,這一機制已經不適用於JAVA9以上版本的描述,若是去查詢有關文章,能夠發現這個經典的類加載委託機制其實已經歷過三次破壞(委託機制出廠時晚於加載器自己,破壞一;線程上下文類加載器,破壞二;熱部署的後門,破壞三),而做者我的認爲類加載器支持JAVA9以後的模塊路徑的加載也是一種破壞,它們之間再也不是簡單的委託加載,也不只從類路徑下加載,不一樣路徑加載到的模塊也有不一樣的處理機制,詳細描述見該文。
    啓動類加載器是由vm實現的,它從BOOTPATH下加載類,包含rt.jar中的類定義。爲了快速啓動,vm也會經過類數據共享(cds)來處來類的預加載。關於cds,在最新的幾版jdk中有所更新,咱們在稍後的章節中簡述。tomcat

    類型安全
    類或者接口名是由包含包名稱的全限定名定義的。一個類的類型由該全限定名和類加載器所惟必定義,因此類加載器其實能夠理解爲一個名稱空間,兩個不一樣類加載器定義的同一個類實際上會是兩個class類型。
    vm會對自定義類加載器進行限定,保證不能與類型安全發生衝突。當類A中調用類B的方法時,vm經過追蹤和檢查加載器約束保證兩個類的加載器在方法參數和返回值上協商一致。

    HotSpot中的類元數據
    類加載的結果是在永久代(舊版)建立一個instanceKlass或者arrayKlass。instanceKlass指向一個java.lang.Class的實例,虛擬機c++代碼經過klassOop訪問instanceClass。
    HotSpot內部類加載數據

    HotSpot虛擬機爲了追蹤類加載而維護了三張主哈希表。分別是SystemDictionary表,它包含被加載的類,它們映射鍵爲一個類名/類加載器對,值爲一個klassOop,它同時包含了類名/初始化加載器對和類名/定義加載器對,目前只有在安全點才能夠移除它們;PlaceholderTable表,它包含當前正在被載器的類,它被用於前述ClassCircularityError檢查和支持多線程類加載的加載器進行並行加載;LoaderConstraintTable,它追蹤類型安全檢查約束。這些哈希表都由一個鎖SystemDictionary_lock來保護,通常狀況下vm中的類加載階段是使用類加載器對象鎖串行執行的。

  • 4.字節碼驗證和格式檢查

    JAVA語言是類型安全的,標準的java編譯器會生產可用的類文件和類型安全的代碼,可是jvm不能保證代碼是由可信任的編譯器生成的,所以它必須在連接時進行字節碼校驗(bytecode verification)重建類型安全。
    字節碼校驗的規範詳見java虛擬機規範的4.8節。規範中規定了JVM校驗的代碼動態和靜態約束。若是發現了任何與約束衝突的地方,虛擬機將會拋出VerifyError並阻斷類的連接。
    可靜態檢查的字節碼約束有不少,'ldc'碼(Low Disparity Code 低差異編碼)的操做數必須爲一個可用的常量池索引,它的類型是CONSTANT_Integer, CONSTANT_String 或 CONSTANT_Float。其餘指令須要的檢查參數類型和個數的約束須要對代碼動態分析,這樣來決定執行時哪一個操做數可出如今表達式棧。
    目前,有兩種辦法(截止1.6)分析字節碼並決定在每一條指定中出現的操做數類型和個數。傳統的辦法被稱做「類型推斷」,它經過對每一個字節碼進行抽象解釋,在代碼的分支處或異常句柄處進行類型狀態的合併。整個分析過程會迭代所有的字節碼,直到發現這些類型的「穩態」。若是不能達到穩態,或者結果類型與一些字節碼的約束衝突,那麼拋出VerifyError。這一步的驗證代碼位於外部庫libverify.so中,它使用JNI去收集所需的類和類型的信息。
    在JDK6中出現了第二種被稱爲「類型驗證「的方法,在這種方法中,java編譯器經過代碼屬性,StackMapTable來提供每個分支和異常目標的穩態類型信息。StackMapTable包含大量的棧圖楨,每個楨表示方法的某一個偏移量的表達式棧和局部變量表中的條目類型。jvm接下來只須要遍歷字節碼並驗證字其中的類型正確性。這是一個已經在JAVAME CLDC中使用的技術。由於它小而快,此驗證方法vm自身便可構建。
    對於全部版本號低於50,建立早於JDK6的類文件,jvm會使用傳統的類型推薦方式驗證類文件,不然會使用新辦法。

  • 5.類數據共享(cds)

    類數據共享是一個JDK5引入的功能,旨在提升java程序語言應用的啓動時間,尤爲是小型應用,同時,它也能減小內存佔用。當jre安裝在32位系統時而且使用sun提供的安裝器時,安裝器會從系統jar中載入一組類並生成一種內部的格式,而後轉儲爲一個文件,這個文件被稱做」共享存檔「。若是沒有使用sun提供的jre安裝器,也能夠手動執行。在後續的jvm執行時,這個共享存檔文件被映射進內存,節省了其餘jvm裝載類和元數據的時間。
    目前官方對於cds的文檔未整理完善,在截止到JAVA8的有關文檔中,仍能夠見到這樣一句描述:Class data sharing is supported only with the Java HotSpot Client VM, and only with the serial garbage collector,即類共享目前只在HotSpot client虛擬機中支持,且只能使用serial垃圾收集器。而在JAVA9-12的若干新特性中,也對cds有過一些更新描述,如JAVA10中對類數據的共享包含了應用程序的類,在JAVA11中模塊路徑也支持了cds。但做者並未在專門的垃圾收集器中找到大篇幅的詳述,不過根據jdk12的jvm文檔中介紹,cds已經在 G1, serial, parallel, 和 parallelOldGC 幾種垃圾收集器中支持,且默認使用G1的128M堆內存。且G1在JDK7中已出現,在JDK9中已經成爲默認的垃圾收集器,parallel 出現的相對更早,所以做者嚴重懷疑JAVA8中相應文檔描述的準確性,好在咱們能夠直接去看最新版。
    cds能夠減小啓動時間,由於它減小了裝載固定的類庫的開銷,應用程序相對於使用的核心類越小,cds就至關節省了越多的啓動時間。cds同時也有兩種方式減小了jvm實例的內存佔用。首先,一部分共享存檔文件被映射進內存並做爲只讀的庫,多個jvm進程不須要重複佔用進程的內存空間;其次,由於共享存檔文件中包含的類數據已是jvm使用的格式,處理rt.jar(低於9的版本)所需的額外內存開銷也能夠省去了,這使得多個應用在同一機器上可以更優的併發執行。
    在HotSpot虛擬機中,類共享的實現實際是在永久代(元空間)中開闢了新的內存區域存放共享數據。存檔文件名爲」classes.jsa「,它會在vm啓動時映射進這個空間。後續的管理由vm內存管理子系統負責。
    共享數據是隻讀的,它包含常量方法對象(constMethodOops),符號對象(symbolOops),基本類型數組,多數字符數組。可讀寫的共享數據包含可變的方法對象(methodOops),常量池對象(constantPoolOops),vm內部的java類和數組實現(instanceKlasses和arrayKlasses), 以及大量的String,Class和Exception對象。
    做者看來,近幾版的jdk關於cds的幾處更新明顯借鑑了一些如tomcat等服務器的機制,適配愈來愈多的雲生產環境,減小內存開銷和啓動開銷都是爲雲用戶省錢的方式。

  • 6.解釋器

    當前HotSpot解釋器是一個基於模板的解釋器,它被用來執行字節碼。HotSpot在啓動時運行時用InterpreterGenerator在內存中利用TemplateTable(每一個字節碼有關的彙編代碼)中的信息生成一個解釋器實例。模板是每一個字節碼的描述,模板表定義了全部模板並提供了獲取指定字節碼的訪問方法。在jvm啓動時,可以使用-XX:+PrintInterpreter打印有關的模板表信息。
    執行效果上看,模板好於經典的switch語句循環的方式,緣由也很簡單,首先switch語句執行重複的比較操做來獲得目標字節碼,最極端狀況它可能須要對一個給定的指定比較全部的字節碼;第二,模板使用共享的棧來傳遞java參數,同時本地c方法棧被vm自身來使用,大量的jvm內部變量是用c變量存放的(如線程的程序計數器或棧指針),它們不保證永久存放在硬件寄存器中,管理這些軟件的解釋結構會消耗總執行時間中的至關可觀的一部分。
    從全局來看,HotSpot解釋器大幅彌合了虛擬機和實體機器之間的裂縫,它大大加快了解釋的時間,可是犧牲了不少代碼的機器塊,同時也增大了代碼大小和複雜度,也須要一些代碼的動態生成。很明顯,debug機器動態生成的代碼要比靜態代碼更加困難。
    對於一些對彙編語言來講過於複雜的操做,如常量池的查找,解釋器會運行時調用vm來完成。
    HotSpot解釋器也是整個HotSpot自適應優化歷史中重要的一部分,自適應優化解決了JIT編譯的問題,大部分狀況下,幾乎全部的程序都是用大量時間執行極少許的代碼,所以運行時不須要逐方法編譯,vm僅使用解釋器來當即運行程序,分析代碼在程序中運行的次數,避免編譯不頻繁運行的程序代碼(大多數),這樣HotSpot編譯器能夠專一於程序中最須要性能優化的部分,並不增長全局的編譯時間,在程序持續運行期間進行動態的監控,達到最適應用戶須要的目的。

  • 7.JAVA異常處理

    jvm使用異常做爲一個信號,它說明程序中出現了與java語言語義相沖突的事件,數組越界是一個極簡的案例。異常會致使控制流從異常發生或拋出的點轉到程序指定的處理點或捕獲點的一次非本地轉換。HotSpot解釋器和動態編譯器在運行時協做實現了異常的處理。異常處理有兩種簡單案例,異常拋出並由同一方法捕獲,異常拋出並由調用者捕獲。後一種狀況稍微複雜一些,由於須要展開棧來找出恰當的處理者。
    要初始化一個異常有多個方式,如throw字節碼,從vm內部調用中返回,JNI調用中返回,或java調用中返回,最後一個狀況實際上是前三者的後一階段。當vm意識到有異常拋出時,執行運行時系統去找出該異常最近的處理器,這一過程會用到三片信息:當前方法,當前字節碼,異常對象。若是當前方法沒有找處處理器,如上面提到的,將當前活化的棧楨出棧,進程將在此前的棧楨中迭代重複上述步驟。一旦找到了合適的處理器,vm更新執行狀態,跳轉到相應的處理器,java代碼在相應位置繼續執行。

  • 8.同步

    普遍來說,能夠把「同步」定義爲一個阻止或恢復不恰當的併發交互(通常稱爲競態)的一個機制。在java中,併發經過線程來表示,鎖排他是java中常見的一個同步案例,這一過程當中,只有一個線程同時被容許訪問一段保護的代碼或數據。
    HotSpot提供了java監視器的概念,線程可經過監視器來排它的運行應用代碼。監視器只有兩個狀態:鎖或者未鎖,一個線程能夠在任什麼時候間持有(鎖住)監視器。只有在獲取了監視器後,線程才能進入被監視器保護的代碼塊。在java中這類被監視器保護的代碼塊稱爲同步代碼塊。
    無競態的同步包含了大多的同步狀況,它由常量時技術實現。java對於同步機制作了大量的優化,偏向鎖技術是其中之一,由於大多數的對象一輩子只被最多一個線程持有鎖,所以容許該線程將監視器偏向給本身,一旦偏向,該線程後續鎖和解鎖再也不須要額外又昂貴的原子指令開銷。
    對於有競態的同步操做場景,使用高級自適應自旋技術提升吞吐量。即便此時應用中有大量的競態,在經歷這些優化後,同步操做性能已經大幅提高,從jdk6開始,它再也不是如今的real-world程序中的重大問題。
    在HotSpot中,大多的同步操做是由一種被稱做「fast-path」(快路)代碼的調用完成的。有兩種即時編譯器(JIT)和一個解釋器,它們均可以產生快路代碼。兩種編譯器分別是C1,即-client編譯器,以及C2,即-server編譯器。C1和C2均直接在同步點生成快路代碼。在通常沒有競態的狀況下,同步操做將會徹底在快路中執行,然而當發現須要去阻塞或者喚醒一個線程時(如monitorenter monitorexit),將會進入slow-path執行,它由本地C++代碼實現。
    單個對象的同步狀態是在對象中的第一個word中編碼存放的(mark word,詳見前面的文章「54個JAVA官方文檔術語」)。mark word對同步狀態元數據來講是多用的(其實mark word自己也是多用的,它還包含gc分代數據,對象的hash碼值)。這些狀態包含:
    Neutral(中立): 未鎖
    Biased(偏向): 鎖/未鎖+非共享
    Stack-Locked(棧鎖): 鎖+共享 無競態
    Inflated(膨脹鎖): 鎖/未鎖+共享和競態

  • 9.線程管理

    線程管理覆蓋線程從建立到銷燬的整個生命週期,並負責在vm內協調各個線程。這個過程包含java代碼建立的線程(應用代碼或庫代碼),綁定到vm的本地線程,出於各類目的建立的vm內部線程。線程管理在絕大多數狀況下是獨立於運行平臺的,但仍有一些細節與所運行的操做系統有所關聯。
    線程模型
    在hotspot虛擬機中,java線程和操做系統線程是一對一映射的關係,java線程即一個java.lang.Thread實例,當它被開啓(start)後,本地線程也隨之建立,當它終止(terminated)時,本地線程回收。操做系統負責調度全部的線程以及派發可用的cpu資源。java線程的優先級以及操做系統線程的優先級機制很是複雜,在不一樣的操做系統中表現也差別極大,此處略。

    線程建立和銷燬

    有兩種辦地能夠向虛擬機中引入一個線程:執行java.lang.Thread對象的start方法;或使用JNI將一個已存在的本地線程綁定到vm。出於一些目的,vm內部也有一些辦法建立線程,本處不予討論。
    在vm的一個線程上實際上關聯了若干個對象(HotSpot虛擬機是由面向對象的c++實現),具體有:
    a.java.lang.Thread實例表現java代碼中的一個線程。
    b.JavaThread實例表示vm中的一個java.lang.Thread,JavaThread是Thread的子類,它包含額外的用以追蹤線程狀態的信息。一個JavaThread實例持有關聯的java.lang.Thread對象的引用(指針),同時持有OSThread實例的引用。java.lang.Thread也持有JavaThread的引用(以一個整數表示)。
    c.OSThread(直譯爲操做系統線程)實例表示了一個操做系統的線程,它包含額外的可用於追蹤線程狀態的操做系統級別的信息。OSThread包含了一個平臺指定的可用於定位真實操做系統線程的句柄。
    當java.lang.Thread實例啓動,vm建立關聯的JavaThread和OSThread對象,並最終建立了一個本地線程。在準備好全部vm狀態後(如線程本地存儲,分配緩存,同步對象等)以後,本地線程得以啓動。本地線程完成初始化並執行一個start-up方法,它會導向java.lang.Thread對象的run方法。隨後,在該方法返回或拋出未捕獲的異常時終止線程,而且在終止時與vm交互,這個過程是用以判斷是否它此時也須要終止vm。線程終止會釋放掉全部關聯的資源,從已知線程集中移除掉JavaThread實例,執行OSThread實例和JavaThread實例的銷燬過程,並最終中止startup 方法的執行。
    可以使用JNI調用AttachCurrentThread把本地線程綁定到虛擬機。做爲此方法的響應,OSThread和JavaThread實例會被建立並進行基本的初始化。接下來會使用綁定線程命令提供的參數及Thread類的構造器反射初始化一個java線程。綁定完成後,線程能夠經過可用的JNI方法調用所需的java代碼。當本地線程不但願繼續在vm中進行執行時,可以使用JNI調用DetachCurrentThread來解除與vm的關聯(會釋放資源,丟棄指向java.lang.Thread實例的引用,銷燬JavaThread和OSThread對象等)。
    使用JNI調用CreateJavaVM建立vm是一個特殊的綁定本地線程的例子,它會由執行器(java.c)完成或經過一個本地應用來完成。這件事會形成一系列的初始化操做,也會在接下來出現相似執行AttachCurrentThread的行爲。隨後線程繼續執行所需的java代碼(此例中即反射執行main方法)

    線程狀態

    vm維護了一組內部的線程狀態來標識各線程的工做。協調各線程的交互,或當線程執行錯誤進行debug時均須要用到這些狀態標識。當執行了不一樣的動做時,線程的狀態能夠發生改變,可即時使用這些轉換點檢測相應的線程是否具有執行將要執行的動做的客觀條件,安全點便是一個典型的例子。
    以虛擬機的視圖來看,線程有如下幾個狀態:
    _thread_new:表示一個新線程處於初始化的過程當中。
    _thread_in_Java:表示一個線程正在執行java代碼。
    _thread_in_vm:表示一個線程正在vm內部執行。
    _thread_blocked:表示線程因某些緣由阻塞(緣由多是正在獲取一個鎖,等待一個條件,sleep,執行阻塞io等)。
    出於debug的目的,可能須要一些額外的信息。如對於一些工具,或者用戶須要進行線程棧轉儲或棧跡追蹤等操做時,均須要額外的信息,OSThread維護了相應的一些信息,但部分信息如今已經再也不使用了,在線程轉儲時,可報告的狀態額外包含:
    MONITOR_WAIT:表示線程正在等待獲取競態鎖。
    CONDVAR_WAIT:表示線程正在等待vm使用的內部條件變量(與java級別的對象無關聯)。
    OBJECT_WAIT:線程執行了Object.wait方法。
    虛擬機的其餘子系統和庫可能維護了本身的狀態信息,如JVMTI工具,Thread類自己維護的ThreadState等。這些狀態通常不被其餘組件使用。

    虛擬機內部線程

    JAVA的執行有着嚴格的步驟,不一樣於某些腳本語言,java即便運行簡單的Hello World也須要相應的資源準備,所以,對於最簡單的Hello World,也能夠發現系統中其實建立了若干個線程,它們主要是由vm中的線程和有關代碼庫中使用的線程(包含引用處理器,終結者線程等)組成。主要的虛擬機線程有如下幾種:
    a.vm線程:它是VMThread的單例,負責執行虛擬機操做。
    b.週期任務線程:它是在vm內部執行週期操做的線程,是WatcherThread的實例。
    c.GC線程:顧名思義。
    d.編譯器線程:負責運行時執行字節碼到本地代碼的編譯。
    e.信號派發線程:負責等待進程信號並派發給java級別的信號處理方法。
    以上全部線程是Thread類的實例,且全部執行java代碼的線程均爲JavaThread實例。vm內部維護了一個Threads_list的數據結構,它是一個追蹤全部線程的鏈表,在vm內部有一個核心的同步鎖Threads_lock,該鎖就用於保護Threads_list。

  • 10.虛擬機操做和安全點

    VMThread會監測一個VMOperationQueue隊列,該隊列中存放的成員所有爲「操做」,等待相應的操做入隊後,它會執行相應的操做。這些操做被交給VMThread來執行,由於它們須要vm到達安全點纔可執行。簡單來講,當vm在到達安全點時,全部vm內運行的線程均會阻塞,全部在本地代碼中執行的線程在安全點期間被禁止返回vm執行。這意味着虛擬機操做能夠在已知無線程處於正在更改java堆的前提下進行運行,且此時全部的線程處在一個特殊的,不改變java棧的可檢視狀態。
    最著名的虛擬機操做之一即gc,或者更精確一點是不少gc算法中的「stop the world」階段,但也存在不少基於安全點的其餘操做,做者在「54個java官方文檔術語」一文中簡單列舉了這些操做。
    不少虛擬機操做是同步阻塞的,請求者會阻塞到操做完成,但也有一些異步併發的操做,請求者能夠和VMThread並行執行。
    安全點是使用協做輪詢的機制初始化的。簡單來講,線程會去詢問「我是否要爲一個安全點阻塞」。這個詢問機制的實現並不簡單。當發生線程的狀態轉換時會常見詢問這個問題,但並是全部的狀態轉換都會詢問,如當一個線程離開vm並進入native代碼塊時。當從編譯的代碼返回時,或在循環迭代的階段,線程也會詢問這個問題。對於執行解釋代碼的線程來講是不常詢問的,但在安全點,它也有相應的方案,當請求安全點時,解釋器會切換到一個包含了該詢問的代碼的轉發表,當安全點結束後,從派發表切回。一旦請求了安全點,VMThread必須等到全部已知線程均處於安全點-安全狀態,而後纔可執行虛擬機操做。在安全點期間,使用Threads_lock來block住那些正在運行的線程,虛擬機操做完成後,VMThread釋放該鎖。

  • 11.C++堆管理

    除了由JAVA堆管理者和gc維護的JAVA堆之外,HotSpot虛擬機也使用一個c/c++堆(即所謂的分配堆)來存放虛擬機的內部對象和數據。這些用來管理C++堆操做的類都由一個基類Arena(競技場)派生而來。
    Arena和它的子類提供了位於分配/釋放機制頂層的一個快速分配層。每個Arena在3個全局的塊池(ChunkPools)中進行內存塊(Chunk)的分配。不一樣的塊池知足不一樣大小區間的分配,舉例說明,若是請求分配1k的內存,那麼會用「small」塊池分配,若是請求分配10k內存,則使用「medium」塊池,這樣能夠避免內存碎片浪費。
    Arena系統也提供了比純粹的分配/釋放機制更佳的性能。由於後者可能須要獲取一個操做系統的全局鎖,它會嚴重影響擴展性並傷害系統性能。Arena是一些緩存了指定內存數量的線程本地對象,這樣的設計使得它能夠在分配時使用「快路」分配而不用獲取該全局鎖,對於釋放內存的操做,一般狀況下Arena不須要得到鎖。
    Arena的兩個子類,ResourceArena應用於線程本地資源管理,HandleArena用於句柄管理,在client和server編譯器中均用到了這兩種arena。

  • 12.JAVA本地接口(JNI)

    JNI表明本地程序接口。它容許運行在jvm中的java代碼與使用其餘語言(如c/c++)實現的應用或庫進行交互。JNI本地方法能夠用來作不少事情,如建立對象,檢視對象,更新對象,調用java方法,捕獲拋出的異常,加載類和獲取類信息,執行運行時類型檢測等。JNI也可使用Invocation api來啓用jvm中嵌入的任意native應用,經過它,咱們能夠輕易地讓已有應用能夠用java運行而不用去連接vm源碼。
    但有重要的一點,一旦使用了JNI,便失去了使用java平臺的兩個重要的好處。
    第一,依賴jni的java應用不保證能在多平臺上可用,儘管基於java實現的部分是能夠跨宿主機環境的,使用本地程序語言實現的部分仍舊須要從新編譯。
    第二,使用java語言編寫的程序是類型安全的,C或者C++則不是。結果就是使用了JNI的程序員必須額外注意這部分代碼,行爲不端的本地方法可能擾亂整個應用,出於這個緣由考慮,在執行jni功能前,相應使用到jni的應用必定要負責它的安全性檢查。
    原則上講,應儘量少地使用本地方法,並作好這部分代碼與java應用的隔離,做者看來,unsafe後門包是一個典型的案例。
    在HotSpot虛擬機中,jni方法的實現相對直接,它使用各類vm內部原生規則來執行諸如對象建立方法調用等行爲,一般狀況,相應的如解釋器等子系統也使用了這些運行時規則。
    可以使用命令行選項-Xcheck:jni來幫助debug那些使用了本地方法的應用,該選項會使得JNI調用時用到一組debug接口。這些接口會更加嚴格地進行JNI調用的參數驗證,同時還會作一些額外的內部一致性檢查。
    HotSpot對於執行本地方法的線程進行了額外「照顧」,對於一些vm的工做,好比gc過程當中,一部分線程必須保證在安全點阻塞,從而保證java堆在這些敏感過程當中不會再次更改。當咱們但願把一個安全點上的線程帶入到本地代碼執行時,它會被容許進入本地方法,可是禁止從該方法返回java代碼或者執行JNI調用。

  • 13.虛擬機致命故障處理

    毫無疑問,提供致命故障的處理對jvm來講是很是之必需的。以oom爲例,它是一個典型的致命錯誤。當發生這類錯誤時,必定要給用戶提供一些合理且友好的方式來理解致命錯誤成因,從而能快速修復問題,這方面的問題不只包含應用自己,也包含jvm自己。
    第一,通常當jvm在致命故障發生時crash掉,它會轉儲一個hotspot的錯誤日誌文件,格式爲:hs_err_pid<pid>.log。從JDK6開始大幅提高了這些致命錯誤的可診斷性,當發生crash,錯誤日誌文件中會包含當前的內存圖像,所以能夠很容易搞清楚發生crash時的內存佈局。
    第二,也可使用-XX:ErrorFile=選項來指定錯誤日誌的位置。
    第三,發生oom時,也會觸發生成該錯誤文件。
    還有一個重要的功能,能夠指定一個選項:-XX:OnError="cmd1 args...;com2 ...",這樣當發生了crash時會執行這些指令,相應的指令就比較自由,好比咱們能夠指定此時執行一些諸如dbx或Windbg之類的debugger執行相應的操做。早於jdk6的應用可以使用-XX:+ShowMessageBoxOnError來指定發生crash時使用的debugger。如下是jvm內部處理致命錯誤的一些摘要:
    首先,用VMError類聚合和轉儲hs_err_pid<pid>.log文件,當發現未識別的信號/異常時,由操做系統指定的代碼調用它生成該文件。
    第二,vm使用信號來進行內部的交流,當出現未識別的信號,致命錯誤處理器被執行。而這個信號可能源自一個應用的jni代碼,操做系統本地庫,jre本地庫,甚至是jvm自己。
    第三,致命錯誤處理器是慎重編寫的,這也是爲了不它本身也出現錯誤,好比在出現StackOverFlow時,或在持有重要的鎖期間發生crash(如持有分配鎖)。
    死鎖是一種常見的錯誤,通常發生在應用程序在申請多個鎖時順序不正確的狀況。當死鎖發生時,找出相應的點也是比較困難的,此時能夠抓出java進程id,發送SIGQUIT到該進程(Solaris/Linux),會在標準輸出中輸出java級別的棧信息,這對分析死鎖幫助極大,不過在jdk6之上的版本,已經可使用Jconsole來輕鬆處理該問題。
    順便簡單提一提除了Jconsole/VisualVM等集成工具以外,一些單一目的的自帶工具。
    jps:jvm進程工具,能夠查看各jvm進程,名稱和編號。
    jstat:虛擬機統計信息。好比發生了多少次full gc等。
    jinfo:java配置信息工具,運行時查看jvm進程的配置。
    jmap:內存映像工具,能夠將當前內存狀況轉儲一個快照文件。
    jhat:堆轉儲快照分析工具。
    jstack:java堆棧跟蹤工具。

  • 總結

    本文簡述了包含運行時參數處理,線程管理,類加載,類數據共享,運行時編譯,異常處理,重大錯誤處理等java運行時技術。參考資料主要源自官方的若干文檔,一部分資料是專屬性的,如專門描述JVM或JIT,但根本沒法肯定成做於哪一個版本(關於cds做者判斷與JAVA8中的描述相同,但顯然早已不適用),一部分資料是依託於較新版本的,由於新舊版本的文檔並未保持同一目錄結構,有些組件未能在新版中找到詳盡的文檔,所以不免會有不許確或過期的內容,做者爭取在後面找到最新且更加權威的資料以修正。 做者我的認爲有兩點重要的收穫,一是宏觀上了解了官方出品的HotSpot虛擬機在運行時的框架設計,理解java在運行時爲咱們不遺餘力作了哪些事;二是瞭解某些具體模塊在新版中的優化和取捨,從而間接瞭解接下來java的使用趨勢。如「雲友好」,「多適應」,「開放」等。 這三點是做者我的不成熟的簡單總結,寫到這裏,也順便對這三點進行一個「簡單總結」。 雲友好其實體現的方面不少,cds就是重要一點,它在最新幾個版本的更新用一句俗化表示:幫用戶省錢。G1定時釋放無用內存的新特性也體現了這一點。 多適應和開放也很好理解,不止是gc方面,前面簡單提過的zgc等針對超大堆的gc,以及G1這種放權讓用戶指定目標的gc,綜合此前的各類gc,基本涵蓋了咱們全部可能的應用環境。一樣的,模塊化系統也自然匹配了中小型到大型項目的需求,一個項目從初創到逐漸壯大,或許最終就是模塊不斷擴充的過程,模塊化系統甚至容許對jdk自己進行按需定製,對於小型設備用戶也無益因而一個福音。JIT自己就具有自適應的編譯思想,最優化最常執行的代碼,graal是新出的基於java的JIT編譯器。同步機制也引入了「自適應」自旋鎖,G1中對cs的選擇也具有自適應性等。 再一次,膜拜前輩。

相關文章
相關標籤/搜索