01.29 從 Java 的平臺無關性引入的一系列面試題

在 Java 面試中,有一條很常見的詢問路線:從對 Java 的認識,到談 Java 的平臺無關性,到 Java 中的反射機制,再到類加載機制,繼而深入到雙親委派機制等。本文將根據這條路線,給出一份可供參考的回答,如有錯誤萬望指正。(推薦讀者們在回答的時候結合自己的認識和項目經歷作答)

1、談談 Java 的 Compile Once,Run Anywhere

1.1、一次編譯到處運行是如何實現的?

Java 程序從編寫到運行需要經歷這麼個過程:首先編寫 Java 源代碼,然後通過 Javac 編譯器將 Java 源代碼編譯成 .class 字節碼文件,然後把字節碼文件交給虛擬機,虛擬機會在執行 Java 程序的時候把字節碼轉換為機器碼執行。

字節碼文件是平臺無關性的重要一環。同一個 Java 程序能夠在 Linux,Windows 等不同平臺上運行,是因為不同平臺上安裝了不同的 Java 虛擬機,這些不同的虛擬機會根據同一份字節碼文件,轉換成各個平臺對應的機器碼,從而使 Java 程序在不同的平臺上運行。

按照這個邏輯,只要是符合規範的字節碼文件就能通過虛擬機在不同平臺運行,我們完全可以使用另一種語言編寫程序,編寫完後把程序編譯為字節碼文件,然後通過虛擬機在不同的平臺上運行。現實中也確實存在運行於 Java 虛擬機之上的其他語言程序。

1.2、為什麼 JVM 不直接將源碼解析成機器碼執行?

不同的機器安裝不同的虛擬機,JVM 直接解析 Java 源碼不是也能做到一次編譯到處運行嗎?

  1. 這樣每次執行之前就要進行各種檢查(這是編譯成字節碼前的工作),降低了執行效率。
  2. 上面提到的兼容其他語言的功能將無法實現。

2、JVM 是如何加載 .class 文件的?

JVM 是通過 Class Loader 類加載器來加載字節碼文件的,在介紹如何加載之前要先講講它加載的目的地,也就是 Java 虛擬機。

2.1、JVM 的結構:

Java 虛擬機是通過 c/c++ 編寫的,可以看作是一個模擬真實計算機的程序,通俗來說說它是一個跑在內存中的機器,而後面我們談到的 JVM 的內存結構就是存放在我們真實計算機的內存中的。(後面我會直接講 JVM 的內存)

JVM 大致可以分為四個模塊:

  1. 類加載器:用於把字節碼文件加載進運行時數據區
  2. 運行時數據區:字節碼文件的不同部分會被放入運行時數據區的不同地方
  3. 執行器:執行字節碼中的程序
  4. 本地代碼接口:調用宿主計算機的一些本地方法( c/c++ 或其他語言編寫的接口)

其中運行時數據區是 Java 虛擬機的精華所在,它把字節碼文件中的數據按一定的格式和數據結構組織存放好,方便了執行器調用。

2.2、JVM 是如何加載 .class 文件的

JVM 是通過類加載器加載 .class 字節碼文件的,而 JVM 中的類加載器不只一個,它是按照父子關係進行劃分的,至於為什麼要這麼劃分,這會涉及到雙親委派機制,這個稍後便講。具體的父子關係如下:

  1. BootStrapClassLoader:最頂層的類加載器,由 C++ 編寫,用於加載 Java 的核心類庫
  2. ExtClassLoader:Java 編寫,用於加載 Java 的擴展庫
  3. AppClassLoader:Java 編寫,用於加載應用程序所在目錄下的 class 文件
  4. 自定義 ClassLoader:Java 編寫,用於定製化加載

由不同類加載器的作用可以知道,不同加載器加載的路徑是不同的。但它們共同的任務都是把字節碼文件加載到運行時數據區中(不同語言實現的類加載器具體加載部分的代碼也會有所差別),更形象地說是把字節碼文件中的二進制 01 數據加載到 JVM 中,並按照運行時數據區進行劃分存放。

2.3、什麼是雙親委派機制,為什麼要制定雙親委派機制?

我們先從一個類的加載順序開始講起:

  1. 當需要加載一個類時,如果沒有自定義類加載器,默認會使用 AppClassLoader 進行加載,
  2. AppClassLoader 先會查找一下該類是否已經被加載,若已經被加載則返回,若沒有則調用它的父加載器 ExtClassLoader 進行加載,
  3. ExtClassLoader 也會查找一下該類是否已經被加載,若已經被加載則返回,若沒有則向上調用它的父加載器 BootStrapClassLoader 進行加載,
  4. BootStrapClassLoader 會嘗試加載該類,若加載不到則返回
  5. 接著 ExtClassLoader 也會嘗試加載該類,若加載不到則返回
  6. 接著 AppClassLoader 也會嘗試加載該類,假設該類被加載到(在項目目錄下),則成功返回,否則拋出 ClassNotFoundException

當然以上過程也可以從自定義的類加載器開始,過程是一樣的。這種先委派父加載器進行加載的機制就是雙親委派機制。

為什麼不直接加載,而是往上委派雙親加載呢?

  1. 因為內存資源是寶貴的,如果一個類已經加載過一次了,也就沒有必要再加載一份相同的拷貝在內存中了。為了保證一個類只被加載一次,加載類時就會從最頂層的父類加載器開始加載,只要加載過了,在後續需要用的時候就會返回已經加載過的 class 文件。
  2. 為了保證安全性和穩定性。Java 自身是有許多核心類的,這些類都通過頂層的父加載器進行加載,保證運行的時候用的是 Java 系統自己的類。如果沒有雙親委派機制保證,用戶自己也可以編寫一些系統類並用自己編寫的類加載器進行加載,那麼就會導致Java 自身的系統類和用戶編寫的類混在一起,破壞了 Java 程序執行的安全性和穩定性。

2.4、額外:談談 Java 中的反射機制

在問到類加載,JVM 等話題時難免會問到反射機制,這裡補充一下:

反射是 Java 中一個很強大的機制,要說清楚反射還得結合上面說到的 JVM 結構去談。

由於類加載器會把整個 .class 文件加載進運行時數據區,.class 文件中包含了一個類的字段,構造函數,以及各種方法,Java 允許我們在程序中通過反射的方式直接操作這些數據。

<code>//比如我們可以根據類的限定類名獲得一個類的實例對象
Class cc = Class.forName("xx.xx.Demo");
Demo demo = (Demo)cc.newIntance();

//獲取並設置一個類的私有字段
Field name = cc.getDeclaredFied("name");
name.setAccessible(true);
name.set(demo, "Zhang san");

//或者調用一個類的方法(公有/私有)
Method sayHi = cc.getMethod("sayHi", String.class);
sayHi.invoke(demo, "Hello");
/<code>

總結一句就是字節碼中的類信息被加載進了運行時數據區,Java 允許我們在程序中直接操作這些類信息。

為什麼說 Java 的反射機制強大呢?這裡就要說到 Spring 框架了。不知道 Java 的反射強大,總該知道 Spring 框架的強大了吧,Spring 就是基於反射實現的。舉個例子:Spring 中最核心的概念莫過於控制反轉了,通俗的解釋就是把對象的生命週期交給框架管理,而不是由程序員管理。既然是交給框架管理對象,那創建對象就不能通過 new 關鍵字實現了,而是由框架通過反射創建,同理對象中方法的調用也由框架通過反射進行調用。


分享到:


相關文章: