Java知識點總結

Java知識點總結

JVM概述

JVM是什麼

JVM全稱是Java Virtual Machine(Java虛擬機)。它之所以被稱之爲是“虛擬”的,就是因爲它僅僅是由一個規範來定義的抽象計算機。我們平時經常使用的Sun HotSpot虛擬機只是其中一個具體的實現(另外還有BEA JRockit、IBM J9等等虛擬機)。
JVM的設計目標是提供一個基於抽象規格描述的計算機模型,爲解釋程序開發人員提供很好的靈活性,同時也確保Java代碼可在符合該規範的任何系統上運行。JVM對其實現的某些方面給出了具體的定義,特別是對Java可執行代碼,即字節碼(Bytecode)的格式給出了明確的規格。這一規格包括操作碼和操作數的語法和數值、標識符的數值表示方式、以及Java類文件中的Java對象、常量緩衝池在JVM的存儲映象。這些定義爲JVM解釋器開發人員提供了所需的信息和開發環境。Java的設計者希望給開發人員以隨心所欲使用Java的自由。
JVM是java的核心和基礎,在java編譯器和os平臺之間的虛擬處理器。它是一種基於下層的操作系統和硬件平臺並利用軟件方法來實現的抽象的計算機,可以在上面執行java的字節碼程序。

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就是爲了支持與操作系統無關,實現跨平臺。

JVM的生命週期

當啓動一個Java程序時,一個虛擬機實例也就誕生了。當該程序關閉退出,這個虛擬機實例也就隨之消亡。如果在同一臺計算機上同時運行三個Java程序,將得到三個Java虛擬機實例。每個Java程序都運行於它自己的Java虛擬機實例中。
JVM實例對應了一個獨立運行的java程序,它是進程級別。
1、啓動。
啓動一個Java程序時,一個JVM實例就產生了,任何一個擁有publicstatic void main(String[] args)函數的class都可以作爲JVM實例運行的起點
2、運行。
main()作爲該程序初始線程的起點,任何其他線程均由該線程啓動。JVM內部有兩種線程:守護線程和非守護線程,main()屬於非守護線程,守護線程通常由JVM自己使用,java程序也可以標明自己創建的線程是守護線程
3、消亡。
當程序中的所有非守護線程都終止時,JVM才退出;若安全管理器允許,程序也可以使用Runtime類或者System.exit()來退出

JVM運行原理

操作系統裝入JVM是通過jdk中Java.exe來完成,通過下面4步來完成JVM環境。

1、JVM裝入環境。
JVM提供的方式是操作系統的動態連接文件。既然是文件那就存在一個裝入路徑的問題,Java是怎麼找這個路徑的呢?下面基於Windows的實現的分析。
首先查找jre路徑,Java是通過GetApplicationHomeapi來獲得當前的Java.exe絕對路徑,c:\jdk1.7.0_45\bin\Java.exe,然後截取到絕對路徑c:\jdk1.7.0_45\,判斷c:\jdk1.7.0_45\bin\Java.dll文件是否存在,如果存在就把c:\jdk1.7.0_45\作爲jre路徑,如果不存在則判斷c:\jdk1.7.0_45\jre\bin\Java.dll是否存在,如果存在這c:\jdk1.7.0_45\jre作爲jre路徑,如果不存在調用GetPublicJREHome查HKEY_LOCAL_MACHINE\Software\JavaSoft\JavaRuntime Environment\“當前JRE版本號”\JavaHome的路徑爲jre路徑。
然後裝載JVM.cfg文件。在我們的jdk目錄中jre\bin\server和jre\bin\client都有JVM.dll文件存在,而Java正是通過JVM.cfg配置文件來管理這些不同版本的JVM.dll的。
最後獲得JVM.dll的路徑,JRE路徑+\bin+\JVM類型字符串+\JVM.dll就是JVM的文件路徑了,但是如果在調用Java程序時用-XXaltJVM=參數指定的路徑path,就直接用path+\JVM.dll文件做爲JVM.dll的文件路徑。

