MNIST手寫數字集的多分類問題(Convolution Neural Network)
一、類的生命週期
類被載入到jvm虛擬機器記憶體開始,到卸載出記憶體為止,他的生命週期可以分為:載入->驗證->準備->解析->初始化->使用->解除安裝。
其中驗證、準備、解析統一稱為連結階段
1、載入
將類的位元組碼載入方法區中,內部採用 C++ 的 instanceKlass 描述 java 類,它的重要 field 有:
_java_mirror 即 java 的類映象,例如對 String 來說,就是 String.class,作用是把 klass 暴露給 java 使用
_super 即父類
_fields 即成員變數
_methods 即方法
_constants 即常量池
_class_loader 即類載入器
_vtable 虛方法表
_itable 介面方法表
如果這個類還有父類沒有載入,先載入父類 載入和連結可能是交替執行的
2、連結
2.1驗證
驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。驗證階段大致會完成4個階段的檢驗動作:
1)檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機器的處理範圍之內、常量池中的常量是否有不被支援的型別。
2)元資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。
3)位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。
4)符號引用驗證:確保解析動作能正確執行。
驗證階段是非常重要的,但不是必須的,它對程式執行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用-Xverifynone
引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間。
2.2準備
當完成位元組碼檔案的校驗之後,JVM 便會開始為類變數分配記憶體並初始化。這裡需要注意兩個關鍵點,即記憶體分配的物件以及初始化的型別。
記憶體分配的物件。Java 中的變數有「類變數」和「類成員變數」兩種型別,「類變數」指的是被 static 修飾的變數,而其他所有型別的變數都屬於「類成員變數」。
在準備階段,JVM 只會為「類變數」分配記憶體,而不會為「類成員變數」分配記憶體。「類成員變數」的記憶體分配需要等到初始化階段才開始。
例如下面的程式碼在準備階段,只會為 factor 屬性分配記憶體,而不會為 website 屬性分配記憶體。
public static int factor = 3;
public String website = "www.baidu.com";
初始化的型別。在準備階段,JVM 會為類變數分配記憶體,併為其初始化。但是這裡的初始化指的是為變數賦予 Java 語言中該資料型別的零值,而不是使用者程式碼裡初始化的值。
例如下面的程式碼在準備階段之後,sector 的值將是 0,而不是 3。
public static int sector = 3;
但如果一個變數是常量(被 static final 修飾)的話,那麼在準備階段,屬性便會被賦予使用者希望的值。例如下面的程式碼在準備階段之後,number 的值將是 3,而不是 0。
public static final int number = 3;
2.3解析
解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行。
符號引用:簡單的理解就是字串,比如引用一個類,java.util.ArrayList 這就是一個符號引用,字串引用的物件不一定被載入。
直接引用:指標或者地址偏移量。引用物件一定在記憶體(已經載入)。
3、初始化
初始化,這個階段就是執行類構造器< clinit >()方法的過程,為類的靜態變數賦予正確的初始值,JVM負責對類進行初始化,主要對類變數進行初始化。
在Java中對類變數進行初始值設定有兩種方式:
宣告類變數是指定初始值
使用靜態程式碼塊為類變數指定初始值
JVM初始化步驟
1)假如這個類還沒有被載入和連線,則程式先載入並連線該類
2)假如該類的直接父類還沒有被初始化,則先初始化其直接父類
3)假如類中有初始化語句,則系統依次執行這些初始化語句
類初始化時機:只有當對類的主動使用的時候才會導致類的初始化,類的主動使用包括以下六種:
1)建立類的例項,也就是new的方式
2)訪問某個類或介面的靜態變數,或者對該靜態變數賦值
3)呼叫類的靜態方法
4)反射(如Class.forName(“com.shengsiyuan.Test”))
5)初始化某個類的子類,則其父類也會被初始化
6)Java虛擬機器啟動時被標明為啟動類的類(Java Test),直接使用java.exe命令來執行某個主類
不會導致類初始化的情況
1)訪問類的 static final 靜態常量(基本型別和字串)不會觸發初始化
2)類物件.class 不會觸發初始化
3)建立該類的陣列不會觸發初始化
4、使用
當 JVM 完成初始化階段之後,JVM 便開始從入口方法開始執行使用者的程式程式碼。
5、解除安裝
當用戶程式程式碼執行完畢後,JVM 便開始銷燬建立的 Class 物件,最後負責執行的 JVM 也退出記憶體。
二、類載入器
1、類載入器分類
1)啟動類載入器:Bootstrap ClassLoader,負責載入存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath引數指定的路徑中的,並且能被虛擬機器識別的類庫(如rt.jar,所有的java.開頭的類均被Bootstrap ClassLoader載入)。啟動類載入器是無法被Java程式直接引用的。
2)擴充套件類載入器:Extension ClassLoader,該載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫(如javax.開頭的類),開發者可以直接使用擴充套件類載入器。
3)應用程式類載入器:Application ClassLoader,該類載入器由sun.misc.Launcher$AppClassLoader來實現,它負責載入使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
應用程式都是由這三種類載入器互相配合進行載入的,如果有必要,我們還可以加入自定義的類載入器。
2、雙親委派
類載入器載入類的原始碼
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1. 檢查該類是否已經載入
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上級的話,委派上級 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 如果沒有上級了(ExtClassLoader),則委派BootstrapClassLoader
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.
long t1 = System.nanoTime();
// 4. 每一層找不到,呼叫 findClass 方法(每個類載入器自己擴充套件)來載入
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();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
從圖中我們發現除啟動類載入器外,每個載入器都有父的類載入器。
雙親委派機制:如果一個類載入器在接到載入類的請求時,它首先不會自己嘗試去載入這個類,而是把這個請求任務委託給父類載入器去完成,依次遞迴,如果父類載入器可以完成類載入任務,就成功返回;
只有父類載入器無法完成此載入任務時,才自己去載入。
從類的繼承關係來看,ExtClassLoader和AppClassLoader都是繼承URLClassLoader,都是ClassLoader的子類。而BootStrapClassLoader是有C寫的,不再java的ClassLoader子類中。
從圖中可以看到類載入器間的父子關係不是以繼承的方式實現的,而是以組合關係的方式來複用父載入器的程式碼。
如果一個類載入器收到了類載入的請求,它首先會把這個請求委派給父載入器去完成,每一個層次的類載入器都是如此。
雙親委派模型的好處
Java類隨著載入它的類載入器一起具備了一種帶有優先順序的層次關係。比如,Java中的Object類,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object在各種類載入環境中都是同一個類。如果不採用雙親委派模型,那麼由各個類載入器自己取載入的話,那麼系統中會存在多種不同的Object類。
3、打破雙親委派
3.1 自定義載入器重寫loadClass()方法,具體可以參考https://www.cnblogs.com/ITPower/p/13211490.html
3.2執行緒上下文類載入器(利用了java的SPI機制)
這裡以JDBC為例來講解,我們在使用 JDBC 時,都需要載入 Driver 驅動,不知道你注意到沒有,不寫Class.forName("com.mysql.jdbc.Driver"),也是可以讓 com.mysql.jdbc.Driver 正確載入,那是怎麼做到的呢,我們看一下原始碼
public class DriverManager {
// 註冊驅動的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
// 初始化驅動
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
我們手動輸出一下DriverManager的類載入器
System.out.println(DriverManager.class.getClassLoader());
列印 null,表示它的類載入器是 Bootstrap ClassLoader,會到 JAVA_HOME/jre/lib 下搜尋類,但 JAVA_HOME/jre/lib 下顯然沒有 mysql-connector-java-5.1.47.jar 包,
這樣問題來了,在 DriverManager 的靜態程式碼塊中,怎麼能正確載入 com.mysql.jdbc.Driver 呢
繼續看 loadInitialDrivers() 方法:
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
1)使用 ServiceLoader 機制載入驅動,即 SPI
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2)使用 jdbc.drivers 定義的驅動名載入驅動
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
先看 2)發現它最後是使用 Class.forName 完成類的載入和初始化,關聯的是應用程式類載入器,因此可以順利完成類載入
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)約定如下,在 jar 包的 META-INF/services 包下,以介面全限定名名為檔案,檔案內容是實現類名稱
這樣就可以使用ServiceLoader來得到實現類,體現的是【面向介面程式設計+解耦】的思想,在下面一些框架中都運用了此思想:
JDBC
Servlet 初始化器
Spring 容器
Dubbo(對 SPI 進行了擴充套件)
接著看 ServiceLoader.load 方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 獲取執行緒上下文類載入器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
執行緒上下文類載入器是當前執行緒使用的類載入器,預設就是應用程式類載入器,它內部又是由 Class.forName 呼叫了執行緒上下文類載入器完成類載入,具體程式碼在 ServiceLoader 的內部類 LazyIterator 中:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
4、自定義載入器
問問自己,什麼時候需要自定義類載入器
1)想載入非 classpath 隨意路徑中的類檔案
2)都是通過介面來使用實現,希望解耦時,常用在框架設計
3)這些類希望予以隔離,不同應用的同名類都可以載入,不衝突,常見於 tomcat 容器
步驟:1) 繼承 ClassLoader 父類
2)要遵從雙親委派機制,重寫 findClass 方法,注意不是重寫 loadClass 方法,否則不會走雙親委派機制
3)讀取類檔案的位元組碼
4)呼叫父類的 defineClass 方法來載入類
5)使用者呼叫該類載入器的 loadClass 方法