1. 程式人生 > >Java 基礎知識整理 (待整理)

Java 基礎知識整理 (待整理)

ioc容器 invoke string spa 額外 器) cas 描述符 如果

JVM之類加載器(ClassLoader)基本介紹

類加載器用於將class文件加載到JVM中去執行。下面介紹類加載器涉及到的基本概念和加載基本過程。

一、Java虛擬機與程序的生命周期

在運行Java程序時,會啟動JVM進程,該進程中會使用一個線程去執行我們的Java程序。在如下幾種情況下,Java虛擬機將結束生命周期:

1.執行了System.exit(0)(內部調用了Runtime.getRuntime().exit(n)),如果是非0參數表示異常退出。

2.程序正常結束

3.程序在執行過程中遇到了異常或錯誤而導致異常終止。如果異常沒有try-catch而一直throw,那麽一直throw到main方法,

main方法還不處理的化那就會throw給JVM,JVM看到異常就會結束運行。

4.由於操作系統錯誤而導致的Java虛擬機進程終止

二、類的加載、連接與初始化

加載:查找並加載類的二進制數據到JVM

連接:分為三個步驟

? 驗證:確保被加載類的正確性

? 準備:為類的靜態變量分配內存,並將其初始化為默認值

? 解析:把類中的符號引用轉換為直接引用

初始化:為類的靜態變量賦予正確的初始值。(就是在類中定義變量時賦予的默認值)

以上的過程中並不會初始化對象變量,因為此時並不存在任何對象,只是將類的class文件數據加載到內存中,對象的生成是在該過程完成之後。

類中的靜態代碼塊是按照聲明的順序執行的,例如:

static int a;

static {

a=1;

}

static{

a=2;

}

此時a的最終值是2,如果兩個代碼塊顛倒順序,那麽a的最終值就是1。

三、類的主動使用與被動使用

Java程序中對類的使用可以分為兩種:主動使用 和 被動使用。

1Java程序對類的主動使用的情況

> 創建類的實例

> 訪問某個類或接口的靜態變量,或者對該靜態變量賦值

> 調用類的靜態方法

> 通過反射訪問類,例如Class.forName(“package.ClassName”)

> 初始化一個類的子類,或訪問子類的靜態變量或靜態方法也是對父類的主動使用。

> Java虛擬機啟動時被標註為啟動類的類,例如JUnit中的TestCase類。

所有的Java虛擬機實現必須在每個類或接口被Java程序首次主動使用時才初始化它們。因此Java虛擬機只有在一個類第一次

發生上面的六種情況時才會初始化該類,僅第一次主動使用時初始化。

除了以上的主動使用的情況,其它的情況都是對類的被動使用,因此都不會導致類的初始化。

類的加載、連接與初始化過程的詳細分析(上)

一、類加載階段

1.類加載方式

類的加載指的是將類的.class文件的二進制數據讀入內存中,將其放在運行時數據區的方法區內。然後在堆區創建一個Java.lang.Class對象,

用來封裝類在方法區內的數據結構,該對象是由JVM在加載類時創建的。所以每個類都會對應一個Class類型的對象,通過getClass()來獲取,

並且無論生成該類的多個少對象,其Class類型的對象只有一個。Class類的構造方法是私有的,並且Class類的對象只有JVM才能創建,創建時機為加載.class文件時。

因此Class類是整個反射的入口,因為每個類都會在內存中對應一個描述它的Class類型的對象,使用這個對象就可以獲取到目標類所關聯的class文件中的數據結構。



類的加載有以下幾種方式(加載.class文件的方式)

? 從本地文件系統直接加載

? 通過網絡下載.class文件(java.NET.URLClassLoader)

? 從zip、jar等歸檔文件中加載 .class文件

? 從專有的數據庫中提取 .class文件

? 將Java源文件動態編譯為.class文件



類加載的最終產物就是位於堆區中的Class對象(註意此時並沒有被加載類的對象存在,剛剛加載類)。Class對象封裝了類在方法區中的數據結構,

並且向Java程序員提供了訪問方法區內的數據結構的接口,這些接口就是Java反射的相關類和方法。

1類加載器

有兩種類型的類加載器

1).Java虛擬機自帶的類加載器,其中包括以下三種類加載器

? 根類加載器(Bootstrap ClassLoader)

? 擴展類加載器(Extension ClassLoader)

? 系統類加載器(System ClassLoader),又稱為應用類加載器

其中第一種類加載器是JVM最底層的類加載器,由C++編寫,因此我們無法訪問到根類加載器。後面兩種其實是基於第一種的,由Java語言實現的類加載器。

