最近在準備一個數據庫框架的專題,想從driver一路講到上層orm框架。在準備JDBC這層時發現其中有不少可講的java知識點。因而舍遠求近,先講講框架裏的java。java
本文選取JDBC獲取connection這個切入點,但願類的加載、SPI模式和一些涉及到的其餘知識點。因爲篇幅緣由本文先講解類的加載過程。 經過學習本文能夠學習如下內容:mysql
從下面這個代碼片斷能夠發現JDBC得到一個可以使用的connection僅僅須要調用一個靜態方法。c++
// 實驗腳本
public class JdbcTest {
private String userName;
private String password;
private String serverName;
private String portNumber;
public JdbcTest(String userName, String password, String server, String port){
this.userName = userName;
this.password = password;
this.serverName = server;
this.portNumber = port;
}
public Connection getConnection() throws SQLException {
Connection conn = null;
Properties connectionProps = new Properties();
connectionProps.put("user", this.userName);
connectionProps.put("password", this.password);
conn = DriverManager.getConnection(
"jdbc:mysql://" +
this.serverName +
":" + this.portNumber + "/",
connectionProps);
System.out.println("Connected to database");
return conn;
}
public static void main(String[] args) throws SQLException {
JdbcTest jdbcTest = new JdbcTest("dal", "dal123", "localhost", "3306");
Connection connection = jdbcTest.getConnection();
}
}
複製代碼
單步執行能夠發現從getConnection進入了DriverManager的static代碼塊,注意這裏是在調用getConnection以後才執行這段關鍵代碼。sql
//DriverManager類內部的static代碼塊
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
複製代碼
但這個過程是怎麼實現的爲何發生在getConnection以後呢。這裏就引出了第一個問題,類的加載過程是怎樣的,發生在什麼階段。數據庫
類的加載過程主要分爲3個階段:loading,linking,initializing。同時linking階段又有三個步驟:verifying,preparing和resolving。bootstrap
這裏的加載和類的加載容易引發歧義,加載在不一樣的語境能夠指整個類的加載過程(包括加載、鏈接和初始化),也可能指類的加載中的第一步loading。安全
下面看一下loading作什麼,不作什麼:bash
也有人翻譯稱爲經過名字找到字節流,這種說法有一點不嚴謹可是適用於大部分場景。好比Class.forName每每是經過提供一個名字來定位類的文件的。數據結構
找到,這個信息就很是模糊了,在哪兒找到徹底不限制。這也給了classloader極大的創造空間,好比能夠經過DynamicProxy中動態生成字節流的方法體會自定義類的生成與加載過程,具體參考java.lang.reflect.Proxy/sun.misc.ProxyGenerator生成二進制字節流的源代碼。關於loading下文的類加載器章節會繼續探討。多線程
這句話的信息量就很是大了:這裏涉及JVM中的兩個空間(1)方法區method area, (2)動態常量區run-time constant pool;同時涉及兩個類C和D。
假設C是咱們要加載的類,D就是觸發C加載的類。首先D在本身的run-time constant中得到C的引用,把C的二進制字節流中所表明的靜態存儲結構轉化爲method area/方法區的數據結構並建立class對象做爲這個類的訪問入口。這裏具體的數據結構和內存分區和jvm的不一樣實現相關。
講到link一會兒想到了咱們林克老師,塞爾達的創造者曾解釋以林克爲主角的遊戲爲何命名爲塞爾達:塞爾達表明了這個充滿無窮魅力的世界,而林克就是鏈接咱們進入這個世界的人。回到正題,linking過程,其實也是要把這個進入jvm method area的類和運行時聯繫在一塊兒,從而類/接口進入可執行狀態。
鏈接過程包括三個過程:驗證(verification),準備(preparation)和解析(resolvation)。
其實java編譯器能夠確認編譯過的字節碼是安全的,可是正如loading中講到的,二進制字節能夠來自各個地方。來自各類不明途徑的二進制字節碼是否是有錯誤,是否是有危險,就值得懷疑了。因此在類投入使用以前須要進行驗證。
驗證的內容包括文件格式,語言規範,數據流控制流等。這裏就不展開了。
準備是爲static fields分配內存並賦予默認值的過程。 若是一個類的內容沒有異常,那麼就進入了準備階段。這一階段jvm會爲類的靜態變量分配內存並賦予默認值,注意準備階段並不會執行任何static fields的初始化和static代碼塊。
java代碼中各類引用在編譯時統一用規範的符號引用(symbolic references)來表示,並存儲在class文件裏。符號引用是一種邏輯定位符,追求定位無歧義,但並不表示實際的內存地址。但當真正執行jvm指令時,須要具體能找到數據的內存地址的方法,因而就有了直接引用(direct references)。直接引用可使指針、偏移量等具體不一樣實現。
JVM規範規定了在執行一系列指令( anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface等)時須要直接引用,而解析就是將符號引用替換爲直接引用的過程。
從驗證字節流、分配內存到引用解析能夠看出這一切都是爲執行類中定義的代碼作準備。
一切準備就緒以後,就開始執行類/接口的初始化方法cinit了。cinit是編譯器提供的方法,直接參與類的初始化過程。 編譯器蒐集了靜態變量的賦值、static代碼塊編譯進入cinit方法使之做爲類的惟一構造器;同時一個類的cinit在調用前,jvm會保證其全部父類的cinit也都調用完成。這也解釋了爲何一個類的初始化的緣由是其子類被初始化。
java是自然支持多線程的語言,JVM爲了保證類的初始化線程安全而且只發生一次,每一個類都有初始化鎖unique initialization lock和狀態,類的初始化是「synchronize」的並在結束以後設置類的狀態爲「已初始化」來保證 cinit的執行過程是線程安全的,並且只會加載一次。
這也就是爲何static字段能夠幫助實現單例。到這裏,類的加載就完成了。
回到JDBC的例子,會發現用戶代碼腳本執行完getConnection以後,DriverManager的static代碼塊纔開始執行。這裏就引起了一個疑問:類的整個加載過程是何時發生的呢?
這裏有兩個關鍵認知:(1)類的整個加載環節是不須要一塊兒執行的,一個類可能很早就加載了,但並無初始化;(2)類的整個加載過程是何時發生是根據虛擬機實現的不一樣和一個jvm中類的使用方法不一樣都是不一樣的。
sun公司的JVM通常是lazy策略類的加載的全過程,在使用一個類時,纔會對類進行loading;在須要調用類中的方法和字段時才進行初始化過程。初始化的具體條件,JVM specification中定義的初始化條件比較經常使用到的有:
下面有例子請參考註釋和輸出:
package lang;
import java.lang.reflect.Constructor;
public class Tester {
public static void main(String[] args) throws Exception {
System.out.println("step 1");
Class<TestClassLoader> theTestClass; # 這裏不須要loadclass
System.out.println("step 2");
theTestClass = TestClassLoader.class; # 這裏須要load class,但不須要初始化
Constructor<TestClassLoader> classLoaderConstructor = TestClassLoader.class.getDeclaredConstructor();
System.out.println("step 3");
classLoaderConstructor.newInstance(); # 執行類的代碼時初始化
}
}
輸出內容
step 1
step 2
step 3
static part is loaded
message field is loaded!
testclassloader instance is created
複製代碼
根據上面的分析,JDBC的實驗就很容易理解了,import driverManager並不會觸發static代碼塊來loadInitialDrivers,static執行發生在invokeStatic也就是getConnection以後。
這裏就涉及到另外一個常見的問題,單例模式怎麼寫(沒法理解什麼懶漢惡漢命名法就直接分析寫法了)。 有兩種利用類的加載模式來創造單例的方法。
public class Singleton {
static
{
System.out.println("singleton part is initialized");
}
// 第1種單例寫法
private static TestClassLoader classLoader = new TestClassLoader();
public static TestClassLoader getClassLoader(){
return SingletonHolder.classLoader;
}
// 第2種單例寫法
private static class SingletonHolder{
static {
System.out.println("holder initialized");
}
private static TestClassLoader classLoader = new TestClassLoader();
}
public static TestClassLoader getClassLoader2(){
return SingletonHolder.classLoader;
}
}
複製代碼
講類的加載器每每會講三大加載器bootstrap、ext和app-classloader和雙親委派模型。可是從jvm角度講,類的加載器分爲兩類:jvm直接提供的bootstrapClassloader,和其餘用戶實現的classloader。用戶實現的cl都是抽象類ClassLoader的一個實例。除了array對象由jvm直接建立以外,全部的class和interface都由classloader來load。
類的加載器參與類的運行時命名空間(N,Li):JVM specification規定,運行時對於一個類的定義是(類的名字+類的defining loader)二者的組合,也就是若是你用classloader1 load了名字爲N的類,會在運行時標記爲NL1,用classloader2 load 的N是另外一個運行時的類稱爲NL2,這兩個類就是徹底不一樣的類了。equal,instanceof在兩個類及類的實例之間都不能成立。
剛剛提到一個詞叫作defining loader,什麼叫definingloader呢。這裏就涉及類的委派模型classloader delegation model。一個classloader C能夠直接建立class或者把它委派給另外一個classloader D,直接建立這個class的loader D稱爲defining loader,發起建立動做C的稱爲initiating loader。固然C和D能夠是一個loader- -。
最常聽到的雙親委派總感受翻譯有點問題- -,英文parent delegation其實源自於類的層次關係,classloader中每每經過組合而設置一個邏輯上的parent Classloader來做爲委派對象,而不用繼承的實際父類。下面分析一下源碼中涉及parent的部分。
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
# 核心classLoader中設置parent字段,從而全部具體類都有一個parent,不須要樹狀的繼承關係也能夠經過parent來找到委派的對象;
private final ClassLoader parent;
public URLClassLoader(URL[] urls, ClassLoader parent) {
# URLclassloader是抽象類Classloader的一個實現,你們經常提到的ext/appClassloader都繼承自這個類;
# 雖然ext/app都繼承自這個類,可是他們的parent並非這個類;
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.acc = AccessController.getContext();
ucp = new URLClassPath(urls, acc);
}
static class AppClassLoader extends URLClassLoader {
複製代碼
具體的雙親委派過程核心就在於Classloader中的loadClass的定義了。Classloader完整的實現了loadClass的核心步驟:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
# (1)保證loading的線程安全設置lock
synchronized (getClassLoadingLock(name)) {
# (2)保證類只加載一次,因此得到鎖以後再次檢查是否已加載
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
# (3-1) 若是有parent delegation,則直接委派;
if (parent != null) {
c = parent.loadClass(name, false);
} else {
# (3-2)null表明parent是jvm中的bootstrap loader,直接委派bootstrap;
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
# (4)parent委派找不到類時,才本身嘗試加載類,findClass是該抽象類擴展的核心
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
# (5) 經過參數判斷是否進行link過程;
if (resolve) {
resolveClass(c);
}
return c;
}
}
複製代碼
經過classloader中loadclass的代碼能夠發現雙親委派的核心過程是「啃老」- -!先讓parent幹,幹不動了再本身幹。同時能夠發現,bootstrap被null表明,是默認的第一個parent。固然這個模型能夠經過子類overriding掉,但這個模型是一個推薦的類加載通用模型。
那麼爲何通常講類的加載器都會講ext和app-classloader呢,這裏就要涉及java的Launcher類了。java Laucher涉及三個classloader:設置了bootstrap的路徑,定義並使用內部靜態類ExtClassLoader和AppClassloader。
bootstrapClassloader直接由jvm實現,加載java的lib中的最核心的類,這個加載過程還會根據文件名過濾好比rt.jar的類庫都是由bootstrap直接加載的。
從laucher中的源碼能夠看到,Ext和App都是UrlClassloader的子類。ExtClassLoader設定parent是null,null表明jvm內部c++實現的bootstrapClassloader;Ext對應的路徑是java.ext.dirs系統變量中的類目錄; AppClassloader被launcher將其parent設置爲ExtClassLoader,其對應的類加載路徑是java.class.path也就指向用戶實現的類的CLASSPATH。根據這兩個類的設定就有了一個系統加載對類進行加載的委派路線,參考下圖。
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));
複製代碼
launcher所在rt.jar是由bootstrap進行加載的,初始化時生成了實例。具體分析請直接參考下面源碼及註釋。
private static Launcher launcher = new Launcher();
public Launcher() {
//(1)這裏初始化extClassloader和appClassloader,並把ext設置爲app的parent
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// (2)這裏把app設置爲線程上下文裏的類加載器,從而創建起了系統的類的委派模型;
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
複製代碼
本文的內容就到此爲止了,一下篇會講完整個jdbc getConnection的過程,大體的內容有spi模式,doPrivilege,class.forName的用法:)