2、裝載JVM.dll
通過第一步已經找到了JVM的路徑,Java通過LoadJavaVM來裝入JVM.dll文件。裝入工作很簡單,就是調用Windows API函數:
LoadLibrary裝載JVM.dll動態連接庫.然後把JVM.dll中的導出函數JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs掛接到InvocationFunctions變量的CreateJavaVM和GetDefaultJavaVMInitArgs函數指針變量上。JVM.dll的裝載工作宣告完成。

3、初始化JVM。
掛接到JNIENV(JNI調用接口)實例,獲得本地調用接口,這樣就可以在Java中調用JVM的函數了。調用InvocationFunctions->CreateJavaVM也就是JVM中JNI_CreateJavaVM方法獲得JNIEnv結構的實例。

4、運行Java程序.
Java程序有兩種方式一種是jar包,一種是class。運行jar(Java -jarXXX.jar)的時候,Java.exe調用GetMainClassName函數,該函數先獲得JNIEnv實例然後調用Java類Java.util.jar.JarFileJNIEnv中方法getManifest()並從返回的Manifest對象中取getAttributes(“Main-Class”)的值即jar包中文件:META-INF/MANIFEST.MF指定的Main-Class的主類名作爲運行的主類。之後main函數會調用Java.c中LoadClass方法裝載該主類(使用JNIEnv實例的FindClass)。main函數直接調用Java.c中LoadClass方法裝載該類。如果是執行class方法。main函數直接調用Java.c中LoadClass方法裝載該類。
然後main函數調用JNIEnv實例的GetStaticMethodID方法查找裝載的class主類中“publicstatic void main(String[] args)”方法,並判斷該方法是否爲public方法,然後調用JNIEnv實例的CallStaticVoidMethod方法調用該Java類的main方法。

JVM體系結構

JVM的體系結構圖如下:
JVM體系結構
主要包括兩個子系統和兩個組件: Classloader(類裝載器) 子系統,Execution engine(執行引擎) 子系統;Runtime data area (運行時數據區域)組件, Native interface(本地接口)組件。
Class loader子系統:根據給定的全限定名類名(如java.lang.Object)來裝載class文件的內容到 Runtime data area中的method area(方法區域)。
Execution engine子系統:執行classes中的指令。方法的字節碼是由Java虛擬機的指令序列構成的。每一條指令包含一個單字節的操作碼,後面跟隨0個或多個操作數。執行引擎執行字節碼時,首先取得一個操作碼,如果操作碼有操作數,取得它的操作數。它執行操作碼和跟隨的操作數規定的動作,然後再取得下一個操作碼。這個執行字節碼的過程在線程完成前將一直持續。任何JVM實現的核心是Execution engine,換句話說:Sun 的JDK 和IBM的JDK好壞主要取決於他們各自實現的Execution engine的好壞。
Native interface組件 :與nativelibraries交互,是其它編程語言交互的接口。Java裏聲明爲native的方法多數在jdk/src//native裏可以找到。其中可以是share,也就是平臺中立的代碼;也可以是某個具體平臺。這個native目錄裏的結構跟Java源碼結構一樣是按包名來組織的。不過需要提醒的是,這些native方法不是“JVM”的,是“類庫”的,不在JVM裏面。
Runtime data area 組件:Java虛擬機定義了若干種程序運行時使用到的運行時數據區,有一些是隨虛擬機的啓動而創建,隨虛擬機的退出而銷燬,如堆、方法區。第二種則是與線程一一對應,隨線程的開始和結束而創建和銷燬,如Java棧,PC寄存器。

JVM相關

JVM定義了若干個程序執行期間使用的數據區域。這個區域裏的一些數據在JVM啓動的時候創建,在JVM退出的時候銷燬。而其他的數據依賴於每一個線程,在線程創建時創建,在線程退出時銷燬。
JVM內存模型

