(1)基本概念:
JVM 是可運行 JAVA 代碼的假想計算機 ,包括一套字節碼指令集、一組寄存器、一個棧、
一個垃圾回收,堆 和 一個存儲方法域。 JVM 是運行在操作系統之上的,它與硬件沒有直接
的交互。
(2)運行過程:
我們都知道 Java 源文件,通過編譯器,能 夠生產相應的 .Class 文件,也就是字節碼文件,
而字節碼文件又通過 Java 虛擬機中的解釋器,編譯成特定機器上的機器碼 。
也就是如下:
① Java 源文件 —->編譯器 —->字節碼文件
② 字節碼文件 —->JVM JVM—->機器碼
每一種平臺的解釋器是不同的,但是實現的虛擬機是相同的,這也就是 Java 為什么能夠
跨平臺的原因了 ,當一個程序從開始運行,這時虛擬機就開始實例化了,多個程序啟動就會
存在多個虛擬機實例。程序退出或者關閉,則虛擬機實例消亡,多個虛擬機實例之間數據不
能共享。
1.1. 線程
這里所說的線程指程序執行過程中的一個線程實體。
JVM 允許一個應用并發執行多個線程。
Hotspot JVM 中的 Java 線程與原生操作系統線程有直接的映射關系 。當線程本地存儲、緩
沖區分配、同步對象、棧、程序計數器等準備好以后,就會創建一個操作系統原生線程 。 Java 線程結束,原生線程隨之被回收。操作系統負責調度所有線程,并把它們分配到任何可
用的 CPU 上。當原生線程初始化完畢,就會調用 Java 線程的 run() 方法。當線程結束時,會釋放原生線程和Java 線程的所有資源。
Hotspot JVM 后臺運行的系統線程主要有下面幾個:
1.2. JVM內存區域
JVM 內存區域主要分為線程私有區域【程序計數器、虛擬機棧、本地方法區】、線程共享區
域【 JAVA 堆、方法區】、直接內存。
線程私有數據區域生命周期與線程相同, 依賴用戶線程的啟動/結束 而 創建/銷毀(在Hotspot VM內, 每個線程都與操作系統的本地線程直接映射, 因此這部分內存區域的存/否跟隨本地線程的生/死對應)。
線程共享區域隨虛擬機的啟動/關閉而創建/銷毀。
直接內存并不是JVM運行時數據區的一部分, 但也會被頻繁的使用: 在JDK 1.4引入的NIO提供了基于Channel與Buffer的IO方式, 它可以使用Native函數庫直接分配堆外內存, 然后使用DirectByteBuffer對象作為這塊內存的引用進行操作(詳見: Java I/O 擴展), 這樣就避免了在Java堆和Native堆中來回復制數據, 因此在一些場景中可以顯著提高性能。
1.2.1. 程序計數器(線程私有)
一塊較小的內存空間, 是當前線程所執行的字節碼的行號指示器,每條線程都要有一個獨立的程序計數器,這類內存也稱為“線程私有”的內存。
正在執行java方法的話,計數器記錄的是虛擬機字節碼指令的地址(當前指令的地址)。如果還是Native方法,則為空。
這個內存區域是唯一一個在虛擬機中沒有規定任何OutOfMemoryError情況的區域。
1.2.2. 虛擬機棧(線程私有)
是描述java方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
棧幀( Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接 (Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。棧幀隨著方法調用而創建,隨著方法結束而銷毀——無論方法是正常完成還是異常完成(拋出了在方法內未被捕獲的異常)都算作方法結束。
1.2.3. 本地方法區(線程私有)
本地方法區和Java Stack作用類似, 區別是虛擬機棧為執行Java方法服務, 而本地方法棧則為Native方法服務, 如果一個VM實現使用C-linkage模型來支持Native調用, 那么該棧將會是一個C棧,但HotSpot VM直接就把本地方法棧和虛擬機棧合二為一。
1.2.4. 堆(Heap-線程共享)-運行時數據區
是被線程共享的一塊內存區域,創建的對象和數組都保存在Java堆內存中,也是垃圾收集器進行垃圾收集的最重要的內存區域。由于現代VM采用分代收集算法, 因此Java堆從GC的角度還可以細分為: 新生代(Eden區、From Survivor區和To Survivor區)和老年代。
1.2.5. 方法區/永久代(線程共享)
即我們常說的永久代(Permanent Generation), 用于存儲被JVM加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據. HotSpot VM把GC分代收集擴展至方法區, 即使用Java堆的永久代來實現方法區, 這樣HotSpot的垃圾收集器就可以像管理Java堆一樣管理這部分內存, 而不必為方法區開發專門的內存管理器(永久帶的內存回收的主要目標是針對常量池的回收和類型的卸載, 因此收益一般很小)。 運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。 Java虛擬機對Class文件的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用于存儲哪種數據都必須符合規范上的要求,這樣才會被虛擬機認可、裝載和執行。
1.3. JVM運行時內存
Java堆從GC的角度還可以細分為: 新生代(Eden區、From Survivor區和To Survivor區)和老年代。
1.3.1. 新生代
是用來存放新生的對象。一般占據堆的1/3空間。由于頻繁創建對象,所以新生代會頻繁觸發MinorGC進行垃圾回收。新生代又分為 Eden區、ServivorFrom、ServivorTo三個區。
1.3.1.1. Eden區
Java新對象的出生地(如果新創建的對象占用內存很大,則直接分配到老年代)。當Eden區內存不夠的時候就會觸發MinorGC,對新生代區進行一次垃圾回收。
1.3.1.2. ServivorFrom
上一次GC的幸存者,作為這一次GC的被掃描者。
1.3.1.3. ServivorTo
保留了一次MinorGC過程中的幸存者。
1.3.1.4. MinorGC的過程(復制->清空->互換)
MinorGC采用復制算法。
1:eden、servicorFrom 復制到ServicorTo,年齡+1
首先,把Eden和ServivorFrom區域中存活的對象復制到ServicorTo區域(如果有對象的年齡以及達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(如果ServicorTo不夠位置了就放到老年區);
2:清空eden、servicorFrom
然后,清空Eden和ServicorFrom中的對象;
3:ServicorTo和ServicorFrom互換
最后,ServicorTo和ServicorFrom互換,原ServicorTo成為下一次GC時的ServicorFrom區。
1.3.2. 老年代
主要存放應用程序中生命周期長的內存對象。
老年代的對象比較穩定,所以MajorGC不會頻繁執行。在進行MajorGC前一般都先進行了一次MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次MajorGC進行垃圾回收騰出空間。
MajorGC采用標記清除算法:首先掃描一次所有老年代,標記出存活的對象,然后回收沒有標記的對象。MajorGC的耗時比較長,因為要掃描再回收。MajorGC會產生內存碎片,為了減少內存損耗,我們一般需要進行合并或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出OOM(Out of Memory)異常。
1.3.3. 永久代
指內存的永久保存區域,主要存放Class和Meta(元數據)的信息,Class在被加載的時候被放入永久區域,它和和存放實例的區域不同,GC不會在主程序運行期對永久區域進行清理。所以這也導致了永久代的區域會隨著加載的Class的增多而脹滿,最終拋出OOM異常。
1.3.3.1. JAVA8與元數據
在Java8中,永久代已經被移除,被一個稱為“元數據區”(元空間)的區域所取代。元空間的本質和永久代類似,元空間與永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。類的元數據放入 native memory, 字符串池和類的靜態變量放入java堆中,這樣可以加載多少類的元數據就不再由MaxPermSize控制, 而由系統的實際可用空間來控制。
1.4. 垃圾回收與算法
1.4.1. 如何確定垃圾
1.4.1.1. 引用計數法
在Java中,引用和對象是有關聯的。如果要操作對象則必須用引用進行。因此,很顯然一個簡單的辦法是通過引用計數來判斷一個對象是否可以回收。簡單說,即一個對象如果沒有任何與之關聯的引用,即他們的引用計數都不為0,則說明對象不太可能再被用到,那么這個對象就是可回收對象。
1.4.1.2. 可達性分析
為了解決引用計數法的循環引用問題,Java使用了可達性分析的方法。通過一系列的“GC roots”對象作為起點搜索。如果在“GC roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的。
要注意的是,不可達對象不等價于可回收對象,不可達對象變為可回收對象至少要經過兩次標記過程。兩次標記后仍然是可回收對象,則將面臨回收。
1.4.2. 標記清除算法(Mark-Sweep)
最基礎的垃圾回收算法,分為兩個階段,標注和清除。標記階段標記出所有需要回收的對象,清除階段回收被標記的對象所占用的空間。如圖
從圖中我們就可以發現,該算法最大的問題是內存碎片化嚴重,后續可能發生大對象不能找到可利用空間的問題。
2.4.3. 復制算法(copying)
為了解決Mark-Sweep算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分為等大小的兩塊。每次只使用其中一塊,當這一塊內存滿后將尚存活的對象復制到另一塊上去,把已使用的內存清掉,如圖:
這種算法雖然實現簡單,內存效率高,不易產生碎片,但是最大的問題是可用內存被壓縮到了原本的一半。且存活對象增多的話,Copying算法的效率會大大降低。
1.4.4. 標記整理算法(Mark-Compact)
結合了以上兩個算法,為了避免缺陷而提出。標記階段和Mark-Sweep算法相同,標記后不是清理對象,而是將存活對象移向內存的一端。然后清除端邊界外的對象。如圖:
1.4.5. 分代收集算法
分代收集法是目前大部分JVM所采用的方法,其核心思想是根據對象存活的不同生命周期將內存劃分為不同的域,一般情況下將GC堆劃分為老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特點是每次垃圾回收時只有少量對象需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的算法。
1.4.5.1. 新生代與復制算法
目前大部分JVM的GC對于新生代都采取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,即要復制的操作比較少,但通常并不是按照1:1來劃分新生代。一般將新生代劃分為一塊較大的Eden空間和兩個較小的Survivor空間(From Space, To Space),每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將該兩塊空間中還存活的對象復制到另一塊Survivor空間中。
1.4.5.2. 老年代與標記復制算法
而老年代因為每次只回收少量對象,因而采用Mark-Compact算法。 1. JAVA虛擬機提到過的處于方法區的永生代(Permanet Generation),它用來存儲class類,常量,方法描述等。對永生代的回收主要包括廢棄常量和無用的類。 2. 對象的內存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放對象的那一塊),少數情況會直接分配到老生代。 3. 當新生代的Eden Space和From Space空間不足時就會發生一次GC,進行GC后,Eden Space和From Space區的存活對象會被挪到To Space,然后將Eden Space和From Space進行清理。 4. 如果To Space無法足夠存儲某個對象,則將這個對象存儲到老生代。 5. 在進行GC后,使用的便是Eden Space和To Space了,如此反復循環。 6. 當對象在Survivor區躲過一次GC后,其年齡就會+1。默認情況下年齡到達15的對象會被移到老生代中。
1.5. JAVA 四中引用類型
1.5.1. 強引用
在Java中最常見的就是強引用,把一個對象賦給一個引用變量,這個引用變量就是一個強引用。當一個對象被強引用變量引用時,它處于可達狀態,它是不可能被垃圾回收機制回收的,即使該對象以后永遠都不會被用到JVM也不會回收。因此強引用是造成Java內存泄漏的主要原因之一。
1.5.2. 軟引用
軟引用需要用SoftReference類來實現,對于只有軟引用的對象來說,當系統內存足夠時它不會被回收,當系統內存空間不足時它會被回收。軟引用通常用在對內存敏感的程序中。
1.5.3. 弱引用
弱引用需要用WeakReference類來實現,它比軟引用的生存期更短,對于只有弱引用的對象來說,只要垃圾回收機制一運行,不管JVM的內存空間是否足夠,總會回收該對象占用的內存。
1.5.4. 虛引用
虛引用需要PhantomReference類來實現,它不能單獨使用,必須和引用隊列聯合使用。虛引用的主要作用是跟蹤對象被垃圾回收的狀態。
1.6. GC分代收集算法VS 分區收集算法
1.6.1. 分代收集算法
當前主流VM垃圾收集都采用”分代收集”(Generational Collection)算法, 這種算法會根據對象存活周期的不同將內存劃分為幾塊, 如JVM中的 新生代、老年代、永久代,這樣就可以根據各年代特點分別采用最適當的GC算法
1.6.1.1. 在新生代-復制算法
每次垃圾收集都能發現大批對象已死, 只有少量存活. 因此選用復制算法, 只需要付出少量存活對象的復制成本就可以完成收集.
1.6.1.2. 在老年代-標記整理算法
因為對象存活率高、沒有額外空間對它進行分配擔保, 就必須采用“標記—清理”或“標記—整理”算法來進行回收, 不必進行內存復制, 且直接騰出空閑內存.
1.6.2. 分區收集算法
分區算法則將整個堆空間劃分為連續的不同小區間, 每個小區間獨立使用, 獨立回收. 這樣做的好處是可以控制一次回收多少個小區間 , 根據目標停頓時間, 每次合理地回收若干個小區間(而不是整個堆), 從而減少一次GC所產生的停頓。
1.7. GC垃圾收集器
Java堆內存被劃分為新生代和年老代兩部分,新生代主要使用復制和標記-清除垃圾回收算法;年老代主要使用標記-整理垃圾回收算法,因此java虛擬中針對新生代和年老代分別提供了多種不同的垃圾收集器,JDK1.6中Sun HotSpot虛擬機的垃圾收集器如下:
1.7.1. Serial垃圾收集器(單線程、復制算法)
Serial(英文連續)是最基本垃圾收集器,使用復制算法,曾經是JDK1.3.1之前新生代唯一的垃圾收集器。Serial是一個單線程的收集器,它不但只會使用一個CPU或一條線程去完成垃圾收集工作,并且在進行垃圾收集的同時,必須暫停其他所有的工作線程,直到垃圾收集結束。 Serial垃圾收集器雖然在收集垃圾過程中需要暫停所有其他的工作線程,但是它簡單高效,對于限定單個CPU環境來說,沒有線程交互的開銷,可以獲得最高的單線程垃圾收集效率,因此Serial垃圾收集器依然是java虛擬機運行在Client模式下默認的新生代垃圾收集器。
1.7.2. ParNew垃圾收集器(Serial+多線程)
ParNew垃圾收集器其實是Serial收集器的多線程版本,也使用復制算法,除了使用多線程進行垃圾收集之外,其余的行為和Serial收集器完全一樣,ParNew垃圾收集器在垃圾收集過程中同樣也要暫停所有其他的工作線程。
ParNew收集器默認開啟和CPU數目相同的線程數,可以通過-XX:ParallelGCThreads參數來限制垃圾收集器的線程數。【Parallel:平行的】 ParNew雖然是除了多線程外和Serial收集器幾乎完全一樣,但是ParNew垃圾收集器是很多java虛擬機運行在Server模式下新生代的默認垃圾收集器。
1.7.3. Parallel Scavenge收集器(多線程復制算法、高效)
Parallel Scavenge收集器也是一個新生代垃圾收集器,同樣使用復制算法,也是一個多線程的垃圾收集器,它重點關注的是程序達到一個可控制的吞吐量(Thoughput,CPU用于運行用戶代碼的時間/CPU總消耗時間,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)),高吞吐量可以最高效率地利用CPU時間,盡快地完成程序的運算任務,主要適用于在后臺運算而不需要太多交互的任務。自適應調節策略也是ParallelScavenge收集器與ParNew收集器的一個重要區別。
1.7.4. Serial Old收集器(單線程標記整理算法)
Serial Old是Serial垃圾收集器年老代版本,它同樣是個單線程的收集器,使用標記-整理算法,這個收集器也主要是運行在Client默認的java虛擬機默認的年老代垃圾收集器。 在Server模式下,主要有兩個用途: 1. 在JDK1.5之前版本中與新生代的Parallel Scavenge收集器搭配使用。 2. 作為年老代中使用CMS收集器的后備垃圾收集方案。 新生代Serial與年老代Serial Old搭配垃圾收集過程圖:
1.7.5. Parallel Old收集器(多線程標記整理算法)
Parallel Old收集器是Parallel Scavenge的年老代版本,使用多線程的標記-整理算法,在JDK1.6才開始提供。 在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保證新生代的吞吐量優先,無法保證整體的吞吐量,Parallel Old正是為了在年老代同樣提供吞吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,可以優先考慮新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。 新生代Parallel Scavenge和年老代Parallel Old收集器搭配運行過程圖:
1.7.6. CMS收集器(多線程標記清除算法)
Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾回收停頓時間,和其他年老代使用標記-整理算法不同,它使用多線程的標記-清除算法。 最短的垃圾收集停頓時間可以為交互比較高的程序提高用戶體驗。 CMS工作機制相比其他的垃圾收集器來說更復雜,整個過程分為以下4個階段:
1.7.6.1. 初始標記
只是標記一下GC Roots能直接關聯的對象,速度很快,仍然需要暫停所有的工作線程。
1.7.6.2. 并發標記
進行GC Roots跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。
1.7.6.3. 重新標記
為了修正在并發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,仍然需要暫停所有的工作線程。
1.7.6.4. 并發清除
清除GC Roots不可達對象,和用戶線程一起工作,不需要暫停工作線程。由于耗時最長的并發標記和并發清除過程中,垃圾收集線程可以和用戶現在一起并發工作,所以總體上來看 CMS收集器的內存回收和用戶線程是一起并發地執行。 CMS收集器工作過程:
1.7.7. G1收集器
Garbage first垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與CMS收集器,G1收集器兩個最突出的改進是:
1. 基于標記-整理算法,不產生內存碎片。
2. 可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。
G1收集器避免全區域垃圾收集,它把堆內存劃分為大小固定的幾個獨立區域,并且跟蹤這些區域的垃圾收集進度,同時在后臺維護一個優先級列表,每次根據所允許的收集時間,優先回收垃圾最多的區域。區域劃分和優先級區域回收機制,確保G1收集器可以在有限時間獲得最高的垃圾收集效率。
1.8. JAVA IO/NIO
1.8.1. 阻塞IO模型
最傳統的一種IO模型,即在讀寫數據過程中會發生阻塞現象。當用戶線程發出IO請求之后,內核會去查看數據是否就緒,如果沒有就緒就會等待數據就緒,而用戶線程就會處于阻塞狀態,用戶線程交出CPU。當數據就緒之后,內核會將數據拷貝到用戶線程,并返回結果給用戶線程,用戶線程才解除block狀態。典型的阻塞IO模型的例子為:data = socket.read();如果數據沒有就緒,就會一直阻塞在read方法。
1.8.2. 非阻塞IO模型
當用戶線程發起一個read操作后,并不需要等待,而是馬上就得到了一個結果。如果結果是一個error時,它就知道數據還沒有準備好,于是它可以再次發送read操作。一旦內核中的數據準備好了,并且又再次收到了用戶線程的請求,那么它馬上就將數據拷貝到了用戶線程,然后返回。所以事實上,在非阻塞IO模型中,用戶線程需要不斷地詢問內核數據是否就緒,也就說非阻塞IO不會交出CPU,而會一直占用CPU。典型的非阻塞IO模型一般如下:
while(true){ data = socket.read(); if(data!= error){ 處理數據 break; } }
但是對于非阻塞IO就有一個非常嚴重的問題,在while循環中需要不斷地去詢問內核數據是否就緒,這樣會導致CPU占用率非常高,因此一般情況下很少使用while循環這種方式來讀取數據。
1.8.3. 多路復用IO模型
多路復用IO模型是目前使用得比較多的模型。Java NIO實際上就是多路復用IO。在多路復用IO模型中,會有一個線程不斷去輪詢多個socket的狀態,只有當socket真正有讀寫事件時,才真正調用實際的IO讀寫操作。因為在多路復用IO模型中,只需要使用一個線程就可以管理多個socket,系統不需要建立新的進程或者線程,也不必維護這些線程和進程,并且只有在真正有socket讀寫事件進行時,才會使用IO資源,所以它大大減少了資源占用。在Java NIO中,是通過selector.select()去查詢每個通道是否有到達事件,如果沒有事件,則一直阻塞在那里,因此這種方式會導致用戶線程的阻塞。多路復用IO模式,通過一個線程就可以管理多個socket,只有當socket真正有讀寫事件發生才會占用資源來進行實際的讀寫操作。因此,多路復用IO比較適合連接數比較多的情況。 另外多路復用IO為何比非阻塞IO模型的效率高是因為在非阻塞IO中,不斷地詢問socket狀態時通過用戶線程去進行的,而在多路復用IO中,輪詢每個socket狀態是內核在進行的,這個效率要比用戶線程要高的多。 不過要注意的是,多路復用IO模型是通過輪詢的方式來檢測是否有事件到達,并且對到達的事件逐一進行響應。因此對于多路復用IO模型來說,一旦事件響應體很大,那么就會導致后續的事件遲遲得不到處理,并且會影響新的事件輪詢。
1.8.4. 信號驅動IO模型
在信號驅動IO模型中,當用戶線程發起一個IO請求操作,會給對應的socket注冊一個信號函數,然后用戶線程會繼續執行,當內核數據就緒時會發送一個信號給用戶線程,用戶線程接收到信號之后,便在信號函數中調用IO讀寫操作來進行實際的IO請求操作。
1.8.5. 異步IO模型
異步IO模型才是最理想的IO模型,在異步IO模型中,當用戶線程發起read操作之后,立刻就可以開始去做其它的事。而另一方面,從內核的角度,當它受到一個asynchronous read之后,它會立刻返回,說明read請求已經成功發起了,因此不會對用戶線程產生任何block。然后,內核會等待數據準備完成,然后將數據拷貝到用戶線程,當這一切都完成之后,內核會給用戶線程發送一個信號,告訴它read操作完成了。也就說用戶線程完全不需要實際的整個IO操作是如何進行的,只需要先發起一個請求,當接收內核返回的成功信號時表示IO操作已經完成,可以直接去使用數據了。
也就說在異步IO模型中,IO操作的兩個階段都不會阻塞用戶線程,這兩個階段都是由內核自動完成,然后發送一個信號告知用戶線程操作已完成。用戶線程中不需要再次調用IO函數進行具體的讀寫。這點是和信號驅動模型有所不同的,在信號驅動模型中,當用戶線程接收到信號表示數據已經就緒,然后需要用戶線程調用IO函數進行實際的讀寫操作;而在異步IO模型中,收到信號表示IO操作已經完成,不需要再在用戶線程中調用IO函數進行實際的讀寫操作。 注意,異步IO是需要操作系統的底層支持,在Java 7中,提供了Asynchronous IO。
更多參考: http://www.importnew.com/19816.html
1.8.1. JAVA IO包
1.8.2. JAVA NIO
NIO主要有三大核心部分:Channel(通道),Buffer(緩沖區), Selector。傳統IO基于字節流和字符流進行操作,而NIO基于Channel和Buffer(緩沖區)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。Selector(選擇區)用于監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個線程可以監聽多個數據通道。
NIO和傳統IO之間第一個最大的區別是,IO是面向流的,NIO是面向緩沖區的。
1.8.2.1. NIO的緩沖區
Java IO面向流意味著每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前后移動流中的數據。如果需要前后移動從流中讀取的數據,需要先將它緩存到一個緩沖區。NIO的緩沖導向方法不同。數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩沖區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩沖區時,不要覆蓋緩沖區里尚未處理的數據。
1.8.2.2. NIO的非阻塞
IO的各種流是阻塞的。這意味著,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再干任何事情了。 NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什么都不會獲取。而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。 線程通常將非阻塞IO的空閑時間用于在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)
1.8.3. Channel
首先說一下Channel,國內大多翻譯成“通道”。Channel和IO中的Stream(流)是差不多一個等級的。只不過Stream是單向的,譬如:InputStream, OutputStream,而Channel是雙向的,既可以用來進行讀操作,又可以用來進行寫操作。 NIO中的Channel的主要實現有:
1. FileChannel
2. DatagramChannel
3. SocketChannel
4. ServerSocketChannel
這里看名字就可以猜出個所以然來:分別可以對應文件IO、UDP和TCP(Server和Client)。下面演示的案例基本上就是圍繞這4個類型的Channel進行陳述的。
1.8.4. Buffer
Buffer,故名思意,緩沖區,實際上是一個容器,是一個連續數組。Channel提供從文件、網絡讀取數據的渠道,但是讀取或寫入的數據都必須經由Buffer。
上面的圖描述了從一個客戶端向服務端發送數據,然后服務端接收數據的過程。客戶端發送數據時,必須先將數據存入Buffer中,然后將Buffer中的內容寫入通道。服務端這邊接收數據必須通過Channel將數據讀入到Buffer中,然后再從Buffer中取出數據來處理。 在NIO中,Buffer是一個頂層父類,它是一個抽象類,常用的Buffer的子類有:ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、ShortBuffer
1.8.5. Selector
Selector類是NIO的核心類,Selector能夠檢測多個注冊的通道上是否有事件發生,如果有事件發生,便獲取事件然后針對每個事件進行相應的響應處理。這樣一來,只是用一個單線程就可以管理多個通道,也就是管理多個連接。這樣使得只有在連接真正有讀寫事件發生時,才會調用函數來進行讀寫,就大大地減少了系統開銷,并且不必為每個連接都創建一個線程,不用去維護多個線程,并且避免了多線程之間的上下文切換導致的開銷。
1.9. JVM 類加載機制
JVM類加載機制分為五個部分:加載,驗證,準備,解析,初始化,下面我們就分別來看一下這五個過程。
1.9.1.1. 加載
加載是類加載過程中的一個階段,這個階段會在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的入口。注意這里不一定非得要從一個Class文件獲取,這里既可以從ZIP包中讀取(比如從jar包和war包中讀取),也可以在運行時計算生成(動態代理),也可以由其它文件生成(比如將JSP文件轉換成對應的Class類)。
1.9.1.2. 驗證
這一階段的主要目的是為了確保Class文件的字節流中包含的信息是否符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
1.9.1.3. 準備
準備階段是正式為類變量分配內存并設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間。注意這里所說的初始值概念,比如一個類變量定義為:
public static int v = 8080;
實際上變量v在準備階段過后的初始值為0而不是8080,將v賦值為8080的put static指令是程序被編譯后,存放于類構造器<client>方法之中。 但是注意如果聲明為:
public static final int v = 8080;
在編譯階段會為v生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將v賦值為8080。
1.9.1.4. 解析
解析階段是指虛擬機將常量池中的符號引用替換為直接引用的過程。符號引用就是class文件中的:
1. CONSTANT_Class_info
2. CONSTANT_Field_info
3. CONSTANT_Method_info 等類型的常量。
1.9.1.5. 符號引用
符號引用與虛擬機實現的布局無關,引用的目標并不一定要已經加載到內存中。各種虛擬機實現的內存布局可以各不相同,但是它們能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在Java虛擬機規范的Class文件格式中。
1.9.1.6. 直接引用
直接引用可以是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。
1.9.1.7. 初始化
初始化階段是類加載最后一個階段,前面的類加載階段之后,除了在加載階段可以自定義類加載器以外,其它操作都由JVM主導。到了初始階段,才開始真正執行類中定義的Java程序代碼。
1.9.1.8. 類構造器<client>
初始化階段是執行類構造器<client>方法的過程。<client>方法是由編譯器自動收集類中的類變量的賦值操作和靜態語句塊中的語句合并而成的。虛擬機會保證子<client>方法執行之前,父類的<client>方法已經執行完畢,如果一個類中沒有對靜態變量賦值也沒有靜態語句塊,那么編譯器可以不為這個類生成<client>()方法。 注意以下幾種情況不會執行類初始化: 1. 通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。 2. 定義對象數組,不會觸發該類的初始化。 3. 常量在編譯期間會存入調用類的常量池中,本質上并沒有直接引用定義常量的類,不會觸發定義常量所在的類。 4. 通過類名獲取Class對象,不會觸發類的初始化。 5. 通過Class.forName加載指定類時,如果指定參數initialize為false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。 6. 通過ClassLoader默認的loadClass方法,也不會觸發初始化動作。
1.9.2. 類加載器
虛擬機設計團隊把加載動作放到JVM外部實現,以便讓應用程序決定如何獲取所需的類,JVM提供了3種類加載器:
1.9.2.1. 啟動類加載器(Bootstrap ClassLoader)
1. 負責加載 JAVA_HOMElib 目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。
1.9.2.2. 擴展類加載器(Extension ClassLoader)
2. 負責加載 JAVA_HOMElibext 目錄中的,或通過java.ext.dirs系統變量指定路徑中的類庫。
1.9.2.3. 應用程序類加載器(Application ClassLoader):
3. 負責加載用戶路徑(classpath)上的類庫。 JVM通過雙親委派模型進行類的加載,當然我們也可以通過繼承java.lang.ClassLoader實現自定義的類加載器。
2.9.3. 雙親委派
當一個類收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父類去完成,每一個層次類加載器都是如此,因此所有的加載請求都應該傳送到啟動類加載其中,只有當父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的Class),子類加載器才會嘗試自己去加載。 采用雙親委派的一個好處是比如加載位于rt.jar包中的類java.lang.Object,不管是哪個加載器加載這個類,最終都是委托給頂層的啟動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個Object對象。
1.9.4. OSGI(動態模型系統) OSGi(Open Service Gateway Initiative),
是面向Java的動態模型系統,是Java動態化模塊化系統的一系列規范。
1.9.4.1. 動態改變構造
OSGi服務平臺提供在多種網絡設備上無需重啟的動態改變構造的功能。為了最小化耦合度和促使這些耦合度可管理,OSGi技術提供一種面向服務的架構,它能使這些組件動態地發現對方。
1.9.4.2. 模塊化編程與熱插拔
OSGi旨在為實現Java程序的模塊化編程提供基礎條件,基于OSGi的程序很可能可以實現模塊級的熱插拔功能,當程序升級更新時,可以只停用、重新安裝然后啟動程序的其中一部分,這對企業級程序開發來說是非常具有誘惑力的特性。 OSGi描繪了一個很美好的模塊化開發目標,而且定義了實現這個目標的所需要服務與架構,同時也有成熟的框架進行實現支持。但并非所有的應用都適合采用OSGi作為基礎架構,它在提供強大功能同時,也引入了額外的復雜度,因為它不遵守了類加載的雙親委托模型。