
Java 教程
- Java - 首頁
- Java - 概述
- Java - 歷史
- Java - 特性
- Java 與 C++
- JVM - Java 虛擬機器
- Java - JDK 與 JRE 與 JVM
- Java - Hello World 程式
- Java - 環境設定
- Java - 基本語法
- Java - 變數型別
- Java - 資料型別
- Java - 型別轉換
- Java - Unicode 系統
- Java - 基本運算子
- Java - 註釋
- Java - 使用者輸入
- Java - 日期和時間
Java 控制語句
- Java - 迴圈控制
- Java - 決策制定
- Java - If-else
- Java - Switch
- Java - For 迴圈
- Java - For-Each 迴圈
- Java - While 迴圈
- Java - do-while 迴圈
- Java - Break
- Java - Continue
面向物件程式設計
- Java - OOPs 概念
- Java - 物件和類
- Java - 類屬性
- Java - 類方法
- Java - 方法
- Java - 變數作用域
- Java - 建構函式
- Java - 訪問修飾符
- Java - 繼承
- Java - 聚合
- Java - 多型
- Java - 重寫
- Java - 方法過載
- Java - 動態繫結
- Java - 靜態繫結
- Java - 例項初始化塊
- Java - 抽象
- Java - 封裝
- Java - 介面
- Java - 包
- Java - 內部類
- Java - 靜態類
- Java - 匿名類
- Java - 單例類
- Java - 包裝類
- Java - 列舉
- Java - 列舉建構函式
- Java - 列舉字串
Java 內建類
Java 檔案處理
Java 錯誤和異常
- Java - 異常
- Java - try-catch 塊
- Java - try-with-resources
- Java - 多重 catch 塊
- Java - 巢狀 try 塊
- Java - Finally 塊
- Java - throw 異常
- Java - 異常傳播
- Java - 內建異常
- Java - 自定義異常
Java 多執行緒
- Java - 多執行緒
- Java - 執行緒生命週期
- Java - 建立執行緒
- Java - 啟動執行緒
- Java - 執行緒連線
- Java - 執行緒命名
- Java - 執行緒排程器
- Java - 執行緒池
- Java - 主執行緒
- Java - 執行緒優先順序
- Java - 守護執行緒
- Java - 執行緒組
- Java - 關閉鉤子
Java 同步
Java 網路
- Java - 網路
- Java - 套接字程式設計
- Java - URL 處理
- Java - URL 類
- Java - URLConnection 類
- Java - HttpURLConnection 類
- Java - Socket 類
- Java - 泛型
Java 集合
Java 介面
Java 資料結構
Java 集合演算法
高階 Java
- Java - 命令列引數
- Java - Lambda 表示式
- Java - 傳送電子郵件
- Java - Applet 基礎
- Java - Javadoc 註釋
- Java - 自動裝箱和拆箱
- Java - 檔案不匹配方法
- Java - REPL (JShell)
- Java - 多版本 Jar 檔案
- Java - 私有介面方法
- Java - 內部類菱形運算子
- Java - 多解析度影像 API
- Java - 集合工廠方法
- Java - 模組系統
- Java - Nashorn JavaScript
- Java - Optional 類
- Java - 方法引用
- Java - 函式式介面
- Java - 預設方法
- Java - Base64 編碼解碼
- Java - Switch 表示式
- Java - Teeing 收集器
- Java - 微基準測試
- Java - 文字塊
- Java - 動態 CDS 存檔
- Java - Z 垃圾收集器 (ZGC)
- Java - 空指標異常
- Java - 打包工具
- Java - 密封類
- Java - 記錄類
- Java - 隱藏類
- Java - 模式匹配
- Java - 緊湊數字格式化
- Java - 垃圾回收
- Java - JIT 編譯器
Java 雜項
- Java - 遞迴
- Java - 正則表示式
- Java - 序列化
- Java - 字串
- Java - 程序 API 改進
- Java - 流 API 改進
- Java - 增強的 @Deprecated 註釋
- Java - CompletableFuture API 改進
- Java - 流
- Java - 日期時間 API
- Java 8 - 新特性
- Java 9 - 新特性
- Java 10 - 新特性
- Java 11 - 新特性
- Java 12 - 新特性
- Java 13 - 新特性
- Java 14 - 新特性
- Java 15 - 新特性
- Java 16 - 新特性
Java API 和框架
Java 類參考
- Java - Scanner
- Java - 陣列
- Java - 字串
- Java - Date
- Java - ArrayList
- Java - Vector
- Java - Stack
- Java - PriorityQueue
- Java - LinkedList
- Java - ArrayDeque
- Java - HashMap
- Java - LinkedHashMap
- Java - WeakHashMap
- Java - EnumMap
- Java - TreeMap
- Java - IdentityHashMap
- Java - HashSet
- Java - EnumSet
- Java - LinkedHashSet
- Java - TreeSet
- Java - BitSet
- Java - Dictionary
- Java - Hashtable
- Java - Properties
- Java - Collection
- Java - Array
Java 有用資源
Java - 即時 (JIT) 編譯器
即時 (JIT) 編譯器是 JVM 內部使用的編譯器,用於將位元組碼中的熱點轉換為機器可理解的程式碼。JIT 編譯器的主要目的是對效能進行大量最佳化。
Java 編譯的程式碼面向 JVM。Java 編譯器 javac 將 Java 程式碼編譯成位元組碼。現在 JVM 解釋此位元組碼並在底層硬體上執行它。如果某些程式碼要反覆執行,JVM 會將該程式碼識別為熱點,並使用 JIT 編譯器 將程式碼進一步編譯到本機機器程式碼級別,並在需要時重用編譯後的程式碼。
讓我們首先了解編譯型語言與解釋型語言之間的區別,以及 Java 如何利用這兩種方法的優勢。
編譯型語言與解釋型語言
諸如 C、C++ 和 FORTRAN 等語言是編譯型語言。它們的程式碼作為面向底層機器的二進位制程式碼交付。這意味著高階程式碼由專門為底層架構編寫的靜態編譯器一次性編譯成二進位制程式碼。生成的二進位制檔案不會在任何其他架構上執行。
另一方面,解釋型語言(如 Python 和 Perl)可以在任何機器上執行,只要它們具有有效的直譯器即可。它逐行遍歷高階程式碼,將其轉換為二進位制程式碼。
解釋型程式碼通常比編譯型程式碼慢。例如,考慮一個迴圈。一個直譯器將為迴圈的每次迭代轉換相應的程式碼。另一方面,編譯後的程式碼只會轉換一次。此外,由於直譯器一次只看到一行程式碼,因此它們無法執行任何重要的程式碼最佳化,例如更改語句的執行順序,例如編譯器。
示例
我們將在下面研究此類最佳化的一個示例:
將儲存在記憶體中的兩個數字相加:由於訪問記憶體可能會消耗多個 CPU 週期,因此一個好的編譯器會發出指令從記憶體中獲取資料,並且只有在資料可用時才執行加法運算。它不會等待,並且在此期間執行其他指令。另一方面,在解釋期間不可能進行此類最佳化,因為直譯器在任何給定時間都不瞭解整個程式碼。
但是,解釋型語言可以在任何具有該語言有效直譯器的機器上執行。
Java 是編譯型語言還是解釋型語言?
Java 試圖找到一個折衷方案。由於 JVM 位於 javac 編譯器和底層硬體之間,因此 javac(或任何其他編譯器)編譯器會將 Java 程式碼編譯成位元組碼,位元組碼由特定於平臺的 JVM 理解。然後,JVM 在程式碼執行時使用JIT(即時)編譯將位元組碼編譯成二進位制程式碼。
熱點
在典型的程式中,只有一小部分程式碼會被頻繁執行,並且通常,正是這段程式碼會顯著影響整個應用程式的效能。此類程式碼部分稱為熱點。
如果某些程式碼段只執行一次,那麼編譯它將是一種浪費,並且解釋位元組碼會更快。但是,如果該部分是熱點並且執行多次,則 JVM 將對其進行編譯。例如,如果多次呼叫某個方法,那麼編譯程式碼所需的多餘週期將被生成的更快二進位制程式碼抵消。
此外,JVM 執行特定方法或迴圈的次數越多,它收集的資訊就越多,從而進行各種最佳化,以便生成更快的二進位制程式碼。
JIT 編譯器的工作原理
JIT 編譯器透過將某些熱點程式碼編譯成本機程式碼或機器程式碼來幫助提高 Java 程式的執行時間。
JVM 掃描完整程式碼並識別熱點或需要由 JIT 最佳化的程式碼,然後在執行時呼叫 JIT 編譯器,從而提高程式的效率並使其執行得更快。
由於JIT 編譯是一項佔用處理器和記憶體的活動,因此需要相應地規劃 JIT 編譯。
編譯級別
JVM 支援五種編譯級別 -
- 直譯器
- 帶有完整最佳化(無分析)的 C1
- 帶有呼叫和回邊計數器(輕量級分析)的 C1
- 帶有完整分析的 C1
- C2(使用來自先前步驟的分析資料)
如果您希望停用所有 JIT 編譯器 並僅使用直譯器,請使用 -Xint。
客戶端與伺服器 JIT(即時)編譯器
使用 -client 和 -server 啟用相應的模式。客戶端編譯器(C1)比伺服器編譯器(C2)更早開始編譯程式碼。因此,當 C2 開始編譯時,C1 已經編譯了部分程式碼。
但是,在等待時,C2 會分析程式碼以瞭解比 C1 更多的資訊。因此,等待的時間被可以用來生成更快的二進位制檔案的最佳化所抵消。
從使用者的角度來看,權衡是在程式的啟動時間和程式執行時間之間。如果啟動時間是首要因素,則應使用 C1。如果應用程式預計要執行很長時間(伺服器上部署的應用程式的典型情況),最好使用 C2,因為它會生成更快的程式碼,從而大大抵消任何額外的啟動時間。
對於 IDE(NetBeans、Eclipse)和其他 GUI 程式等程式,啟動時間至關重要。NetBeans 可能需要一分鐘或更長時間才能啟動。當啟動 NetBeans 等程式時,會編譯數百個類。在這種情況下,C1 編譯器是最佳選擇。
請注意,C1 有兩個版本 - 32 位和 64 位。C2 僅提供 64 位版本。
JIT 編譯器最佳化的示例
以下示例展示了 JIT 編譯器最佳化
物件情況下 JIT 最佳化的示例
讓我們考慮以下程式碼 -
for(int i = 0 ; i <= 100; i++) { System.out.println(obj1.equals(obj2)); //two objects }
如果這段程式碼被解釋,直譯器將在每次 for each 迭代中推斷出 obj1 的類。這是因為 Java 中的每個類都有一個 .equals() 方法,該方法擴充套件自 Object 類並且可以被覆蓋。因此,即使 obj1 在每次迭代中都是字串,仍然會進行推斷。
另一方面,實際上會發生的是,JVM 會注意到每次迭代中 obj1 都是 String 類,因此,它會直接生成對應於 String 類的 .equals() 方法 的程式碼。因此,不需要查詢,編譯後的程式碼將執行得更快。
這種行為只有在 JVM 知道程式碼的行為時才有可能。因此,它會在編譯程式碼的某些部分之前等待。
基本資料型別情況下 JIT 最佳化的示例
下面是另一個示例 -
int sum = 7; for(int i = 0 ; i <= 100; i++) { sum += i; }
對於每個迴圈,直譯器都會從記憶體中獲取 'sum' 的值,將 'i' 加到它上面,並將其儲存回記憶體中。記憶體訪問是一項昂貴的操作,通常需要多個 CPU 週期。由於此程式碼執行多次,因此它是一個熱點。JIT 將編譯此程式碼並進行以下最佳化。
'sum' 的本地副本將儲存在暫存器中,該暫存器特定於某個執行緒。所有操作都將在暫存器中的值上執行,並且當迴圈完成後,該值將被寫回記憶體。
如果其他執行緒也在訪問該變數怎麼辦?由於其他執行緒正在對變數的本地副本進行更新,因此它們將看到過時的值。在這種情況下,需要執行緒同步。一個非常基本的同步原語是將 'sum' 宣告為 volatile。現在,在訪問變數之前,執行緒將重新整理其本地暫存器並從記憶體中獲取該值。訪問它之後,該值會立即寫入記憶體。
即時 (JIT) 編譯器執行的最佳化
以下是 JIT 編譯器執行的一些通用最佳化 -
- 方法內聯
- 死程式碼消除
- 最佳化呼叫站點的啟發式方法
- 常量摺疊