程序計數器

程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
由於Java 虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。
如果線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則爲空(Undefined)。
此內存區域是唯一一個在Java 虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。

虛擬機棧

線程私有,它的生命週期與線程相同。虛擬機棧描述的是Java 方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息。
動畫是由一幀一幀圖片連續切換結果的結果而產生的,其實虛擬機的運行和動畫也類似,每個在虛擬機中運行的程序也是由許多的幀的切換產生的結果,只是這些幀裏面存放的是方法的局部變量,操作數棧,動態鏈接,方法返回地址和一些額外的附加信息組成。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

對於執行引擎來說,活動線程中,只有棧頂的棧幀是有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法。執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。
局部變量表
局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序被編譯成Class文件時,就在方法的Code屬性的max_locals數據項中確定了該方法所需要分配的最大局部變量表的容量。
局部變量表的容量以變量槽(Slot)爲最小單位,32位虛擬機中一個Slot可以存放一個32位以內的數據類型(boolean、byte、char、short、int、float、reference和returnAddress八種)。
reference類型虛擬機規範沒有明確說明它的長度,但一般來說,虛擬機實現至少都應當能從此引用中直接或者間接地查找到對象在Java堆中的起始地址索引和方法區中的對象類型數據。
returnAddress類型是爲字節碼指令jsr、jsr_w和ret服務的,它指向了一條字節碼指令的地址。
虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的,如果是實例方法(非static),那麼局部變量表的第0位索引的Slot默認是用於傳遞方法所屬對象實例的引用,在方法中通過this訪問。
Slot是可以重用的,當Slot中的變量超出了作用域,那麼下一次分配Slot的時候,將會覆蓋原來的數據。Slot對對象的引用會影響GC(要是被引用,將不會被回收)。
系統不會爲局部變量賦予初始值(實例變量和類變量都會被賦予初始值)。也就是說不存在類變量那樣的準備階段。
操作數棧
和局部變量區一樣,操作數棧也是被組織成一個以字長爲單位的數組。但是和前者不同的是,它不是通過索引來訪問,而是通過標準的棧操作——壓棧和出棧—來訪問的。比如,如果某個指令把一個值壓入到操作數棧中,稍後另一個指令就可以彈出這個值來使用。
虛擬機在操作數棧中存儲數據的方式和在局部變量區中是一樣的:如int、long、float、double、reference和returnType的存儲。對於byte、short以及char類型的值在壓入到操作數棧之前,也會被轉換爲int。
虛擬機把操作數棧作爲它的工作區——大多數指令都要從這裏彈出數據,執行運算,然後把結果壓回操作數棧。比如,iadd指令就要從操作數棧中彈出兩個整數,執行加法運算,其結果又壓回到操作數棧中,看看下面的示例,它演示了虛擬機是如何把兩個int類型的局部變量相加,再把結果保存到第三個局部變量的:
begin
iload_0 // push the int in local variable 0 ontothe stack
iload_1 //push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end

在這個字節碼序列裏,前兩個指令iload_0和iload_1將存儲在局部變量中索引爲0和1的整數壓入操作數棧中,其後iadd指令從操作數棧中彈出那兩個整數相加,再將結果壓入操作數棧。第四條指令istore_2則從操作數棧中彈出結果,並把它存儲到局部變量區索引爲2的位置。

