1. 程式人生 > 其它 >MNIST手寫數字集的多分類問題(Convolution Neural Network)

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 方法