2).用戶自定義的類加載器,可以定義加載的方式,加載時機以及加載過程中做一些事情。

使用java.lang.ClassLoader的子類,通過ClassLoader來實現自定義的類加載器,這個過程中可以定義類的加載方式,加載時機以及加載過程中做一些事情。

通過給定ClassLoader一個類的名稱,它會將其作為一個文件名稱試圖去讀取該文件內容並根據內容組裝一個類的描述。每個類對象其實都包含了一個對定義它的

那個ClassLoader的一個引用,因為任何類都是由類加載器加載的,因此通過該類就可以訪問到對應的類加載器。

通過類對象的getClass().getClassLoader()或類的class屬性的getClassLoader()就可以獲取到類所對應的類加載器,也就是加載該類的哪個類加載器。

getClassLoader()可能返回一個null,那麽就代表該類的類加載器是根類加載器(Bootstrap ClassLoader);換句話說,如果一個類是有根類加載器加載的,

那麽就無法獲取到該類加載器,此時getClassLoader()返回null,因為根類加載器是使用C++編寫的,我們無法在程序中訪問它。例如String等類就是由根類

加載器加載的,因此String.class.getClassLoader()將返回null。

對於JDK內置的類一般都是由根類加載器加載的,因此通過它們調用getClassLoader()返回null;自定義的類一般通過

sun.misc.Launcher$AppClassLoader加載,通過輸出getClassLoader()結果即可看到。

對於JDK動態代理的InvocationHandler類的invoke方法,第一個參數是一個ClassLoader類型,就是用於動態的加載第二個參數所傳遞的類,

然後會根據所加載的類動態的創建出所加載類的對象,然後根據該對象創建出一個該對象的代理對象。

3.類加載時機

類加載器並不需要等到某個類被主動使用時才加載它。這與類的初始化不同,上面說過JVM必須在每個類或接口被Java程序首次主動使用時才初始化它們,

註意加載與初始化的卻別!

JVM規範允許類加載器在預料到某個類將要被使用時就預先加載它,如果在預先加載的過程中遇到了.class文件不存在或有錯誤,此時並不會報錯,

而是類加載器必須等到在程序首次使用該類時才報告錯誤,這種錯誤類型為LinkageError。因此如果這個類加載後一直沒有被主動使用,

那麽類加載器就一直不會報告錯誤。

類被加載後就進入了連接階段。連接就是將已經讀入到內存的類的二進制數據合並到虛擬機的運行時環境中。所謂的數據合並,因為編譯後的每個class文件

在硬盤中都是獨立的,但是每個class之間可能存在引用關系以及方法之間存在調用關系,此時就需要根據它們之間的關系將這些class數據合並在一起放入運行時環境中。

類的加載、連接與初始化過程的詳細分析(中)

1.類的驗證

類驗證除了包裝類的可用,還為了包裝安全性,防止構件出自定義的類來侵入系統。

類驗證所要完成的功能:

? 類文件結構的檢查

確保類文件遵從Java類文件的固定格式

? 語義檢查

確保類本身符合Java語言的語法規定,比如驗證final類型的類有無子類,以及final類型的方法是否被覆蓋或重寫。

? 字節碼驗證

確保字節碼流可以被Java虛擬機安全地執行。字節碼流代表Java方法(包括靜態和實例方法),它是由被稱作操作碼的單字節指令組成的序列,

每個操作碼後都跟著一個或多個操作數。字節碼驗證步驟會檢查每個操作碼是否合法,即是否有著合法的操作數。

? 二進制兼容性的驗證

確保相互引用的類之間協調一致。例如在Worker類的gotoWork()方法中會調用Car類的run()方法,此Java虛擬機驗證驗證Worker類的時候會

檢查方法區內是否存在Car類的run()方法,如果不存(當Worker類和Car類的版本不兼容就會出現該這個問題,例如低版本JRE編譯class的到高版本JRE中運行)

在就會拋出NoSuchMethodError錯誤。

2. 類連接之準備階段

在準備階段,Java虛擬機為類的靜態變量分配內存空間並設置默認的初始值,註意不是程序中=賦值的哪個值,而是Java對象變量的默認值。

例如,對於以下的Simple類,在準備階段為int類型的靜態變量a分配4個字節的內存空間,並賦予默認值0;為long類型的變量b分配8個字節的內存空間,並賦予默認值0

public class Simple {

private static int a=1;

private static long b;

static { b=2;}

}

3.類連接之解析階段

在解析階段,Java虛擬機會把類的二進制數據中的符號引用替換為直接引用。例如Worker類中gotoWork()方法中會引用Car類的run()方法:

