//原文:http://blog.csdn.net/scythe666/article/details/51841161
一、JVM
1、內存模型
1.1.1 內存分幾部分
(1)程序計數器
可看作當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
在線程創建時創建。執行本地方法時,PC的值爲null。爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,線程私有。
(2)Java虛擬機棧
線程私有,生命週期同線程。每個方法在執行同時,創建棧幀。用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。棧中的局部變量表主要存放一些基本類型的變量(int, short, long, byte, float,double, boolean, char)和對象句柄。
棧中有局部變量表,包含參數和局部變量。
此外,java中沒有寄存器,因此所有的參數傳遞依靠操作數棧。
棧上分配,小對象(一般幾十個bytes),在沒有逃逸的情況下,可以直接分配在棧上。(沒有逃逸是指,對象只能給當前線程使用,如果多個線程都要用,則不可以,因爲棧是線程私有的。)直接分配在棧上,可以自動回收,減輕GC壓力。因爲棧本身比較小,大對象也不可以分配,會影響性能。
-XX:+DoEscapeAnalysis 啓用逃逸分析,若非逃逸則可棧上分配。
(3)本地方法棧
線程私有,與Java虛擬機棧非常相似,區別不過是虛擬機棧爲虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的 Native 方法(非java語言實現,比如C)服務。Hotspot 直接把本地方法棧和虛擬機棧合二爲一。
棧&本地方法棧:線程創建時產生,方法執行是生成棧幀。
(4)Java堆
線程共有(可能劃分出多個線程私有的分配緩衝區,Thread Local Allow),Java虛擬機管理內存中最大的一塊,此區域唯一目的就是存放對象實例,幾乎所有對象實例在此區分配,線程共享內存。可細分爲新生代和老年代,方便GC。主流虛擬機都是按可擴展實現(通過-Xmx 和 -Xms 控制)。
注意:Java堆是Java代碼可及的內存,是留給開發人員使用的;非堆(Non-Heap)就是JVM留給 自己用的,所以方法區、JVM內部處理或優化所需的內存(如JIT編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼都在非堆內存中。
關於TLAB
Sun Hotspot JVM爲了提升對象內存分配的效率,對於所創建的線程都會分配一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據運行的情況計算而得,在TLAB上分配對象時不需要加鎖,因此JVM在給線程的對象分配內存時會盡量的在TLAB上分配,在這種情況下JVM中分配對象內存的性能和C基本是一樣高效的,但如果對象過大的話則仍然是直接使用堆空間分配
TLAB僅作用於新生代的Eden Space,因此在編寫Java程序時,通常多個小的對象比大的對象分配起來更加高效。詳見:http://www.cnblogs.com/sunada2005/p/3577799.html
Java堆:在虛擬機啓動時創建
(5)方法區
線程共有,用於存儲已被虛擬機加載的類信息、常量池、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,但它卻有一個別名Non-Heap(非堆),目的是與Java堆區分開。
注意,通常和永久區(Perm)關聯在一起。但也不一定,JDK6時,String等常量信息保存於方法區,JDK7時,移動到了堆。永久代和方法區不是一個概念,但是有的虛擬機用永久代來實現方法區,可以用永久代GC來管理方法區,省去專門寫的功夫。
(6)運行時常量池
方法區的一部分,存放編譯期生成的各種字面量和符號引用。
(7)直接內存
並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,也可能導致 OOM 異常(內存區域綜合>物理內存時)。NIO類,可以使用Native 函數庫直接分配堆外內存,然後通過一個存儲在Java 堆裏面的 DirectByteBuffer 對象作爲這塊內存的引用進行操作。
類加載時 方法信息保存在一塊稱爲方法區的內存中, 並不隨你創建對象而隨對象保存於堆中。可參考《深入java虛擬機》前幾章。
另參考(他人文章):
如果instance method也隨着instance增加而增加的話,那內存消耗也太大了,爲了做到共用一小段內存,Java 是根據this關鍵字做到的,比如:instance1.instanceMethod(); instance2.instanceMethod(); 在傳遞給對象參數的時候,Java 編譯器自動先加上了一個this參數,它表示傳遞的是這個對象引用,雖然他們兩個對象共用一個方法,但是他們的方法中所產生的數據是私有的,這是因爲參數被傳進來變成call stack內的entry,而各個對象都有不同call
stack,所以不會混淆。其實調用每個非static方法時,Java 編譯器都會自動的先加上當前調用此方法對象的參數,有時候在一個方法調用另一個方法,這時可以不用在前面加上this的,因爲要傳遞的對象參數就是當前執行這個方法的對象。
詳見:http://blog.csdn.net/scythe666/article/details/51700142
1.1.2 堆溢出、棧溢出原因及實例,線上如何排查
(1)棧溢出
遞歸,容易引起棧溢出stackoverflow;因爲方法循環調用,方法調用會不斷創建棧幀。
造成棧溢出的幾種情況:
1)遞歸過深
2)數組、List、map數據過大
3 ) 創建過多線程
對於Java虛擬機棧和本地方法棧,Java虛擬機規範規定了兩種異常狀況:
① 線程請求深度>虛擬機所允許的深度,將拋出StackOverFlowError(SOF)異常;
② 如果虛擬機可動態擴展,且擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError(OOM)異常。
(2)堆溢出
如果在堆中沒有內存完成實例分配,且堆無法擴展時,將拋出OOM異常。
在方法區也會拋出 OOM 異常。
實例
可使用以下代碼造成堆棧溢出:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
如下代碼會造成OOM堆溢出:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
另外,Java虛擬機的堆大小如何設置:命令行
java –Xms128m //JVM佔用最小內存
–Xmx512m //JVM佔用最大內存
–XX:PermSize=64m //最小堆大小
–XX:MaxPermSize=128m //最大堆大小
2、類加載機制
基本上所有的類加載器都是 java.lang.ClassLoader類的一個實例。下面詳細介紹這個 Java 類。
1.2.1 java.lang.ClassLoader類介紹
java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,然後從這些字節代碼中定義出一個 Java 類,即 java.lang.Class類的一個實例。除此之外,ClassLoader還負責加載 Java 應用所需的資源,如圖像文件和配置文件等。
1.2.2 類加載器的樹狀組織結構
Java 中的類加載器大致可以分成兩類:一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。系統提供的類加載器主要有下面三個:
(1)引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader。
BootStrapClassLoader
負責jdk_home/jre/lib目錄下的核心 api或 -Xbootclasspath選項指定的jar包加載進來。
(2)擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
ExtClassLoader
負責jdk_home/jre/lib/ext目錄下的jar包或 -Djava.ext.dirs指定目錄下的jar包加載進來。
(3)系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。
AppClassLoader
負責java -classpath/-Djava.class.path所指的目錄下的類與jar包加載進來,System.getClassLoader獲取到的就是這個類加載器。
除了系統提供的類加載器以外,開發人員可以通過繼承 java.lang.ClassLoader類的方式實現自己的類加載器,以滿足一些特殊的需求。
除了引導類加載器之外,所有的類加載器都有一個父類加載器。getParent()方法可以得到。對於系統提供的類加載器來說,系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器;對於開發人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。因爲類加載器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類加載器。
1.2.3 雙親委派模型
類加載器在嘗試自己去查找某個類的字節代碼並定義它時,會先代理給其父類加載器,由父類加載器先去嘗試加載這個類,依次類推。
在介紹代理模式背後的動機之前,首先需要說明一下 Java 虛擬機是如何判定兩個 Java 類是相同的。Java 虛擬機不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認爲兩個類是相同的。即便是同樣的字節代碼,被不同的類加載器加載之後所得到的類,也是不同的。比如一個 Java 類 com.example.Sample,編譯之後生成了字節代碼文件 Sample.class。兩個不同的類加載器 ClassLoaderA和 ClassLoaderB分別讀取了這個 Sample.class文件,並定義出兩個 java.lang.Class類的實例來表示這個類。這兩個實例是不相同的。對於 Java 虛擬機來說,它們是不同的類。試圖對這兩個類的對象進行相互賦值,會拋出運行時異常 ClassCastException。
所以纔有雙親委派模型,這樣的話,可保證加載的類(特別是Object和String這類基礎類)是同一個。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
輸出:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
這兩個類並不是一個String類,要包名類名+loader一致是不可能的,所以雙親委派模型從外界無法破壞。
注意:這裏有
- 若加載的類能被系統加載器加載到(Sample類在classpath下),則無異常。因爲defining class loader都是AppClassLoader
- 若加載的類不能被系統加載器加載到,則拋異常。此時的 defining class loader 纔是自定義的 FileSystemClassLoader
1.2.4 defining loader 和 initiating loader
前面提到過類加載器會首先代理給其它類加載器來嘗試加載某個類。這就意味着真正完成類的加載工作的類加載器和啓動這個加載過程的類加載器,有可能不是同一個。真正完成類的加載工作是通過調用 defineClass來實現的;而啓動類的加載過程是通過調用 loadClass來實現的。前者稱爲一個類的定義加載器(defining loader),後者稱爲初始加載器(initiating loader)。在 Java 虛擬機判斷兩個類是否相同的時候,使用的是類的定義加載器。也就是說,哪個類加載器啓動類的加載過程並不重要,重要的是最終定義這個類的加載器。兩種類加載器的關聯之處在於:一個類的定義加載器是它引用的其它類的初始加載器。如類 com.example.Outer引用了類 com.example.Inner,則由類 com.example.Outer的定義加載器負責啓動類 com.example.Inner的加載過程。
方法 loadClass()拋出的是 java.lang.ClassNotFoundException異常;方法 defineClass()拋出的是 java.lang.NoClassDefFoundError異常。
類加載器在成功加載某個類之後,會把得到的 java.lang.Class類的實例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,對於一個類加載器實例來說,相同全名的類只加載一次,即 loadClass方法不會被重複調用。
1.2.5 Class.forName 加載
Class.forName是一個靜態方法,同樣可以用來加載類。該方法有兩種形式:
- 1
- 1
和
- 1
- 1
第一種形式的參數 name表示的是類的全名;initialize表示是否初始化類;loader表示加載時使用的類加載器。
第二種形式則相當於設置了參數 initialize的值爲 true,loader的值爲當前類的類加載器。Class.forName的一個很常見的用法是在加載數據庫驅動的時候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()
用來加載
Apache Derby 數據庫的驅動。
詳見:http://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html
1.2.6 類加載過程
從類被加載到虛擬機內存中開始,到卸載出內存爲止,類的生命週期包括加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。
http://www.open-open.com/lib/view/open1352161045813.html
其中加載(除了自定義加載)+鏈接的過程是完全由jvm負責的,什麼時候要對類進行初始化工作(加載+鏈接在此之前已經完成了),jvm有嚴格的規定(四種情況):
1.遇到new,getstatic,putstatic,invokestatic這4條字節碼指令時,加入類還沒進行初始化,則馬上對其進行初始化工作。其實就是3種情況:用new實例化一個類時、讀取或者設置類的靜態字段時(不包括被final修飾的靜態字段,因爲他們已經被塞進常量池了)、以及執行靜態方法的時候。
2.使用java.lang.reflect.*的方法對類進行反射調用的時候,如果類還沒有進行過初始化,馬上對其進行。
3.初始化一個類的時候,如果他的父親還沒有被初始化,則先去初始化其父親。
4.當jvm啓動時,用戶需要指定一個要執行的主類(包含static void main(String[] args)的那個類),則jvm會先去初始化這個類。
以上4種預處理稱爲對一個類進行主動的引用,其餘的其他情況,稱爲被動引用,都不會觸發類的初始化。
加載:
在加載階段,虛擬機主要完成三件事:
1.通過一個類的全限定名來獲取定義此類的二進制字節流。
2.將這個字節流所代表的靜態存儲結構轉化爲方法區域的運行時數據結構。
3.在Java堆中生成一個代表這個類的java.lang.Class對象,作爲方法區域數據的訪問入口。
驗證:
驗證階段作用是保證Class文件的字節流包含的信息符合JVM規範,不會給JVM造成危害。如果驗證失敗,就會拋出一個java.lang.VerifyError異常或其子類異常。驗證過程分爲四個階段:
1.文件格式驗證:驗證字節流文件是否符合Class文件格式的規範,並且能被當前虛擬機正確的處理。
2.元數據驗證:是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言的規範。
3.字節碼驗證:主要是進行數據流和控制流的分析,保證被校驗類的方法在運行時不會危害虛擬機。
4.符號引用驗證:符號引用驗證發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在解析階段中發生。
準備:
準備階段爲變量分配內存並設置類變量的初始化。在這個階段分配的僅爲類的變量(static修飾的變量),而不包括類的實例變量,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。對非final的變量,JVM會將其設置成“零值”,而不是其賦值語句的值:
- 1
- 1
那麼在這個階段,size的值爲0,而不是12。 final修飾的類變量將會賦值成真實的值。
解析:
解析過程是將常量池內的符號引用替換成直接引用。主要包括四種類型引用的解析。類或接口的解析、字段解析、方法解析、接口方法解析。
初始化:
在準備階段,類變量已經經過一次初始化了,在這個階段,則是根據程序員通過程序制定的計劃去初始化類的變量和其他資源。這些資源有static{}塊,構造函數,父類的初始化等。
至於使用和卸載階段階段,這裏不再過多說明,使用過程就是根據程序定義的行爲執行,卸載由GC完成
3、垃圾回收 GC
1.3.1 引用計數法
目前主流的虛擬機都沒有使用引用計數法,主要原因就是它很難解決對象之間互相循環引用的問題。
1.3.2 可達性分析算法
思想:
通過一系列稱爲 GC Roots 的對象作爲起始點,從這些點開始向下搜索,搜索走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈連接(用圖論的話來說,就是從GC Roots到這個對象不可達),證明此對象不可用。
Java語言中,可作爲GC Roots的對象包括:
(1)虛擬機棧(棧幀中的本地變量表)中引用的對象
(2)方法區中類靜態屬性引用的對象
(3)方法區中常量引用的對象
(4)本地方法棧中JNI ( 即一般說的Native方法)引用的對象
1.3.3 再談引用
在JDK 1.2之後 ,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference )、軟引用(Soft Reference )、弱引用(Weak Reference )、虛引用(Phantom Reference) 4種 , 引用強度依次逐漸減弱。
強引用
指在程序代碼之中普遍存在的,類似“Object obj=new Object ( ) ”這類的引用 ,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
軟引用
用來描述一些還有用但並非必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。在JDK 1.2之後,提供了SoftReference類來實現軟引用。
弱引用
也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。在JDK1.2之後,提供了PhantomReference類來實現虛引用。
虛引用
也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK1.2之後,提供了PhantomReference類來實現虛引用。
1.3.4 對象回收過程
即使在可達性分析算法中不可達的對象,也並非是“非死不可”的 ,這時候它們暫時處於“緩刑” 階段 ,要真正宣告一個對象死亡 ,至少要經歷兩次標記過程
如果這個對象被判定爲有必要執行finalize() 方法,那麼這個對象將會放置在一個叫做 F-Queue的隊列之中,並在稍後由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。
1.3.5 對於方法區(Hotspot虛擬機的永久代)的回收
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是“無用的類”:
(1)該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例
(2)加載該類的ClassLoader已經被回收
(3)該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
1.3.6 垃圾收集算法
1.3.6.1 標記-清除算法
望名生意,算法分爲“標記”和“清除”兩個階段:
首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象,它的標記過程如前
它的主要不足有兩個:
(1)效率問題,標記和清除兩個過程的效率都不高;
(2)空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
1.3.6.2 複製算法
將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。
適用於對象存活率低的場景(新生代)
這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針 ,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲了原來的一半,未免太高了一點。
將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間 ,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次地複製到另外一塊Survivor空間上,最 後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用內存空間爲整個新生代容量的90% ( 80%+10% ) ,只有10% 的內存會被 “浪費”。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多於10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這裏指老年代)進行分配擔保( Handle Promotion ) 。
1.3.6.3 標記-整理算法
適用於對象存活率高的場景(老年代)
複製收集算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是 ,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
標記過程類似“標記-清除”算法,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存,類似於磁盤整理的過程
總的分類如下圖:
1.3.7 內存申請過程
內存由Perm和Heap組成。其中Heap = {Old + NEW = { Eden , from, to } }。perm用來存放常量等。
heap中分爲年輕代(young)和年老代(old)。年輕代又分爲Eden,Survivor(倖存區)。Survivor又分爲from,to,也可以不只是這兩塊,切from和to沒有先後順序。其中,old和young區比例可手動分配。
當OLD區空間不夠時,JVM會在OLD區進行完全的垃圾收集。完全垃圾收集後,若Survivor及OLD區仍然無法存放從Eden複製過來的部分對象,導致JVM無法在Eden區爲新對象創建內存區域,則出現”out of memory”Error。
好文請見:http://blog.csdn.net/scythe666/article/details/51852938
4、JVM啓動過程
JVM工作原理和特點主要是指操作系統裝入JVM是通過jdk中Java.exe來完成,通過下面4步來完成JVM環境.
1.創建JVM裝載環境和配置
2.裝載JVM.dll
3.初始化JVM.dll並掛界到JNIENV(JNI調用接口)實例
4.調用JNIEnv實例裝載並處理class類。
詳見:http://blog.csdn.net/ning109314/article/details/10411495
5、Class文件結構
Class文件的總體結構如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
1.5.1 文件描述
(1)magic位、class文件版本號。Magic位很容易記住,數值是0xCAFEBABE。
(2)常量池
存儲一組常量,供class文件中其它元素引用。常量池中順序存儲着一組常量,常量在池中的位置稱爲索引。Class文件中其它結構通過索引來引用常量。常量最主要是被指令引用。編譯器將源碼編譯成指令和常量,圖形表示如下:
(3)類概述
存儲了當前類的總體信息,包括當前類名、所繼承的父類、所實現的接口。
(4)字段表
存儲了一組字段結構,類中每個字段對應一個字段結構。
字段結構存儲了字段的信息,包括字段名、字段修飾符、字段指向的類型等。
(5)方法表
存儲了一組方法結構,類中每個方法對應一個方法結構。
方法結構比較複雜,它內部最重要的結構是Code結構。每個非抽象方法的方法結構下有一個Code結構,存儲了方法的字節碼。
(6)擴展信息表
存儲了類級別的可選信息,例如類級別的annotation。(方法、字段級別的annotation分別存儲在方法結構、字段結構中)
1.5.2 棧結構
我們對於站結構的內部構造,大部分則瞭解甚少。字節碼的執行依賴棧結構,理解棧結構是理解字節碼的基礎。
棧由幀組成,一個幀對應一個方法調用。一個方法被調用時,一個幀被創建,方法返回時,對應的幀被銷燬。
幀存儲了方法執行期間的數據,包括變量數據和計算的中間結果。幀由兩部分組成,變量表和操作棧。這兩個結構是字節碼執行期間直接依賴的兩個結構
操作棧
顧名思義,操作棧是一個棧結構,即LIFO結構。操作棧位於幀內部,用於存儲方法執行期間的中間結果。操作棧在JVM中的角色,類似於寄存器在實體機中的角色。
字節碼中絕大多數指令,都是圍繞着操作棧執行的。它們或是從其他地方讀數據,壓入操作棧;或是從操作棧彈數據進行處理;還有的先彈數據,再處理,最會將結果壓入操作。
在JVM中,要對數據進行處理,首先要把數據讀進操作棧。
int變量求和
要對兩個int變量求和,我們先通過iload指令量兩個變量壓入操作棧,然後執行iadd指令。iadd從操作棧彈出兩個int值,求和,然後將結果壓入操作棧。
調用方法對象
調用對象方法時,我們需要將被調用對象,調用參數依次壓入操作棧,然後執行invokevirtual指令。該指令從操作棧彈出調用參數,被調用對象,執行方法調用。
變量表
變量表用於存儲變量數據。
變量表由一組槽組成。一個槽能夠存儲一個除long、double外其他類型的數據。兩個槽能夠存儲一個long型或double型數據。變量所在的槽在變量表中位置稱爲變量索引,對於long和double類型,變量索引是第一個槽的位置。
變量在表量表中的順序是:
this、方法參數(從左向右)、其它變量
如果是static方法,則this沒有。
示例:
有如下方法:
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
其對應的變量表爲:
二、Java基礎
1、什麼是接口?什麼是抽象類?區別是什麼?
2.1.1 接口
在軟件工程中,接口泛指供別人調用的方法或者函數。從這裏,我們可以體會到Java語言設計者的初衷,它是對行爲的抽象。
接口中可以含有 變量和方法。但是要注意,接口中的變量會被隱式地指定爲public static final變量(並且只能是public static final變量,用private修飾會報編譯錯誤),而方法會被隱式地指定爲public abstract方法且只能是public abstract方法(用其他關鍵字,比如private、protected、static、 final等修飾會報編譯錯誤),並且接口中所有的方法不能有具體的實現,也就是說,接口中的方法必須都是抽象方法。從這裏可以隱約看出接口和抽象類的區別,接口是一種極度抽象的類型,它比抽象類更加“抽象”,並且一般情況下不在接口中定義變量。
可以看出,允許一個類遵循多個特定的接口。如果一個非抽象類遵循了某個接口,就必須實現該接口中的所有方法。對於遵循某個接口的抽象類,可以不實現該接口中的抽象方法。
2.1.2 抽象類
抽象方法是一種特殊的方法:它只有聲明,而沒有具體的實現。抽象方法的聲明格式爲:
- 1
- 1
抽象方法必須用abstract關鍵字進行修飾。如果一個類含有抽象方法,則稱這個類爲抽象類,抽象類必須在類前用abstract關鍵字修飾。因爲抽象類中含有無具體實現的方法,所以不能用抽象類創建對象。
下面要注意一個問題:在《JAVA編程思想》一書中,將抽象類定義爲“包含抽象方法的類”,但是後面發現如果一個類不包含抽象方法,只是用abstract修飾的話也是抽象類。也就是說抽象類不一定必須含有抽象方法。個人覺得這個屬於鑽牛角尖的問題吧,因爲如果一個抽象類不包含任何抽象方法,爲何還要設計爲抽象類?所以暫且記住這個概念吧,不必去深究爲什麼。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
從這裏可以看出,抽象類就是爲了繼承而存在的,如果你定義了一個抽象類,卻不去繼承它,那麼等於白白創建了這個抽象類,因爲你不能用它來做任何事情。對於一個父類,如果它的某個方法在父類中實現出來沒有任何意義,必須根據子類的實際需求來進行不同的實現,那麼就可以將這個方法聲明爲abstract方法,此時這個類也就成爲abstract類了。
包含抽象方法的類稱爲抽象類,但並不意味着抽象類中只能有抽象方法,它和普通類一樣,同樣可以擁有成員變量和普通的成員方法。注意,抽象類和普通類的主要有三點區別:
1)抽象方法必須爲public或者protected(因爲如果爲private,則不能被子類繼承,子類便無法實現該方法),缺省情況下默認爲public。
2)抽象類不能用來創建對象;
3)如果一個類繼承於一個抽象類,則子類必須實現父類的抽象方法。如果子類沒有實現父類的抽象方法,則必須將子類也定義爲爲abstract類。
在其他方面,抽象類和普通的類並沒有區別。
2.1.3 區別
2.1.3.1 語法層面上的區別
1)抽象類可以提供成員方法的實現細節,而接口中只能存在public abstract 方法;
2)抽象類中的成員變量可以是各種類型的,而接口中的成員變量只能是public static final類型的;
3)接口中不能含有靜態代碼塊以及靜態方法,而抽象類可以有靜態代碼塊和靜態方法;
4)一個類只能繼承一個抽象類,而一個類卻可以實現多個接口。
2.1.3.2 設計層面上的區別
1)抽象類是對一種事物的抽象,即對類抽象,而接口是對行爲的抽象。
抽象類是對整個類整體進行抽象,包括屬性、行爲,但是接口卻是對類局部(行爲)進行抽象。舉個簡單的例子,飛機和鳥是不同類的事物,但是它們都有一個共性,就是都會飛。那麼在設計的時候,可以將飛機設計爲一個類Airplane,將鳥設計爲一個類Bird,但是不能將 飛行 這個特性也設計爲類,因此它只是一個行爲特性,並不是對一類事物的抽象描述。此時可以將 飛行 設計爲一個接口Fly,包含方法fly( ),然後Airplane和Bird分別根據自己的需要實現Fly這個接口。然後至於有不同種類的飛機,比如戰鬥機、民用飛機等直接繼承Airplane即可,對於鳥也是類似的,不同種類的鳥直接繼承Bird類即可。從這裏可以看出,繼承是一個 “是不是”的關係,而 接口 實現則是 “有沒有”的關係。如果一個類繼承了某個抽象類,則子類必定是抽象類的種類,而接口實現則是有沒有、具備不具備的關係,比如鳥是否能飛(或者是否具備飛行這個特點),能飛行則可以實現這個接口,不能飛行就不實現這個接口。
2)設計層面不同,抽象類作爲很多子類的父類,它是一種模板式設計。而接口是一種行爲規範,它是一種輻射式設計。
什麼是模板式設計?最簡單例子,大家都用過ppt裏面的模板,如果用模板A設計了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它們的公共部分需要改動,則只需要改動模板A就可以了,不需要重新對ppt B和ppt C進行改動。而輻射式設計,比如某個電梯都裝了某種報警器,一旦要更新報警器,就必須全部更新。也就是說對於抽象類,如果需要添加新的方法,可以直接在抽象類中添加具體的實現,子類可以不進行變更;而對於接口則不行,如果接口進行了變更,則所有實現這個接口的類都必須進行相應的改動。
詳見好文:http://www.cnblogs.com/dolphin0520/p/3811437.html
2、什麼是序列化?
2.2.1 概念
序列化,序列化是可以把對象轉換成字節流在網絡上傳輸。將一個java對象變成字節流的形式傳出去或者從一個字節流中恢復成一個java對象。
個人認爲,序列化就是一種思想,能夠完成轉換,能夠轉換回來,效率越高越好
序列化(Serialization)是將對象的狀態信息轉換爲可以存儲或傳輸的形式的過程。在序列化期間,對象將其當前狀態寫入到臨時或持久性存儲區。之後可以通過從存儲區中讀取或反序列化對象的狀態,重新創建該對象。
java中的序列化(serialization)機制能夠將一個實例對象的狀態信息寫入到一個字節流中,使其可以通過socket進行傳輸、或者持久化存儲到數據庫或文件系統中;然後在需要的時候,可以根據字節流中的信息來重構一個相同的對象。序列化機制在java中有着廣泛的應用,EJB、RMI等技術都是以此爲基礎的。
一般而言,要使得一個類可以序列化,只需簡單實現java.io.Serializable接口即可(還要實現無參數的構造方法)。該接口是一個標記式接口,它本身不包含任何內容,實現了該接口則表示這個類準備支持序列化的功能。
2.2.2 序列化與反序列化例程
序列化一般有三種形式:默認形式、xml、json格式
默認格式如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
另兩種大同小異
2.2.3 應用場景
序列化的實現:將需要被序列化的類實現Serializable接口,該接口沒有需要實現的方法,implements Serializable只是爲了標註該對象是可被序列化的,然後使用一個輸出流(如:FileOutputStream)來構造一個ObjectOutputStream(對象流)對象,接着,使用ObjectOutputStream對象的writeObject(Object obj)方法就可以將參數爲obj的對象寫出(即保存其狀態),要恢復的話則用輸入流
詳見:http://blog.csdn.net/scythe666/article/details/51718784
三種情況下需要進行序列化
1、把對象持久化到文件或數據中
2、在網絡上傳輸
3、進行RMI傳輸對象時
RPC和RMI都是遠程調用,屬於中間件技術。RMI是針對於java語言的,它使用的是JRMP協議通信,而RPC是更大衆化的,使用http協議傳輸。
其版本號id,Java的序列化機制是通過在運行時判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,如果相同就認爲是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常。
常用序列化技術有3種:Java seriaizable,hessian,hessian2,以及protobuf
工具有很多,網上有個對比:
詳見:http://kb.cnblogs.com/page/515982/
3、網絡通信過程及實踐
2.3.1 TCP三次握手和四次揮手
明顯三次握手是建立連接,四次揮手是斷開連接,總圖如下:
2.3.1.1 握手
(1)首先,Client端發送連接請求報文(SYN=1,seq=client_isn)
(2)Server段接受連接後回覆ACK報文,併爲這次連接分配資源。(SYN=1,seq=client_isn,ack = client_isn+1)
(3)Client端接收到ACK報文後也向Server段發生ACK報文,並分配資源,這樣TCP連接就建立了。(SYN=0,seq=client_isn+1,ack = server_isn+1)
三次握手過程如下圖所示:
2.3.1.2 揮手
注意:
中斷連接端可以是Client端,也可以是Server端。
(1)假設Client端發起中斷連接請求,也就是發送FIN報文。
(2) Server端接到FIN報文後,意思是說”我Client端沒有數據要發給你了”,但是如果你還有數據沒有發送完成,則不必急着關閉Socket,可以繼續發送數據。所以 Server 端會先發送ACK,”告訴Client端,你的請求我收到了,但是我還沒準備好,請繼續你等我的消息”。
這個時候Client端就進入 FIN_WAIT 狀態,繼續等待Server端的FIN報文。
(3)當Server端確定數據已發送完成,則向Client端發送FIN報文,”告訴Client端,好了,我這邊數據發完了,準備好關閉連接了”。
(4)Client端收到FIN報文後,”就知道可以關閉連接了,但是他還是不相信網絡,怕Server端不知道要關閉,所以發送 ACK 後進入 TIME_WAIT 狀態,如果 Server 端沒有收到 ACK 則可以重傳“,Server端收到ACK後,”就知道可以斷開連接了”。
Client端等待了2MSL後依然沒有收到回覆,則證明Server端已正常關閉,那好,我Client端也可以關閉連接了。Ok,TCP連接就這樣關閉了!
注意:
(1)2個wait狀態,FIN_WAIT和TIME_WAIT
(2)如果是Server端發起,過程反過來,因爲在揮手的時候c和s在對等位置。
2.3.1.3 握手揮手狀態圖
Client端所經歷的狀態如下:
Server端所經歷的過程如下:
2.3.1.4 注意問題
1、在TIME_WAIT狀態中,如果TCP client端最後一次發送的ACK丟失了,它將重新發送。TIME_WAIT狀態中所需要的時間是依賴於實現方法的。典型的值爲30秒、1分鐘和2分鐘。等待之後連接正式關閉,並且所有的資源(包括端口號)都被釋放。
2、爲什麼連接的時候是三次握手,關閉的時候卻是四次握手?
答:因爲當Server端收到Client端的SYN連接請求報文後,可以直接發送SYN+ACK報文。其中ACK報文是用來應答的,SYN報文是用來同步的。但是關閉連接時,當Server端收到FIN報文時,很可能並不會立即關閉SOCKET,所以只能先回復一個ACK報文,告訴Client端,”你發的FIN報文我收到了”。只有等到我Server端所有的報文都發送完了,我才能發送FIN報文,因此不能一起發送。故需要四步握手。
3、爲什麼TIME_WAIT狀態需要經過2MSL(最大報文段生存時間)才能返回到CLOSE狀態?
答:雖然按道理,四個報文都發送完畢,我們可以直接進入CLOSE狀態了,但是我們必須假象網絡是不可靠的,有可以最後一個ACK丟失。所以TIME_WAIT狀態就是用來重發可能丟失的ACK報文。
2.3.1.5 附:報文詳解
TCP報文中的SYN,FIN,ACK,PSH,RST,URG
TCP的三次握手是怎麼進行的:發送端發送一個SYN=1,ACK=0標誌的數據包給接收端,請求進行連接,這是第一次握手;接收端收到請求並且允許連接的話,就會發送一個SYN=1,ACK=1標誌的數據包給發送端,告訴它,可以通訊了,並且讓發送端發送一個確認數據包,這是第二次握手;最後,發送端發送一個SYN=0,ACK=1的數據包給接收端,告訴它連接已被確認,這就是第三次握手。之後,一個TCP連接建立,開始通訊。
*SYN:同步標誌
同步序列編號(Synchronize Sequence Numbers)欄有效。該標誌僅在三次握手建立TCP連接時有效。它提示TCP連接的服務端檢查序列編號,該序列編號爲TCP連接初始端(一般是客戶端)的初始序列編號。在這裏,可以把 TCP序列編號看作是一個範圍從0到4,294,967,295的32位計數器。通過TCP連接交換的數據中每一個字節都經過序列編號。在TCP報頭中的序列編號欄包括了TCP分段中第一個字節的序列編號。*ACK:確認標誌
確認編號(Acknowledgement Number)欄有效。大多數情況下該標誌位是置位的。TCP報頭內的確認編號欄內包含的確認編號(w+1,Figure-1)爲下一個預期的序列編號,同時提示遠端系統已經成功接收所有數據。*RST:復位標誌
復位標誌有效。用於復位相應的TCP連接。*URG:緊急標誌
緊急(The urgent pointer) 標誌有效。緊急標誌置位*PSH:推標誌
該標誌置位時,接收端不將該數據進行隊列處理,而是儘可能快將數據轉由應用處理。在處理 telnet 或 rlogin 等交互模式的連接時,該標誌總是置位的。*FIN:結束標誌
帶有該標誌置位的數據包用來結束一個TCP回話,但對應端口仍處於開放狀態,準備接收後續數據。
TCP的幾個狀態對於我們分析所起的作用
在TCP層,有個FLAGS字段,這個字段有以下幾個標識:SYN, FIN, ACK, PSH, RST, URG.其中,對於我們日常的分析有用的就是前面的五個字段。它們的含義是:SYN表示建立連接,FIN表示關閉連接,ACK表示響應,PSH表示有 DATA數據傳輸,RST表示連接重置。其中,ACK是可能與SYN,FIN等同時使用的,比如SYN和ACK可能同時爲1,它表示的就是建立連接之後的響應,如果只是單個的一個SYN,它表示的只是建立連接。
TCP的幾次握手就是通過這樣的ACK表現出來的。但SYN與FIN是不會同時爲1的,因爲前者表示的是建立連接,而後者表示的是斷開連接。RST一般是在FIN之後纔會出現爲1的情況,表示的是連接重置。一般地,當出現FIN包或RST包時,我們便認爲客戶端與服務器端斷開了連接;而當出現SYN和SYN+ACK包時,我們認爲客戶端與服務器建立了一個連接。PSH爲1的情況,一般只出現在 DATA內容不爲0的包中,也就是說PSH爲1表示的是有真正的TCP數據包內容被傳遞。TCP的連接建立和連接關閉,都是通過請求-響應的模式完成的。
詳見:http://blog.csdn.net/scythe666/article/details/50967632
tcp的狀態
http://www.cnblogs.com/qlee/archive/2011/07/12/2104089.html
http://www.2cto.com/net/201209/157585.html
2.3.2 Socket通信
套接字(socket)是通信的基石,是支持TCP/IP協議的網絡通信的基本操作單元。它是網絡通信過程中端點的抽象表示,包含進行網絡通信必須的五種信息:連接使用的協議,本地主機的IP地址,本地進程的協議端口,遠地主機的IP地址,遠地進程的協議端口。
套接字對是一個四元組,(local ip, local port, remote ip, remote port),通過這一四元組,唯一確定了網絡通信的兩端(兩個進程或線程),ip地址確定主機,端口確定進程。
經典的在同一臺主機上兩個進程或線程之間的通信通過以下三種方法
管道通信(Pipes)
消息隊列(Message queues)
共享內存通信(Shared memory)
這裏有許多其他的方法,但是上面三中是非常經典的進程間通信。
詳見:http://blog.csdn.net/violet_echo_0908/article/details/49539593
socket編程實例:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
詳見:http://www.cnblogs.com/linzheng/archive/2011/01/23/1942328.html
2.3.3 Http
HTTP協議是無狀態的,同一個客戶端的這次請求和上次請求是沒有對應關係,對http服務器來說,它並不知道這兩個請求來自同一個客戶端。 爲了解決這個問題, Web程序引入了Cookie機制來維護狀態.
Http響應
在接收和解釋請求消息後,服務器返回一個HTTP響應消息。
HTTP響應也是由三個部分組成,分別是:狀態行、消息報頭、響應正文
1、狀態行格式如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
2、響應報頭
3、響應正文就是服務器返回的資源的內容
詳見:(1)http://www.cnblogs.com/li0803/archive/2008/11/03/1324746.html
(2)http://kb.cnblogs.com/page/130970/#statelesshttp
(3)http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386832653051fd44e44e4f9e4ed08f3e5a5ab550358d000
4、什麼是線程?java線程池運行過程及實踐(Executors)
一個進程包括多個線程,但是這些線程是共同享有進程佔有的資源和地址空間的。
進程是操作系統進行資源分配的基本單位,而線程是操作系統進行調度的基本單位。
進程可能包括多個線程。
好文:http://www.oschina.net/question/565065_86540
2.4.1 Volatile
線程的工作內存中保存了被該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量(包括volatile的底層實現)。
這裏的主內存、工作內存和Java堆棧、方法區不是一個層次內存劃分,基本上沒有關係。
如果要勉強對應:主內存對應Java堆中對象實例數據部分,工作內存對應於虛擬機棧中部分區域。
從更低層次來說,主內存就直接對應物理硬件內存,而爲了優化,工作內存優先儲存於寄存器和高速緩存中。
volatile可以說是Java虛擬機提供的最輕量級的同步機制。
當一個變量定義爲volatile以後,它將具備兩種屬性:
(1)保證此變量對所有線程的可見性
volatile的錯誤用法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
輸出的正確答案應該是200000,但是每次輸出都小於200000,併發失敗的問題在於increase()方法。用javap發編譯看一下發現就increase()方法在Class中文件有四條字節碼組成。
volatile變量只能保證可見性,當不符合一下規則是還是使用synchronized或java.util.concurrent中的原子類。
1.運算結果並不依賴變量的當前值,或者能夠確保單一的線程修改變量的
2.變量不需要與其他的狀態變量共同參與不變約束。
正確用法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
(2)使用volatile變量的第二個語義是禁止指令重排序優化
2.4.2 原子性、可見性與有序性
(1)原子性
保證read、load、assign、use、store和write操作是原子的
(2)可見性
當一個線程修改了共享變量的值,其他線程可以立即得知這個修改
(3)有序性
本線程觀察,所有的操作都是有序的,如果在一個線程中觀察另一個線程,所有操作都是無序的(指令重排序和工作內存與主內存同步延遲)。
2.4.3 Lock vs Synchronized
Synchronized關鍵字經過編譯以後,會在同步塊前後分別形成monitorenter和monitorexit這兩個字節碼指令。Synchronized 使用詳見:http://blog.csdn.net/luoweifu/article/details/46613015
主要相同點:lock能完成synchronized所實現的所有功能
主要不同點:lock有比synchronized更精確的線程語義和更好的性能.synchronized會自動釋放鎖,而Lock一定要求程序員手工釋放,並且必須在finally從句中釋放.
1、ReentrantLock 擁有Synchronized相同的併發性和內存語義,此外還多了 鎖投票,定時鎖等候和中斷鎖等候
線程A和B都要獲取對象O的鎖定,假設A獲取了對象O鎖,B將等待A釋放對O的鎖定,
如果使用 synchronized ,如果A不釋放,B將一直等下去,不能被中斷
如果 使用ReentrantLock,如果A不釋放,可以使B在等待了足夠長的時間以後,中斷等待,而幹別的事情
ReentrantLock獲取鎖定與三種方式:
a) lock(), 如果獲取了鎖立即返回,如果別的線程持有鎖,當前線程則一直處於休眠狀態,直到獲取鎖
b) tryLock(), 如果獲取了鎖立即返回true,如果別的線程正持有鎖,立即返回false;
c)tryLock(long timeout,TimeUnit unit), 如果獲取了鎖定立即返回true,如果別的線程正持有鎖,會等待參數給定的時間,在等待的過程中,如果獲取了鎖定,就返回true,如果等待超時,返回false;
d) lockInterruptibly:如果獲取了鎖定立即返回,如果沒有獲取鎖定,當前線程處於休眠狀態,直到或者鎖定,或者當前線程被別的線程中斷
2、synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現異常,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將unLock()放到finally{}中
3、在資源競爭不是很激烈的情況下,Synchronized的性能要優於ReetrantLock,但是在資源競爭很激烈的情況下,Synchronized的性能會下降幾十倍,但是ReetrantLock的性能能維持常態;
2.4.4 threadlocal
ThreadLocal 不是用於解決共享變量的問題的,不是爲了協調線程同步而存在,而是爲了方便每個線程處理自己的狀態而引入的一個機制,理解這點對正確使用ThreadLocal至關重要。
我們先看一個簡單的例子:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
運行後結果:
Thread-0 : 5
Thread-4 : 5
Thread-2 : 5
Thread-1 : 5
Thread-3 : 5
我們看到,每個線程累加後的結果都是5,各個線程處理自己的本地變量值,線程之間互不影響。
詳見:http://my.oschina.net/clopopo/blog/149368
2.4.5 java線程池 Executor框架
要配置一個線程池是比較複雜的,尤其是對於線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優的,因此在Executors類裏面提供了一些靜態工廠,生成一些常用的線程池。
(1)newSingleThreadExecutor
創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
MyThread.java
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
TestSingleThreadExecutor.java
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
輸出結果:
pool-1-thread-1正在執行…
pool-1-thread-1正在執行…
pool-1-thread-1正在執行…
pool-1-thread-1正在執行…
pool-1-thread-1正在執行…
(2)newFixedThreadPool
創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。
- 1
- 2
- 1
- 2
(3)newCachedThreadPool
創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,
那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。
- 1
- 2
- 1
- 2
(4)newScheduledThreadPool
創建一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
ThreadPoolExecutor構造函數
jvm本身提供的concurrent併發包,提供了高性能穩定方便的線程池,可以直接使用。
ThreadPoolExecutor是核心類,都是由它與3種Queue結合衍生出來的。
BlockingQueue + LinkedBlockingQueue + SynchronousQueue
ThreadPoolExecutor的完整構造方法的簽名是:
- 1
- 1
corePoolSize - 池中所保存的線程數,包括空閒線程。
maximumPoolSize-池中允許的最大線程數。
keepAliveTime - 當線程數大於核心時,此爲終止前多餘的空閒線程等待新任務的最長時間。
unit - keepAliveTime 參數的時間單位。
workQueue - 執行前用於保持任務的隊列。此隊列僅保持由 execute方法提交的 Runnable任務。
threadFactory - 執行程序創建新線程時使用的工廠。
handler - 由於超出線程範圍和隊列容量而使執行被阻塞時所使用的處理程序。
ThreadPoolExecutor是Executors類的底層實現。
在JDK幫助文檔中,有如此一段話:
“強烈建議程序員使用較爲方便的Executors工廠方法Executors.newCachedThreadPool()(無界線程池,可以進行自動線程回收)、Executors.newFixedThreadPool(int)(固定大小線程池)Executors.newSingleThreadExecutor()(單個後臺線程)
線程池實現原理
先從 BlockingQueue<Runnable> workQueue
這個入參開始說起。在JDK中,其實已經說得很清楚了,一共有三種類型的queue。
所有BlockingQueue 都可用於傳輸和保持提交的任務。可以使用此隊列與池大小進行交互:
(1)如果運行的線程少於 corePoolSize,則 Executor始終首選添加新的線程,而不進行排隊。(如果當前運行的線程小於corePoolSize,則任務根本不會存放,添加到queue中,而是直接抄傢伙(thread)開始運行)
(2)如果運行的線程等於或多於 corePoolSize,則 Executor始終首選將請求加入隊列,而不添加新的線程。
(3)如果無法將請求加入隊列,則創建新的線程,除非創建此線程超出maximumPoolSize,在這種情況下,任務將被拒絕。
線程的狀態有 new、runnable、running、waiting、timed_waiting、blocked、dead 一旦線程調用了start 方法,線程就轉到Runnable 狀態,注意,如果線程處於Runnable狀態,它也有可能不在運行,這是因爲還有優先級和調度問題。
排隊策略
排隊有三種通用策略:
(1)直接提交。工作隊列的默認選項是 SynchronousQueue,ExecutorService newCachedThreadPool():無界線程池,可以進行自動線程回收,所以我們可以發現maximumPoolSize爲big big。
(2)無界隊列。使用無界隊列(例如,不具有預定義容量的 LinkedBlockingQueue)將導致在所有 corePoolSize 線程都忙時新任務在隊列中等待。這樣,創建的線程就不會超過 corePoolSize。(因此,maximumPoolSize的值也就無效了。)當每個任務完全獨立於其他任務,即任務執行互不影響時,適合於使用無界隊列;例如,在 Web頁服務器中。這種排隊可用於處理瞬態突發請求,當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性。
(3)有界隊列。當使用有限的 maximumPoolSizes時,有界隊列(如 ArrayBlockingQueue)有助於防止資源耗盡,但是可能較難調整和控制。隊列大小和最大池大小可能需要相互折衷:使用大型隊列和小型池可以最大限度地降低 CPU 使用率、操作系統資源和上下文切換開銷,但是可能導致人工降低吞吐量。如果任務頻繁阻塞(例如,如果它們是 I/O邊界),則系統可能爲超過您許可的更多線程安排時間。使用小型隊列通常要求較大的池大小,CPU使用率較高,但是可能遇到不可接受的調度開銷,這樣也會降低吞吐量。
keepAliveTime
jdk中的解釋是:當線程數大於核心時,此爲終止前多餘的空閒線程等待新任務的最長時間。
有點拗口,其實這個不難理解,在使用了“池”的應用中,大多都有類似的參數需要配置。比如數據庫連接池,DBCP中的maxIdle,minIdle參數。
什麼意思?接着上面的解釋,後來向老闆派來的工人始終是“借來的”,俗話說“有借就有還”,但這裏的問題就是什麼時候還了,如果借來的工人剛完成一個任務就還回去,後來發現任務還有,那豈不是又要去借?這一來一往,老闆肯定頭也大死了。
合理的策略:既然借了,那就多借一會兒。直到“某一段”時間後,發現再也用不到這些工人時,便可以還回去了。這裏的某一段時間便是keepAliveTime的含義,TimeUnit爲keepAliveTime值的度量。
詳參:http://www.oschina.net/question/565065_86540
5、java反射機制實踐
詳見:http://blog.csdn.net/scythe666/article/details/51704809
反射可以拿到一個類所有的方法和屬性,包括父類和接口。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
輸出:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
三、設計模式
1、單例模式
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
線程安全+懶加載實現:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
詳見:http://blog.csdn.net/jason0539/article/details/23297037
2、原型模式
類圖:
原型模式主要用於對象的複製,它的核心是就是類圖中的原型類Prototype。Prototype類需要具備以下兩個條件:
(1)實現Cloneable接口。在java語言有一個Cloneable接口,它的作用只有一個,就是在運行時通知虛擬機可以安全地在實現了此接口的類上使用clone方法。在java虛擬機中,只有實現了這個接口的類纔可以被拷貝,否則在運行時會拋出CloneNotSupportedException異常。
(2)重寫Object類中的clone方法。Java中,所有類的父類都是Object類,Object類中有一個clone方法,作用是返回對象的一個拷貝,但是其作用域protected類型的,一般的類無法調用,因此,Prototype類需要將clone方法的作用域修改爲public類型。
原型模式是一種比較簡單的模式,也非常容易理解,實現一個接口,重寫一個方法即完成了原型模式。在實際應用中,原型模式很少單獨出現。經常與其他模式混用,他的原型類Prototype也常用抽象類來替代。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
詳見:http://blog.csdn.net/jason0539/article/details/23158081
3、動態代理模式
一般的設計模式中的代理模式指的是靜態代理,但是Java實現了動態代理
靜態代理的每一個代理類只能爲一個或一組接口服務,這樣一來程序開發中必然會產生過多的代理,而且,所有的代理操作除了調用的方法不一樣之外,其他的操作都一樣,則此時肯定是重複代碼。解決這一問題最好的做法是可以通過一個代理類完成全部的代理功能,那麼此時就必須使用動態代理完成。
來看一下動態代理:
JDK動態代理中包含一個類和一個接口:
InvocationHandler接口:
- 1
- 2
- 3
- 1
- 2
- 3
參數說明:
- 1
- 2
- 3
- 1
- 2
- 3
可以將InvocationHandler接口的子類想象成一個代理的最終操作類,替換掉ProxySubject。
Proxy類:
Proxy類是專門完成代理的操作類,可以通過此類爲一個或多個接口動態地生成實現類,此類提供瞭如下的操作方法:
- 1
- 2
- 1
- 2
參數說明:
- 1
- 2
- 3
- 1
- 2
- 3
Ps:類加載器
在Proxy類中的newProxyInstance()方法中需要一個ClassLoader類的實例,ClassLoader實際上對應的是類加載器,在Java中主要有一下三種類加載器;
Booststrap ClassLoader:此加載器採用C++編寫,一般開發中是看不到的;
Extendsion ClassLoader:用來進行擴展類的加載,一般對應的是jre\lib\ext目錄中的類;
AppClassLoader:(默認)加載classpath指定的類,是最常使用的是一種加載器。
動態代理
與靜態代理類對照的是動態代理類,動態代理類的字節碼在程序運行時由Java反射機制動態生成,無需程序員手工編寫它的源代碼。動態代理類不僅簡化了編程工作,而且提高了軟件系統的可擴展性,因爲Java 反射機制可以生成任意類型的動態代理類。java.lang.reflect 包中的Proxy類和InvocationHandler 接口提供了生成動態代理類的能力。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
但是,JDK的動態代理依靠接口實現,如果有些類並沒有實現接口,則不能使用JDK代理,這就要使用cglib動態代理了。
Cglib動態代理
JDK的動態代理機制只能代理實現了接口的類,而不能實現接口的類就不能實現JDK的動態代理,cglib是針對類來實現代理的,他的原理是對指定的目標類生成一個子類,並覆蓋其中方法實現增強,但因爲採用的是繼承,所以不能對final修飾的類進行代理。
詳見:http://www.cnblogs.com/jqyp/archive/2010/08/20/1805041.html
四、Spring
1、什麼是IOC
IOC(inverse of controll)控制反轉(控制權反轉),就是把創建對象(bean),和維護對象(bean)的關係和權力從程序中轉移到spring的容器(applicationContext.xml),而程序本身不再關心、維護對象創建和關係
2、什麼是AOP
aop( aspect oriented programming ) 面向切面(方面)編程,是對所有對象或者是一類對象編程,核心是( 在不增加代碼的基礎上, 還增加新功能 ),aop實現原理是代理。
面向切面 spring( ->aop) 面向n多對象編程,面向一批對象編程
交叉點,交叉功能放入的過程叫做織入
使用比較底層的ProxyFactoryBean編程說明:
步驟:
1. 定義接口
2. 編寫對象(被代理對象=目標對象)
3. 編寫通知(前置通知目標方法調用前調用)
4. 在beans.xml文件配置
4.1 配置 被代理對象=目標對象
4.2 配置通知
4.3 配置代理對象 是 ProxyFactoryBean的對象實例
4.3.1 代理接口集
4.3.2 織入通知
4.3.3 配置被代理對象
1.切面(aspect):要實現的交叉功能,是系統模塊化的一個切面或領域。如日誌記錄。
2.連接點:應用程序執行過程中插入切面的地點,可以是方法調用,異常拋出,或者要修改的
字段。
3.通知:切面的實際實現,他通知系統新的行爲。如在日誌通知包含了實
現日誌功能的代碼,如向日志文件寫日誌。通知在連接點插入到應用系統中。
4.切入點:定義了通知應該應用在哪些連接點,通知可以應用到AOP框架支持的任何連接點。
5.引入:爲類添加新方法和屬性。
6.目標對象:被通知的對象。既可以是你編寫的類也可以是第三方類。
7.代理:將通知應用到目標對象後創建的對象,應用系統的其他部分不用爲了支持代理對象而
改變。
8.織入:將切面應用到目標對象從而創建一個新代理對象的過程。織入發生在目標
對象生命週期的多個點上:
編譯期:切面在目標對象編譯時織入.這需要一個特殊的編譯器.
類裝載期:切面在目標對象被載入JVM時織入.這需要一個特殊的類載入器.
運行期:切面在應用系統運行時織入.
提問? 說spring的aop中,當你通過代理對象去實現aop的時候,獲取的ProxyFactoryBean是什麼類型?
答: 返回的是一個代理對象,如果目標對象實現了接口,則spring使用jdk 動態代理技術,如果目標對象沒有實現接口,則spring使用CGLIB技術.
詳見:http://blog.csdn.net/scythe666/article/details/51727234
3、spring事務管理
事務指的是邏輯上的一組操作,這組操作要麼全部成功,要麼全部失敗。
一般的事務指的都是數據庫事務,但是廣義事務的定義不侷限於數據庫事務。
事務有4大特性,即 ACID。
ACID,指數據庫事務正確執行的四個基本要素的縮寫。包含:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)。一個支持事務(Transaction)的數據庫,必需要具有這四種特性,否則在事務過程(Transaction processing)當中無法保證數據的正確性,交易過程極可能達不到交易方的要求。
1、原子性
事務是一個不可分割的工作單位,事務中的操作要麼都發生,要麼都不發生。
2、一致性
事務前後數據的完整性必須保證一致
比如還是剛剛A給B轉賬的例子,那麼A給B轉賬結束後,總金額不變。
3、隔離性
多個用戶併發訪問數據庫時,一個用戶的事務不能被其他用戶的事務所幹擾,多個併發事務之間數據要互相隔離。
隔離性非常重要,如果不考慮隔離性,就可能發生:髒讀、不可重複讀、幻讀的問題
(1)髒讀
一個事務讀取了另一個事務改寫但還未提交的數據,如果這些數據被回滾,則讀到的數據無效。
(2)不可重複讀
在同一事務中,多次讀取同一數據返回的結果不同。
(3)幻讀
一個事務讀取了幾行記錄後,另一個事務插入一些記錄。後來的查詢中,第一個事務就會發現有些原來沒有的記錄。
當然,這些問題是有辦法避免的,有隔離級別來限制,後面做解釋。
4、持久性
一個事務一旦提交,它對數據庫中的數據的改變就是永久性的,即使數據庫發生故障也不應該對其有任何影響
事務的隔離級別(4種)
事務的隔離級別是爲了防止髒讀、不可重複讀、幻讀問題的發生,具體分成四種,如下:
Spring有一個default隔離級別,底層數據庫用的哪個隔離級別,spring就用什麼隔離級別
MySQL用的是repeatable_read
Oracle用的是read_committed
有一個更加直觀的表格如下:
spring中的事務隔離級別配置如下:
4.3.1 模板事務跟標註事務的區別及運理原理
(1)編程式事務
applicationContext.xml
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
AccountService.java
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
AccountServiceImpl.java
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
AccountDao.java
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
AccoutDaoImpl.java
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
(2)聲明式事務1:基於TransactionProxyFactoryBean的方式
因爲聲明式事務管理都是非侵入性的(不用修改原代碼),只用配置,所以就不帖源代碼了
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
(3)聲明式事務2:基於AspectJ的XML方式
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
(4)聲明式事務3:基於註解的方式
基於註解的方式,配置十分簡單,只需在業務層需要事務的類上面打上註解
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
小結
註解和聲明對比:
註解簡單
聲明是非侵入式
感悟:但是本質都是告訴框架,哪些類需要被代理來執行事務。
4.3.2 什麼是事務的傳播機制
首先要清楚的是:事務是因爲有業務需求,才產生的一種機制。
所以事務的配置應該安放在業務層
比如轉錢的例子:
如果aaa()和 bbb()方法需要用事務來解決,應該如何處理他們之間的關係呢?
這就需要用事務的傳播行爲來定義了
事務的傳播行爲詳見下表:
其實這7種行爲看起來很多,但是實則可以就分爲3類:
(1)第一類 required:在當前事務中解決問題
(2)第二類 requires_new:掛起當前事務,簡單來說就是隔離
思考:
爲什麼取流水號和打印日誌需要用requires_new?
這主要有兩個原因:
① 爲了取號速度,取號是事務的第一步,因爲如果不新建一個事務,取號需要加鎖,如果這個事務比較長,就需要一直佔着鎖,這樣就很慢。
② 既然是隔離,就是說取號和真正的事務處理不發生影響。這個原因也造成了一個結果,流水號有“作廢”機制,也就是說萬一發生異常,這個流水號也生成了,後面的會跳號。事務間就沒有依賴關係了,會產生四種情況
(3)第三類 nested:嵌套事務
也就是說:
Required 操作在同一個事務裏面
New aaa() 和 bbb() 不在一個事務中
spring的事務傳播配置如下:
詳見:http://blog.csdn.net/scythe666/article/details/51790655
spring的事務真正處理事務的是事務管理器。
五、數據庫
1、鎖機制
5.1.1 鎖的作用是什麼
數據庫是一個多用戶的共享資源。當多個用戶併發的存取數據時,在數據庫中就會產生多個事務同時存取同一數據的情況。若對併發操作不加控制就可能會讀取和存儲不正確的數據,破壞數據庫一致性。
加鎖是實現數據庫併發控制的一個非常重要的技術。當事務在對某個數據對象進行操作前,先向系統發出請求,對其加鎖。加鎖後事務就對該數據對象有了一定的控制,在該事務釋放鎖之前,其他的事務不能對此數據對象進行更新操作。
基本鎖類型包括行級鎖和表級鎖。
表級:直接鎖定整張表,在你鎖定期間,其它進程無法對該表進行寫操作。如果你是寫鎖,則其它進程則讀也不允許
行級:僅對指定的記錄進行加鎖,這樣其它進程還是可以對同一個表中的其它記錄進行操作。
頁級:表級鎖速度快,但衝突多,行級衝突少,但速度慢。所以取了折衷的頁級,一次鎖定相鄰的一組記錄。
5.1.2 什麼是樂觀鎖,什麼是悲觀鎖,怎麼實現
樂觀鎖
相對悲觀鎖而言,樂觀鎖機制採取了更加寬鬆的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。如一個金融系統,當某個操作員讀取用戶的數據,並在讀出的用戶數據的基礎上進行修改時(如更改用戶帳戶餘額),如果採用悲觀鎖機制,也就意味着整個操作過程中(從操作員讀出數據、開始修改直至提交修改結果的全過程,甚至還包括操作 員中途去煮咖啡的時間),數據庫記錄始終處於加鎖狀態,可以想見,如果面對幾百上千個併發,這樣的情況將導致怎樣的後果。樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖大多是基於數據版本 (Version)記錄機制實現。何謂數據版本?即爲數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過爲數據庫表增加一個“version”字段來實現。讀取出數據時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認爲是過期數據。
悲觀鎖
正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處於鎖定 狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。比如在使用select字句的時候加上for update,那麼直到字句的事務結束爲止,任何應用都無修改select出來的記錄。
5.1.3 關於信號量 Semaphore
http://iaspecwang.iteye.com/blog/1931031
補充:信號量初始化爲1(binary semaphore),而不用lock
jdk文檔有如下一段話:
A semaphore initialized to one, and which is used such that it only has at most one permit available, can serve as a mutual exclusion lock. This is more commonly known as a binary semaphore, because it only has two states: one permit available, or zero permits available. When used in this way, the binary semaphore has the property (unlike many Lock implementations), that the “lock” can be released by a thread other than the owner (as semaphores have no notion of ownership). This can be useful in some specialized contexts, such as deadlock recovery.
將信號量初始化爲1,使得它在使用時最多隻有一個可用的許可,從而可用作一個相互排斥的鎖。這通常也稱爲二進制信號量,因爲它只能有兩種狀態:一個可用的許可,或零個可用的許可。按此方式使用時,二進制信號量具有某種屬性(與很多 Lock 實現不同),即可以由線程釋放“鎖”,而不是由所有者(因爲信號量沒有所有權的概念)。在某些專門的上下文(如死鎖恢復)中這會很有用。
2、索引
MySQL官方對索引的定義爲:索引(Index)是幫助MySQL高效獲取數據的數據結構。提取句子主幹,就可以得到索引的本質:索引是數據結構。目前大部分數據庫系統及文件系統都採用B-Tree或其變種B+Tree作爲索引結構。
聚集索引: InnoDB使用B+Tree作爲索引結構,主索引的葉節點包含了完整的數據記錄。這種索引叫做聚集索引。InnoDB的輔助索引data域存儲相應記錄主鍵的值。換句話說,InnoDB的所有輔助索引都引用主鍵作爲data域。
非聚集索引: MyISAM也採用B+Tree作爲索引結構,但其data域保存數據記錄的地址,因此,MyISAM的索引方式也叫做“非聚集”的,之所以這麼稱呼是爲了與InnoDB的聚集索引區分。在MyISAM中,主索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主索引要求key是唯一的,而輔助索引的key可以重複。
5.2.1 聯合索引
聯合索引又叫複合索引。對於複合索引:Mysql從左到右的使用索引中的字段,一個查詢可以只使用索引中的一部份,但只能是最左側部分。例如索引是key index (a,b,c)。 可以支持a | a,b| a,b,c 3種組合進行查找,但不支持 b,c進行查找 .當最左側字段是常量引用時,索引就十分有效。兩個或更多個列上的索引被稱作複合索引。
利用索引中的附加列,您可以縮小搜索的範圍,但使用一個具有兩列的索引 不同於使用兩個單獨的索引。複合索引的結構與電話簿類似,人名由姓和名構成,電話簿首先按姓氏對進行排序,然後按名字對有相同姓氏的人進行排序。如果您知 道姓,電話簿將非常有用;如果您知道姓和名,電話簿則更爲有用,但如果您只知道名不姓,電話簿將沒有用處。所以說創建複合索引時,應該仔細考慮列的順序。對索引中的所有列執行搜索或僅對前幾列執行搜索時,複合索引非常有用;僅對後面的任意列執行搜索時,複合索引則沒有用處。
參考:http://blog.codinglabs.org/articles/theory-of-mysql-index.html
5.2.2 sql執行計劃
一個SQL語句表示你所想要得到的但是並沒有告訴Server如何去做。 例如, 利用一個SQL語句, 你可能要Server取出所有住在Prague的客戶。 當Server收到的這條SQL的時候, 第一件事情並不是解析它。 如果這條SQL沒有語法錯誤, Server纔會繼續工作。 Server會決定最好的計算方式。 Server會選擇, 是讀整個客戶表好呢, 還是利用索引會比較快些。 Server會比較所有可能方法所耗費的資源。 最終SQL語句被物理性執行的方法被稱做執行計劃或者是查詢計劃。
一個執行計劃右若干基本操作組成。 例如, 遍歷整張表, 利用索引, 執行一個嵌套循環或Hash連接等等。 我們將在這一系列的文章裏詳細討論。 所有的基本操作都有一個輸出: 結果集。 有些, 象嵌套循環, 有一個輸入。 其他的, 象Hash連接, 有兩個輸入。 每個輸入應與其它基本操作的的輸出想連接。 這也就是爲什麼一個執行可以被看做是一個數的原因: 信息從樹葉流向樹根。 在文章的下面部分有很多諸如此類的例子。
負責處理或計算最優的執行計劃的DB Server組件叫優化器。 優化器是建立在其所在的DB資源的基礎上而進行工作的。
說白了就是數據庫服務器在執行sql語句之前會制定幾套執行計劃,看那個機會消耗的系統資源少,就是用那套計劃。
—END—