前言
本文快速回顧了常考的的知識點,用作面試複習,事半功倍。
上篇主要內容為:虛擬機器資料區域,垃圾回收
下篇主要內容為:類載入機制
面試知識點複習手冊
全複習手冊文章導航
Csdn全複習手冊文章導航:blog.csdn.net/qqxx6661/ar…
已釋出知識點複習手冊
- Java基礎知識點面試手冊
- 快速梳理23種常用的設計模式
- Redis基礎知識點面試手冊
- Java容器(List、Set、Map)知識點快速複習手冊
- Java併發知識點快速複習手冊(上)
- Java併發知識點快速複習手冊(下)
- Java虛擬機器知識點快速複習手冊(上)
- Java虛擬機器知識點快速複習手冊(下)
參考
本文內容參考自CyC2018的Github倉庫:CS-Notes
有刪減,修改,補充額外增加內容
其他參考文章:
-
微信文章:精華:Java 開發崗面試知識點解析
本作品採用知識共享署名-非商業性使用 4.0 國際許可協議進行許可。
類載入機制
類是在執行期間動態載入的。
類的生命週期
包括以下 7 個階段:
- 載入(Loading)
- 驗證(Verification)
- 準備(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 解除安裝(Unloading)
其中解析過程在某些情況下可以在初始化階段之後再開始,這是為了支援 Java 的動態繫結。
類載入過程
包含了載入、驗證、準備、解析和初始化這 5 個階段。
1. 載入
載入是類載入的一個階段,注意不要混淆。
載入過程完成以下三件事:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流。
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時儲存結構。
- 在記憶體中生成一個代表這個類的 Class 物件,作為方法區這個類的各種資料的訪問入口。
其中二進位制位元組流可以從以下方式中獲取:
- 從 ZIP 包讀取,成為 JAR、EAR、WAR 格式的基礎。
- 從網路中獲取,最典型的應用是 Applet。
- 執行時計算生成,例如動態代理技術,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理類的二進位制位元組流。
- 由其他檔案生成,例如由 JSP 檔案生成對應的 Class 類。
2. 驗證
確保 Class 檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。
3. 準備
準備階段為static修飾的變數分配記憶體並設定初始值,使用的是方法區的記憶體。
例項變數不會在這階段分配記憶體,它將會在物件例項化時隨著物件一起分配在Java堆中。
初始值一般為 0 值,例如下面的類變數 value 被初始化為 0 而不是 123。
public static int value = 123;
複製程式碼
如果類變數是常量,那麼會按照表示式來進行初始化,而不是賦值為 0。
public static final int value = 123;
複製程式碼
4. 解析
將常量池的符號引用替換為直接引用的過程。
其中解析過程在某些情況下可以在初始化階段之後再開始,這是為了支援 Java 的動態繫結。
5. 初始化
初始化階段才真正開始執行類中定義的 Java 程式程式碼。初始化階段即虛擬機器執行類構造器 <clinit>() 方法的過程。
在準備階段,類變數已經賦過一次系統要求的初始值,而在初始化階段,根據程式設計師通過程式制定的主觀計劃去初始化類變數和其它資源。
<clinit>() 方法具有以下特點:
- 是由編譯器自動收集類中所有類變數的賦值動作和靜態語句塊中的語句合併產生的,編譯器收集的順序由語句在原始檔中出現的順序決定。特別注意的是,靜態語句塊只能訪問到定義在它之前的類變數,定義在它之後的類變數只能賦值,不能訪問。例如以下程式碼:
public class Test {
static {
i = 0; // 給變數賦值可以正常編譯通過
System.out.print(i); // 這句編譯器會提示“非法向前引用”
}
static int i = 1;
}
複製程式碼
-
與類的建構函式(或者說例項構造器 <init>())不同,不需要顯式的呼叫父類的構造器。虛擬機器會自動保證在子類的 <clinit>() 方法執行之前,父類的 <clinit>() 方法已經執行結束。因此虛擬機器中第一個執行 <clinit>() 方法的類肯定為 java.lang.Object。
-
由於父類的 <clinit>() 方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作。例如以下程式碼:
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
複製程式碼
-
<clinit>() 方法對於類或介面不是必須的,如果一個類中不包含靜態語句塊,也沒有對類變數的賦值操作,編譯器可以不為該類生成 <clinit>() 方法。
-
介面中不可以使用靜態語句塊,但仍然有類變數初始化的賦值操作,因此介面與類一樣都會生成 <clinit>() 方法。但介面與類不同的是,執行介面的 <clinit>() 方法不需要先執行父介面的 <clinit>() 方法。只有當父介面中定義的變數使用時,父介面才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的 <clinit>() 方法。
-
虛擬機器會保證一個類的 <clinit>() 方法在多執行緒環境下被正確的加鎖和同步,如果多個執行緒同時初始化一個類,只會有一個執行緒執行這個類的 <clinit>() 方法,其它執行緒都會阻塞等待,直到活動執行緒執行 <clinit>() 方法完畢。如果在一個類的 <clinit>() 方法中有耗時的操作,就可能造成多個執行緒阻塞,在實際過程中此種阻塞很隱蔽。
類初始化時機
主動引用
虛擬機器規範中並沒有強制約束何時進行載入,但是規範嚴格規定了有且只有下列五種情況必須對類進行初始化(載入、驗證、準備都會發生):
-
遇到 new、getstatic、putstatic、invokestatic 這四條位元組碼指令時,如果類沒有進行過初始化,則必須先觸發其初始化。最常見的生成這4 條指令的場景是:
- new 關鍵字例項化物件;
- 讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候;
- 呼叫類的靜態方法。
-
類進行反射呼叫的時候,如果類沒有進行初始化,則需要先觸發其初始化。
-
當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
-
當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機器會先初始化主類;
-
當使用 JDK 1.7 的動態語言支援時,如果一個 java.lang.invoke.MethodHandle 例項最後的解析結果為 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行過初始化,則需要先觸發其初始化;
被動引用
以上 5 種場景中的行為稱為對一個類進行主動引用。
除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用。
被動引用的常見例子包括:
- 通過子類引用父類的靜態欄位,不會導致子類初始化。
System.out.println(SubClass.value); // value 欄位在 SuperClass 中定義
複製程式碼
- 通過陣列定義來引用類,不會觸發此類的初始化。該過程會對陣列類進行初始化,陣列類是一個由虛擬機器自動生成的、直接繼承自 Object 的子類,其中包含了陣列的屬性和方法。
SuperClass[] sca = new SuperClass[10];
複製程式碼
- 使用常量:常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
System.out.println(ConstClass.HELLOWORLD);
複製程式碼
類載入器
實現類的載入動作。在 Java 虛擬機器外部實現,以便讓應用程式自己決定如何去獲取所需要的類。
1. 類與類載入器
兩個類相等:類本身相等,並且還要使用同一個類載入器進行載入。這是因為每一個類載入器都擁有一個獨立的類名稱空間。
這裡的相等,包括類的
- Class 物件的 equals() 方法
- isAssignableFrom() 方法
- isInstance() 方法的返回結果為 true
- 也包括使用 instanceof 關鍵字做物件所屬關係判定結果為 true。
2. 類載入器分類
從 Java 虛擬機器的角度來講,只存在以下兩種不同的類載入器:
-
啟動類載入器(Bootstrap ClassLoader),這個類載入器用C++實現,是虛擬機器自身的一部分;
-
所有其他類的載入器,這些類由 Java 實現,獨立於虛擬機器外部,並且全都繼承自抽象類 java.lang.ClassLoader。
從 Java 開發人員的角度看,類載入器可以劃分得更細緻一些:
-
啟動類載入器(Bootstrap ClassLoader):載入<JAVA_HOME>\lib目錄下核心庫
-
擴充套件類載入器(Extension ClassLoader):載入<JAVA_HOME>\lib\ext目錄下擴充套件包
-
應用程式類載入器(Application ClassLoader): 載入使用者路徑(classpath)上指定的類庫。由於這個類載入器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般稱為系統類載入器。 ,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
3. 雙親委派模型
應用程式都是由三種類載入器相互配合進行載入的,如果有必要,還可以加入自己定義的類載入器。
下圖展示的類載入器之間的層次關係,稱為類載入器的雙親委派模型(Parents Delegation Model)。
該模型要求除了頂層的啟動類載入器外,其餘的類載入器都應有自己的父類載入器。
這裡類載入器之間的父子關係一般通過組合(Composition)關係來實現,而不是通過繼承的關係實現。
(一)工作過程
一個類載入器首先將類載入請求傳送到父類載入器,只有當父類載入器無法完成類載入請求時才嘗試載入。
(二)好處
Java類伴隨其類載入器具備了帶有優先順序的層次關係,確保了在各種載入環境的載入順序。
保證了執行的安全性,防止不可信類扮演可信任的類。
例如 java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 的類並放到 ClassPath 中,程式可以編譯通過。因為雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優先順序更高,因為 rt.jar 中的 Object 使用的是啟動類載入器,而 ClassPath 中的 Object 使用的是應用程式類載入器。正因為 rt.jar 中的 Object 優先順序更高,因為程式中所有的 Object 都是這個 Object。
(三)實現
以下是抽象類java.lang.ClassLoader的程式碼片段,其中的 loadClass() 方法執行過程如下:
- 先檢查類是否已經載入過,如果沒有則讓父類載入器去載入。當父類載入器載入失敗時丟擲 ClassNotFoundException,此時嘗試自己去載入。
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
複製程式碼
4. 自定義類載入器實現
FileSystemClassLoader 是自定義類載入器,繼承自 java.lang.ClassLoader,用於載入檔案系統上的類。它首先根據類的全名在檔案系統上查詢類的位元組程式碼檔案(.class 檔案),然後讀取該檔案內容,最後通過 defineClass() 方法來把這些位元組程式碼轉換成 java.lang.Class 類的例項。
loadClass() 實現了雙親委派模型的邏輯,因此自定義類載入器一般不去重寫它,但是需要重寫 findClass() 方法。
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
複製程式碼
雙親委派的理解
雙親委派模型的實現過程:
實現雙親委派模型的程式碼都集中在java.lang.ClassLoader的loadClass()方法中:
1.當Application ClassLoader 收到一個類載入請求時,他首先不會自己去嘗試載入這個類,而是將這個請求委派給父類載入器Extension ClassLoader去完成。
2.當Extension ClassLoader收到一個類載入請求時,他首先也不會自己去嘗試載入這個類,而是將請求委派給父類載入器Bootstrap ClassLoader去完成。
3.如果Bootstrap ClassLoader載入失敗(在<JAVA_HOME>\lib中未找到所需類),就會讓Extension ClassLoader嘗試載入。
4.如果Extension ClassLoader也載入失敗,就會使用Application ClassLoader載入。
5.如果Application ClassLoader也載入失敗,就會使用自定義載入器去嘗試載入。
6.如果均載入失敗,就會丟擲ClassNotFoundException異常。
雙親委派模型的優點:
保證了執行的安全性,防止不可信類扮演可信任的類。
Class.forName()的理解
Class.forName(className)可以簡單的理解為:獲得字串引數中指定的類,並進行初始化操作。
Class.forName的一個很常見的用法是在載入資料庫驅動的時候。
首先你要明白在java裡面任何class都要裝載在虛擬機器上才能執行。
-
forName這句話就是裝載類用的(new是根據載入到記憶體中的類建立一個例項,要分清楚)。
-
至於什麼時候用,可以考慮一下這個問題,給你一個字串變數,它代表一個類的包名和類名,你怎麼例項化它?
A a = (A)Class.forName("pacage.A").newInstance();
這和 A a =new A();是一樣的效果。
複製程式碼
-
jvm在裝載類時會執行類的靜態程式碼段,要記住靜態程式碼是和class繫結的,class裝載成功就表示執行了你的靜態程式碼了,而且以後不會再執行這段靜態程式碼了。
-
動態載入和建立Class 物件,比如想根據使用者輸入的字串來建立物件
String str = 使用者輸入的字串
Class t = Class.forName(str);
t.newInstance();
複製程式碼
newInstance()方法和new關鍵字最主要的區別
- 一個是方法,一個是關鍵字
- 它們的區別在於建立物件的方式不一樣,前者是使用類載入機制,後者是建立一個新類。
- 這主要考慮到軟體的可伸縮、可擴充套件和可重用等軟體設計思想。
- newInstance: 弱型別。低效率。只能呼叫無參構造。
- new: 強型別。相對高效。能呼叫任何public構造。
JVM 引數
GC 優化配置
配置 | 描述 |
---|---|
-Xms | 初始化堆記憶體大小 |
-Xmx | 堆記憶體最大值 |
-Xmn | 新生代大小 |
-XX:PermSize | 初始化永久代大小 |
-XX:MaxPermSize | 永久代最大容量 |
GC 型別設定
配置 | 描述 |
---|---|
-XX:+UseSerialGC | 序列垃圾回收器 |
-XX:+UseParallelGC | 並行垃圾回收器 |
-XX:+UseConcMarkSweepGC | 併發標記掃描垃圾回收器 |
-XX:ParallelCMSThreads= | 併發標記掃描垃圾回收器 = 為使用的執行緒數量 |
-XX:+UseG1GC | G1 垃圾回收器 |
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar
複製程式碼
關注我
我是蠻三刀把刀,目前為後臺開發工程師。主要關注後臺開發,網路安全,Python爬蟲等技術。
來微信和我聊聊:yangzd1102
Github:github.com/qqxx6661
原創部落格主要內容
- 筆試面試複習知識點手冊
- Leetcode演算法題解析(前150題)
- 劍指offer演算法題解析
- Python爬蟲相關技術分析和實戰
- 後臺開發相關技術分析和實戰
同步更新以下部落格
1. Csdn
擁有專欄:Leetcode題解(Java/Python)、Python爬蟲開發
2. 知乎
擁有專欄:碼農面試助攻手冊
3. 掘金
4. 簡書
個人公眾號:Rude3Knife
如果文章對你有幫助,不妨收藏起來並轉發給您的朋友們~