Java編譯與執行全揭秘:宏觀到微觀開啟java的奇幻之旅
1.宏觀上分析java代碼的執行流程
圖片
編譯階段:代碼編譯是從.java源文件通過編譯器轉換形成.calss字節碼文件的過程(javac編譯器)。
執行階段:.class字節碼文件會通過JVM中的解釋器,翻譯成特定機器上的機器碼,并執行對應的指令。
問題:這兩個階段會操作內存嗎?什么時候操作內存?
- 編譯階段僅僅只是將源文件代碼轉換為對應的字節碼,該過程不會執行指令,因此不會操作內存。
- 執行階段會將字節碼文件解釋為機器碼(解釋執行),并執行指令(編譯執行),因此該過程會頻繁操作內存。
圖片
2.編譯階段執行流程
編譯階段僅僅只是把源文件轉換成對應的字節碼,那我們可以知道,Java代碼首先一定會經過javac編譯器編譯一次。
Javac編譯java源文件轉換為字節碼文件的過程如下圖所示:
圖片
2.1 javac執行步驟分析
javac的組件如下圖:
圖片
源代碼執行流程如下圖:
圖片
詞法分析:把源代碼中的單個字符(各關鍵字、變量等)轉為Token序列,token是編譯過程的最小單元,ScannerFactory
和ParserFactory
用于生成Scanner
和JavacParser
對象,JavacParser
用于規定哪些詞符合java語言規范,Scanner
用于逐步讀取和歸類不同的詞法操作,解析出Token序列,Names
用于存儲和表示解析后的詞法。
語法分析:將Token流構造成為抽象語法樹,語法樹的每一個節點都代表代碼中的一個語法結構(包、類型、接口、修飾符等)。
填充符號表:符號表是一組符號地址和符號信息構成的表格,符號表會填充每個抽象語法樹和package-info.java的頂級節點,生成一個待處理列表。
插入式注解處理器處理注解:注解處理器可以增刪改抽象語法樹中的任意元素,因此每當注解處理器對語法樹進行修改時,都會從詞法分析重新開始執行,直到注解處理器不再修改語法樹為止,每一次的循環過程稱為一次Round。
語義分析:對語法樹結構上正確的源程序進行上下文有關的審查,分為標注檢查和數據及控制流分析兩個步驟,參數不變性由編譯器在編譯期保障。
- 標注檢查:檢查的內容包括變量使用前是否被聲明,變量與賦值的數據類型是否能匹配等,以及常量折疊:int i=1+2;,會被折疊成字面量3。
- 數據及控制分析:數據及控制分析是對程序上下文邏輯更進一步的驗證,例如:檢查變量是否初始化,方法的每個執行分支是否都有返回值,是否所有的異常都被正確處理等,這個階段并不會對變量賦值。
解語法糖:
- 語法糖的定義:在計算機語言中(如java)添加某種語法,這種語法對整個語法的功能并沒有影響,但是更方便程序員使用,增加程序的可讀性,減少程序出錯的機會。
- 解語法糖就是將java中的語法(語法糖)還原回基礎語法結構,例如:泛型、變長參數、自動裝箱/拆箱、遍歷循環、內部類、斷言等JVM不支持的語法結構還原回最基礎的語法結構,這個過程叫解語法糖。
生成字節碼:將前面步驟生成的語法樹、符號表等信息轉化為字節碼,然后寫入磁盤.class文件。
ClassFile {
u4 magic; //Class 文件的標志
u2 minor_version;//Class 的小版本號
u2 major_version;//Class 的大版本號
u2 constant_pool_count;//常量池的數量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的訪問標記
u2 this_class;//當前類
u2 super_class;//父類
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一個類可以實現多個接口
u2 fields_count;//Class 文件的字段屬性
field_info fields[fields_count];//一個類可以有多個字段
u2 methods_count;//Class 文件的方法數量
method_info methods[methods_count];//一個類可以有個多個方法
u2 attributes_count;//此類的屬性表中的屬性數
attribute_info attributes[attributes_count];//屬性表集合
}
3.執行階段執行流程
Java文件通過編譯后生成的class字節碼文件,會加載到JVM中執行,JVM執行過程主要分為加載和執行兩個階段。
- 加載:會將Java文件通過javac執行后的ClassFile通過類加載器(ClassLoader)加載到內存區域,類加載器會對ClassFile進行加載、鏈接、初始化(也就是進行解析)。
- 執行:將初始化后的數據根據類型的不同分別存放在不同的內存區域,并通過執行引擎運行字節碼指令對棧進行操作(執行引擎運行的字節碼指令只會操作當前棧幀(指令流--基于棧的指令集架構))。
執行階段,Java虛擬機對class字節碼文件進行的操作如下圖所示:
圖片
具體操作參考下圖:
圖片
類的定義分為三類保存
- initCode:保存需要初始化執行的實例變量和非static修飾的塊。
- clinitCode:保存需要初始化執行的類變量和static修飾的塊。
- methodDefs:保存方法定義符號。
把initCode中的定義插入到實例構造器init()中。
- 如果程序中定義有構造函數,它在解析的語法分析階段就會被重命名為init()。
- 如果程序中定義沒有構造函數,則實例構造器init()就是作為默認構造函數,在填充符號表時被添加。
- 如果程序中定義的構造函數沒有顯式調用super()或this(),會添加super()的父類構造函數調用。
- 如果程序中定義沒有構造函數,則填充符號表時添加的默認構造函數會自帶super()。
把clinitCode中的定義插入到類構造器clinit()中。
- init()是在每次實例化對象時執行。
- clinit()是在類加載器加載該類時執行。
4. 擴展內容-沙箱安全機制
- Java安全模型的核心是Java沙箱(sandbox)。
- 什么是沙箱:沙箱是一個限制程序運行的環境。沙箱機制就是將Java代碼限定在虛擬機特定的運行范圍中,并且嚴格限制代碼對本地系統資源訪問。
- 作用:保證對代碼的有效隔離,限制系統資源的訪問、防止對本地系統造成破壞--系統資源包括:CPU、內存、文件系統、網絡等。
- Java安全模型:在Java中將執行程序分為本地代碼和遠程代碼兩種,本地代碼默認視為可信任的,而遠程代碼則被看作是不受信任的。對于授信的本地代碼可以訪問一切本地資源(例如:創建文件夾、修改文件內容等),而對于非授信的遠程代碼在早期的Java實現中,安全依賴于沙箱機制(1.0版本完全隔離)。
圖片
1.1版本針對安全機制做了改進,增加了安全策略,允許用戶指定對本地資源的訪問權限。
圖片
1.2版本再次改進安全機制,增加了代碼簽名,不論是本地代碼或是遠程代碼,都會按照用戶的安全策略設定,由類加載器加載到虛擬機中權限不同的運行空間,來實現差異化代碼的執行權限控制。
圖片
- 當前的是最新的安全機制,引入了域(Domain)的概念。虛擬機會把所有代碼加載到不同的系統域或應用域,系統域部分專門負責與關鍵資源進行交互,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行訪問。虛擬機中不同的受保護域(Protected Domain),對應不一樣的權限(Premission)。存在于不同的域中的類文件就具有了當前域的全部權限。
圖片
- 沙箱的基本組件:
Java.security下的類和擴展包下的類,允許用戶為自己的應用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 數字簽名
- 加密
- 鑒別
- 是核心API和操作系統之間的主要接口,實現權限控制,比存取控制器優先級高。
- 存取控制器可以控制核心API對操作系統的存取權限,而這個控制的策略設定,可以由用戶指定
- 它防止惡意代碼干涉善意的代碼(雙親委派機制)
- 它守護了被信任的類庫邊界
- 它將代碼歸入保護域,確定了代碼可以進行哪些操作
- 字節碼校驗器(bytecode verifier):確保Java類文件遵循Java語言規范。這樣可以幫助Java程序實現內存保護,但并不是所有的類文件都會經過字節碼校驗,比如核心類。
- 類裝載器(ClassLoader):其中類裝載器在三個方面對Java沙箱起作用
虛擬機為不同的類加載器載入的類提供了不同的命名空間,命名空間由一系列唯一的名稱組成,每個被裝載的類將有一個名字,這個命名空間是由Java虛擬機為每一個類裝載器維護的,它們互相之間甚至不可見。 - 存取控制器
- 安全管理器
- 安全軟件包