動態連接
虛擬機運行的時候,運行時常量池會保存大量的符號引用,這些符號引用可以看成是每個方法的間接引用。如果代表棧幀A的方法想調用代表棧幀B的方法,那麼這個虛擬機的方法調用指令就會以B方法的符號引用作爲參數,但是因爲符號引用並不是直接指向代表B方法的內存位置,所以在調用之前還必須要將符號引用轉換爲直接引用,然後通過直接引用纔可以訪問到真正的方法。
如果符號引用是在類加載階段或者第一次使用的時候轉化爲直接應用,那麼這種轉換成爲靜態解析,如果是在運行期間轉換爲直接引用,那麼這種轉換就成爲動態連接。
返回地址
方法的返回分爲兩種情況,一種是正常退出,退出後會根據方法的定義來決定是否要傳返回值給上層的調用者,一種是異常導致的方法結束,這種情況是不會傳返回值給上層的調用方法。
不過無論是那種方式的方法結束,在退出當前方法時都會跳轉到當前方法被調用的位置,如果方法是正常退出的,則調用者的PC計數器的值就可以作爲返回地址,,果是因爲異常退出的,則是需要通過異常處理表來確定。
方法的的一次調用就對應着棧幀在虛擬機棧中的一次入棧出棧操作,因此方法退出時可能做的事情包括:恢復上層方法的局部變量表以及操作數棧,如果有返回值的話,就把返回值壓入到調用者棧幀的操作數棧中,還會把PC計數器的值調整爲方法調用入口的下一條指令。

異常
在Java 虛擬機規範中,對虛擬機棧規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError 異常;如果虛擬機棧可以動態擴展(當前大部分的Java 虛擬機都可動態擴展,只不過Java 虛擬機規範中也允許固定長度的虛擬機棧),當擴展時無法申請到足夠的內存時會拋出OutOfMemoryError 異常。

本地方法棧

