Java虛擬機是相對於物理機的概念,也就是對現有馮諾依曼體系結構的硬件系統的抽象,現有計算機硬件系統是由輸入,運算器、控制器、存儲器和輸出構成的。Java虛擬機則是建立在硬件系統之上,提供自己的規範,使在虛擬機上運行的程序具有跨平臺、自動回收內存等諸多特性。
虛擬機內存劃分概述
Java虛擬機對內存劃分爲5種不同的區域,分別是棧內存、本地方法棧、堆內存、方法區、程序計數器。
棧內存
棧內存是線程私有的,它隨着線程的創建而創建,隨着線程的銷燬而銷燬,在執行方法時會創建棧幀,棧幀中存放的是局部變量表(方法的參數以及局部變量)、操作數棧(指令所操作的數據存放處)等信息。方法的調用就是棧幀入棧的過程。
本地方法棧
執行Native方法時所用到的存儲單元
堆內存
堆內存是線程共享的,當虛擬機啓動時便會創建。該內存區域是用來存放對象實例的,也是垃圾收集器管理的主要區域。
方法區
方法區是線程共享的內存區域,它存儲的主要是在類加載時產生的數據,可以理解爲是存儲Class文件信息或者是類相關信息的。所以編譯後的字節碼、常量、靜態變量等與對象無關只有類有關的數據都放在這裏。
程序計數器
程序計數器作爲線程執行時的行號指示器,分支、跳轉、循環都是通過它實現的,在任一時刻,一個處理器核都只會執行一條線程中的指令,每一個核對應一個程序計數器。
Class文件詳解
Java原碼中的各種變量,關鍵字和運算符號的語義最終都是由多條字節碼命令組合而成的,而ava原碼通過編譯器編譯爲虛擬機可加載的Class文件,Class文件中包含了Java字節碼和符號表和其他輔助信息。
Class文件是一組以8位字節爲原子單位的二進制流,各個數據嚴格按照規定的順序緊湊的排列(順序中包含着信息),而Class文件只包含兩種數據類型:
無符號數 – 表示數字、索引引用、數量值、或按照UTF-8表示字符串,通過u1、u2、u4表示1字節2字節4字節
表 – 是由多個無符號數組成的複合結構,所有表都以"_info"結尾
類型 | 名稱 | 描述 | 數量 |
---|---|---|---|
u4 | magic | 魔數 | 1 |
u2 | minor_version | 副版本號 | 1 |
u2 | major | 主版本號 | 1 |
u2 | constant_pool_count | 常量池數量 | 1 |
cp_info | constant_pool | 常量池 | 常量池數量-1 |
u2 | access_flags | 訪問標識 | 1 |
u2 | this_class | 類索引 | 2 |
u2 | super_class | 父類索引 | 2 |
u2 | interfaces_count | 接口數量 | 1 |
u2 | interfaces | 接口索引集合 | 接口數量 |
u2 | fields_count | 字段數量 | 1 |
field_info | fields | 字段表 | 字段數量 |
u2 | methods_counts | 方法數量 | 1 |
method_info | methods | 方法表 | 方法數量 |
u2 | attributes_count | 屬性數量 | 1 |
attributes_info | attributes | 屬性表 | 屬性數量 |
魔數以及版本號
Class文件的前4個字節是魔數它的唯一作用就是確定這個文件是Class文件,接下來2個字節是副版本號,再接下來兩個字節是主版本號,用來確定版本號。
常量池數量
由於每個Class文件的常量數量不是固定的,所以緊接着主版本號後面的是常量池數量,用來確定常量的數量
常量池
根據常量池的數量,後面是常量的具體內容,常量池中包含兩大類常量:
-
字面量:字面量就是沒有特殊意義的常量,就是用來表示常量,比如文本字符串(字段名等),或是被final修飾的常量值等
-
符號引用包括以下三類
類和接口的全限定名(用來確定類和接口)
字段名稱和描述符(用來確定字段及字段的其他信息)
方法名稱和描述符(用來確定字段及字段的其他信息)
類型 | 標識 | 描述 |
---|---|---|
CONSTONT_utf8_info | 1 | utf8編碼字符串 |
CONSTONT_Integer_info | 3 | 整型字面量 |
CONSTONT_Float_info | 4 | 浮點型字面量 |
CONSTONT_Long_info | 5 | 長整型字面量 |
CONSTONT_Double_info | 6 | 雙精度浮點型字面量 |
CONSTONT_Class_info | 7 | 類或者接口的符號引用 |
CONSTONT_String_info | 8 | 字符串類型字面量 |
CONSTONT_Fieldref_info | 9 | 字段的符號引用 |
CONSTONT_Methodref_info | 10 | 方法的符號應用 |
CONSTONT_InterfaceMethodref_info | 11 | 接口方法的符號應用 |
CONSTONT_NameAndType_info | 12 | 字段或方法的部分引用 |
CONSTONT_MethodHandle_info | 15 | 方法的句柄 |
CONSTONT_MethodType_info | 16 | 方法的類型 |
CONSTONT_InvokeDynamic_info | 18 | 動態方法調用點 |
常量池中的數據是以_info結尾,前面講到Class文件只有兩種類型,無符號數和表,而常量池的數據全部都是表,也就是由多個無符號數組成的複合結構。
常量池是提供其他表進行引用的,一個class文件中的所有出現的字符都該在常量池中顯示
訪問標識
在常量池結束之後,接着的是2個字節的訪問標識,用於識別該文件是類還是接口、是否爲public、是否爲abstract等信息
類索引、父類索引、接口索引集合
在訪問標識後的爲類索引,是這個類的的全限定名,指向常量池中的CONSTONT_Class_info
字段表和方法表
字段表用於表述接口或者類中聲明的變量。
類型 | 名稱 | 描述 |
---|---|---|
u2 | access_flags | 描述字段、方法作用域等信息 |
u2 | name_index | 字段、方法簡單名稱常 |
u2 | descriptor_index | 字段、方法描述符 |
u2 | attributes_count | 屬性數量 |
attributes_info | attributes | 屬性表 |
屬性表
屬性表的作用是對其他表額外進行描述,下面是一些常用的屬性。
名稱 | 使用位置 | 描述 |
---|---|---|
code | 方法表 | 編譯後的字節碼指令 |
Exceptions | 方法表 | 方法拋出的異常 |
其中code屬性非常重要,如果把Java程序中的信息分爲代碼和元數據兩部分,那麼Code屬性就是用來描述代碼的,其他所有數據項都是用來描述元數據。
虛擬機類加載過程
虛擬機類加載過程就是將原來磁盤中的代碼加載到內存中。
類加載過程生命週期
類加載的生命週期包括:加載、驗證、準備、解析、初始化、使用、卸載。
其中加載、驗證、準備、初始化、卸載這五個過程順序是確定的,而解析階段有可能在初始化前也有可能在初始化後,在初始化前的解析是靜態綁定(前期綁定),也就是可以在編譯時確定,而初始化後的解析是動態綁定(後期綁定)。
加載
- 通過類的全限定名來獲取類的二進制流
- 將類加載到方法區中
- 生成這個類的Class對象,獲取方法區數據的入口(反射時會使用的)
驗證
驗證合法性
準備
分配類變量內存,並且賦初值
解析
將運行時常量池(類文件中的常量池加載到方法區)的符號引用替換爲直接引用(內存地址)。
初始化
執行方法,爲準備階段類變量的初值賦值。方法是編譯器自動生成的,相當於爲靜態字段賦值的方法,並且會合並static{}代碼塊的代碼。(加載階段還爲涉及到具體對象)
類加載器
上面的生命週期是由類加載器完成的,對於任何一個類,都需要通過類加載器和這個類來確定在JVM中的唯一性。(同一個類不同類加載器加載後的Class對象的equals()方法返回false)
雙親委派機制
如果有一個類加載器收到加載請求,它首先會請求父加載器去加載,所以會導致所有加載請求都會到頂層的啓動類加載器去;當父加載器無法完成加載請求(搜索範圍沒有這個類),就傳遞給子加載器加載。
字節碼執行過程
字節碼源代碼進過編譯又經過類加載器加載到方法區中的,字節碼的執行代表着程序運行時一個方法的執行。
運行時棧幀
棧幀是方法執行時的數據結構,主要包括:局部變量表、操作數棧、動態連接、方法返回地址等。
局部變量表
用來存放方法參數和方法的變量的存儲空間。它的大小在編譯期間就已經確定,存放到Code屬性中。
操作數棧
當方法開始執行時,操作數棧是空的,方法執行過程根據字節碼會往操作數棧中寫入和提取內容。
動態連接
每個棧幀都包含一個指向運行時常量池中所屬方法的引用,這個引用是爲了完成方法調用過程中的動態連接(確定方法),其中靜態方法和私有方法是編譯器可知,運行期不可變的。所以在編譯期就可以確定。如重載就是需要動態連接來確認的
方法返回地址
就是方法的返回地址。。。。。
垃圾回收機制
垃圾回收是回收堆內存中不使用的對象。
判斷對象是否存活
- 引用計數算法
每當有一個地方引用對象就將計數加1,當引用失效就減1,當計數器爲零就說明對象不再被使用了。(無法解決對象循環引用) - 可達性算法
通過一系列GC Root 對象作爲起點,當一個對象到GC Root 沒有任何引用鏈相連就證明對象不可使用
垃圾收集算法
- 標記清除算法
標記需要回收的對象,之後統一回收。(會產生內存碎片) - 複製算法
將內存二等分,每次使用一半,當一半內存使用完之後觸發複製算法,將存活的對象複製到另外一半中。(內存縮小到原來的一半)。
適用於新生代,大量對象死去,少量存活,移動很少的對象。 - 標記整理
標記需要回收的對象,將所有存活對象移動到一端,清除邊界意外的內存。