public void gotoWork(){

car.run(); //這段代碼在Worker類的二進制數據中表示為符號引用

}

在Worker類的二進制數據中,包含了一個對Car類的run()方法的符號引用,它由run()方法的全名和相關描述符組成。在解析階段,Java虛擬機會

把這個符號引用替換為一個指針,該指針指向Car類的run()方法在方法區內的內存位置,這個指針就是直接引用。

類的加載、連接與初始化過程的詳細分析(下)

1.類的初始化時機

類連接階段的解析步驟完成後就進入了類的初始化階段,並且只有主動使用類時才會執行初始化。在初始化階段,Java虛擬機執行類的初始化語句,

為類的靜態變量賦予初始值(程序中使用賦值語句所賦予的值)。

在程序中,靜態變量的初始化有兩種途徑:

> 在靜態變量的聲明處進行初始化

> 在靜態代碼塊中進行初始化,靜態代碼塊也是在類加載後的這個初始化階段被執行的。

例如下面的代碼中,變量a、b被顯式初始化,而變量c沒有顯式初始化,其保持默認值0

public class Sample {

private static int a=1; //聲明時初始化靜態變量

private static int b;

private static int c;

static {

b=2; //在靜態代碼塊中初始化靜態變量

}

}

2.類的初始化步驟:

(1) 如果類沒有被加載和連接,那麽就先執行加載和連接過程

(2) 如果類存在父類並且父類沒有被初始化,那麽就先初始化父類,一直初始化類繼承結構到Object

(3) 如果類存在初始化語句,例如賦值語句或靜態代碼塊,那麽就執行這些初始化語句。

3.接口的初始化時機

當JVM初始化一個類時,要求它的所有父類都已經被初始化,但是這個規則不適用於接口。

(1) 在初始化一個類時,並不會先初始化它所實現的接口;

(2) 初始化一個接口時,並不會先初始化它所繼承的父接口

因此,一個父接口並不會因為它的子接口或實現類的初始化而被初始化。只有當程序首次使用特定接口的靜態變量時,才會導致該接口的初始化。

只有當程序訪問的靜態變量或靜態方法確實在當前類或接口中定義時,才會認為是對類或接口的主動使用

調用ClassLoader的loadClass方法加載一個類,並不設置對類主動使用的六種情況,因此也不會初始化。

Class.forName和ClassLoader.loadClass區別

Java中class是如何加載到JVM中的:
1.class加載到JVM中有三個步驟
裝載:(loading)找到class對應的字節碼文件。
連接:(linking)將對應的字節碼文件讀入到JVM中。
初始化:(initializing)對class做相應的初始化動作。
2.Java中兩種加載class到JVM中的方式
2.1:Class.forName("className");
其實這種方法調運的是:Class.forName(className, true, ClassLoader.getCallerClassLoader())方法
參數一:className,需要加載的類的名稱。
參數二:true,是否對class進行初始化(需要initialize)
參數三:classLoader,對應的類加載器

2.2:ClassLoader.laodClass("className");
其實這種方法調運的是:ClassLoader.loadClass(name, false)方法
參數一:name,需要加載的類的名稱
參數二:false,這個類加載以後是否需要去連接(不需要linking)

2.3:兩種方式的區別
forName("")得到的class是已經初始化完成的
loadClass("")得到的class是還沒有連接的
一般情況下,這兩個方法效果一樣,都能裝載Class。
但如果程序依賴於Class是否被初始化,就必須用Class.forName(name)了。

3.舉例說明他們各自的使用方法
java使用JDBC連接數據庫時候,我們首先需要加載數據庫驅動。
Class.forName("com.MySQL.jdbc.Driver");//通過這種方式將驅動註冊到驅動管理器上
Connection conn = DriverManager.getConnection("url","userName","password");//通過驅動管理器獲得相應的連接
查看com.mysql.jdbc.Driver源碼:

public class Driver extends NonRegisteringDriver
implements java.sql.Driver
{
//註意,這裏有一個static的代碼塊,這個代碼塊將會在class初始化的時候執行
static
{
try
{

//將這個驅動Driver註冊到驅動管理器上
DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can‘t register driver!");
}
}
}

Class.forName("com.mysql.jdbc.Driver")方法以後,他會進行class的初始化,執行static代碼塊。
也就是說class初始化以後,就會將驅註冊到DriverManageer上,之後才能通過DriverManager去獲取相應的連接。
但是要是我們使用ClassLoader.loadClass(com.mysql.jdbc.Driver)的話,不會link,更也不會初始化class。
相應的就不會回將Driver註冊到DriverManager上面,所以肯定不能通過DriverManager獲取相應的連接。