本地方法棧(Native MethodStacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧爲虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native 方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。
與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

堆是Java 虛擬機所管理的內存中最大的一塊。Java 堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。但是隨着JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那麼“絕對”了。
堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC 堆”。
堆的大小可以通過-Xms(最小值)和-Xmx(最大值)參數設置,-Xms爲JVM啓動時申請的最小內存,默認爲操作系統物理內存的1/64但小於1G,-Xmx爲JVM可申請的最大內存,默認爲物理內存的1/4但小於1G,默認當空餘堆內存小於40%時,JVM會增大Heap到-Xmx指定的大小,可通過-XX:MinHeapFreeRation=來指定這個比列;當空餘堆內存大於70%時,JVM會減小heap的大小到-Xms指定的大小,可通過XX:MaxHeapFreeRation=來指定這個比列,對於運行系統,爲避免在運行時頻繁調整Heap的大小,通常-Xms與-Xmx的值設成一樣。

如果從內存回收的角度看,由於現在收集器基本都是採用的分代收集算法,所以Java 堆中還可以細分爲:新生代和老年代;
新生代:程序新創建的對象都是從新生代分配內存,新生代由Eden Space和兩塊相同大小的Survivor Space(通常又稱S0和S1或From和To)構成,可通過-Xmn參數來指定新生代的大小,也可以通過-XX:SurvivorRation來調整Eden Space及SurvivorSpace的大小。
老年代:用於存放經過多次新生代GC仍然存活的對象,例如緩存對象,新建的對象也有可能直接進入老年代,主要有兩種情況:1、大對象,可通過啓動參數設置-XX:PretenureSizeThreshold=1024(單位爲字節,默認爲0)來代表超過多大時就不在新生代分配,而是直接在老年代分配。2、大的數組對象,且數組中無引用外部對象。
老年代所佔的內存大小爲-Xmx對應的值減去-Xmn對應的值。

如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError 異常。

方法區

方法區在一個jvm實例的內部,類型信息被存儲在一個稱爲方法區的內存邏輯區中。類型信息是由類加載器在類加載時從類文件中提取出來的。類(靜態)變量也存儲在方法區中。
簡單說方法區用來存儲類型的元數據信息,一個.class文件是類被java虛擬機使用之前的表現形式,一旦這個類要被使用,java虛擬機就會對其進行裝載、連接(驗證、準備、解析)和初始化。而裝載(後的結果就是由.class文件轉變爲方法區中的一段特定的數據結構。這個數據結構會存儲如下信息:

類型信息
這個類型的全限定名
這個類型的直接超類的全限定名
這個類型是類類型還是接口類型
這個類型的訪問修飾符
任何直接超接口的全限定名的有序列表

字段信息
字段名
字段類型
字段的修飾符

方法信息
方法名
方法返回類型
方法參數的數量和類型(按照順序)
方法的修飾符

其他信息
除了常量以外的所有類(靜態)變量
一個指向ClassLoader的指針
一個指向Class對象的指針
常量池(常量數據以及對其他類型的符號引用)

JVM爲每個已加載的類型都維護一個常量池。常量池就是這個類型用到的常量的一個有序集合,包括實際的常量(string,integer,和floating point常量)和對類型,域和方法的符號引用。池中的數據項象數組項一樣,是通過索引訪問的。

每個類的這些元數據,無論是在構建這個類的實例還是調用這個類某個對象的方法,都會訪問方法區的這些元數據。
構建一個對象時,JVM會在堆中給對象分配空間,這些空間用來存儲當前對象實例屬性以及其父類的實例屬性(而這些屬性信息都是從方法區獲得),注意,這裏並不是僅僅爲當前對象的實例屬性分配空間,還需要給父類的實例屬性分配,到此其實我們就可以回答第一個問題了,即實例化父類的某個子類時,JVM也會同時構建父類的一個對象。從另外一個角度也可以印證這個問題:調用當前類的構造方法時,首先會調用其父類的構造方法直到Object,而構造方法的調用意味着實例的創建,所以子類實例化時,父類肯定也會被實例化。
類變量被類的所有實例共享,即使沒有類實例時你也可以訪問它。這些變量只與類相關,所以在方法區中,它們成爲類數據在邏輯上的一部分。在JVM使用一個類之前,它必須在方法區中爲每個non-final類變量分配空間。

方法區主要有以下幾個特點:
1、方法區是線程安全的。由於所有的線程都共享方法區,所以,方法區裏的數據訪問必須被設計成線程安全的。例如,假如同時有兩個線程都企圖訪問方法區中的同一個類,而這個類還沒有被裝入JVM,那麼只允許一個線程去裝載它,而其它線程必須等待
2、方法區的大小不必是固定的,JVM可根據應用需要動態調整。同時,方法區也不一定是連續的,方法區可以在一個堆(甚至是JVM自己的堆)中自由分配。
3、方法區也可被垃圾收集,當某個類不在被使用(不可觸及)時,JVM將卸載這個類,進行垃圾收集

可以通過-XX:PermSize 和 -XX:MaxPermSize 參數限制方法區的大小。
對於習慣在HotSpot 虛擬機上開發和部署程序的開發者來說,很多人願意把方法區稱爲“永久代”(PermanentGeneration),本質上兩者並不等價,僅僅是因爲HotSpot 虛擬機的設計團隊選擇把GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對於其他虛擬機(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。
相對而言,垃圾收集行爲在這個區域是比較少出現的,但並非數據進入了方法區就如永久代的名字一樣“永久”存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。

總結

名稱 特徵 作用 配置參數 異常
程序計數器 佔用內存小,線程私有,生命週期與線程相同 大致爲字節碼行號指示器
虛擬機棧 線程私有,生命週期與線程相同,使用連續的內存空間 Java 方法執行的內存模型,存儲局部變量表、操作棧、動態鏈接、方法出口等信息 -Xss StackOverflowError OutOfMemoryError
Java堆 線程共享,生命週期與虛擬機相同,可以不使用連續的內存地址 保存對象實例,所有對象實例(包括數組)都要在堆上分配 -Xms -Xsx -Xmn OutOfMemoryError
方法區 線程共享,生命週期與虛擬機相同,可以不使用連續的內存地址 存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據 -XX:PermSize:16M -XX:MaxPermSize64M OutOfMemoryError
運行時常量池 方法區的一部分,具有動態性 存放字面量及符號引用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章