Spring容器啟動過程

一、一切從手動啟動IoC容器開始

  1. ClassPathResource resource = new ClassPathResource("bean.xml");
  2. DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
  3. XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
  4. reader.loadBeanDefinitions(resource);

第一行代碼:ClassPathResource的作用是起到了資源定位的作用。通常情況下,spring的配置信息使用文件來描述,通過這樣一行代碼,指明了需要加載的資源的位置,並且使用Spring能夠理解的Resource接口的形式將資源描述出來。

第二行代碼:DefaultListableBeanFactory是一個純粹的IoC容器類,它是這個Spring的一個基礎的IoC容器類,其他的一個IoC容器都是以這個類為基礎進行擴展的。這樣代碼只是定義了一個IoC容器,它不具有任何其他的能力。

第三行代碼:XmlBeanDefinitionReader是一個配置文件讀取器。它實現了BeanDefinitionReader接口,它能夠按照Spring配置文件,將其中的配置信息轉換為spring內部可以識別的信息(BeanDefinition)。註意,在這裏它的構造函數需要一個BeanDefinitionRegistry類型的參數,BeanDefinitionRegistry接口提供了一個回調函數,通過這個回調函數可以向IoC容器註冊bean的定義信息。DefaultListableBeanFactory實現了這個接口。

第四行代碼:調用loadeBeanDefinitions方法,通過給定的Resource資源,從中讀取出spring的配置信息,轉換為BeanDefinition,然後再調用BeanDefinitionRegistry的回調函數進行註冊。

通過以上的四行代碼,完成了spring容器的啟動。

二、容器啟動過程

1. 定位

在spring中,使用統一的資源表現方式Resource。根據不同的情況進行不同的選擇。上述程序中,采用了編程式的資源定位方法,使用ClassPathResource定位位於classpath下的資源文件。

2. 加載

在加載這個過程中,主要工作是讀取spring配置文件,解析配置文件中的內容,將這些信息轉換成為Spring內容可以理解、使用的BeanDefinition。

3. 註冊

加載過配置文件後,就將BeanDefinition信息註冊到BeanDefinitionRegistry中,通常情況下Spring容器的實現類都實現這個接口。

SprinMVC工作流程描述

1. 用戶向服務器發送請求,請求被Spring 前端控制Servelt DispatcherServlet捕獲;

2. DispatcherServlet對請求URL進行解析,得到請求資源標識符(URI)。然後根據該URI,調用HandlerMapping獲得該Handler配置的所有相關的對象(包括Handler對象以及Handler對象對應的攔截器),最後以HandlerExecutionChain對象的形式返回;

3. DispatcherServlet 根據獲得的Handler,選擇一個合適的HandlerAdapter。(附註:如果成功獲得HandlerAdapter後,此時將開始執行攔截器的preHandler(...)方法)

4. 提取Request中的模型數據,填充Handler入參,開始執行Handler(Controller)。 在填充Handler的入參過程中,根據你的配置,Spring將幫你做一些額外的工作:

HttpMessageConveter: 將請求消息(如Json、xml等數據)轉換成一個對象,將對象轉換為指定的響應信息

數據轉換:對請求消息進行數據轉換。如String轉換成Integer、Double等

數據根式化:對請求消息進行數據格式化。 如將字符串轉換成格式化數字或格式化日期等

數據驗證: 驗證數據的有效性(長度、格式等),驗證結果存儲到BindingResult或Error中

5. Handler執行完成後,向DispatcherServlet 返回一個ModelAndView對象;

6. 根據返回的ModelAndView,選擇一個適合的ViewResolver(必須是已經註冊到Spring容器中的ViewResolver)返回給DispatcherServlet ;

7. ViewResolver 結合Model和View,來渲染視圖

8. 將渲染結果返回給客戶端。

技術分享圖片

Spring工作流程描述

為什麽Spring只使用一個Servlet(DispatcherServlet)來處理所有請求?

詳細見J2EE設計模式-前端控制模式

Spring為什麽要結合使用HandlerMapping以及HandlerAdapter來處理Handler?

符合面向對象中的單一職責原則,代碼架構清晰,便於維護,最重要的是代碼可復用性高。如HandlerAdapter可能會被用於處理多種Handler。

ContextLoaderListener與 DispatcherContext 關系

DispatcherContext加載的時候會以ContextLoaderListener加載的spring context容器作為parent context容器,

技術分享圖片

問題?

web初始化

Java 基礎知識整理 (待整理)