《JAVA核心知識》學習筆記(JVM)-1

JVMjava

(1) 基本概念:
JVM 是可運行 Java 代碼的假想計算機 ,包括一套字節碼指令集、一組寄存器、一個棧、
一個垃圾回收,堆 和 一個存儲方法域。 JVM 是運行在操做系統之上的,它與硬件沒有直接
的交互算法

Hotspot JVM 後臺運行的系統線程主要有下面幾個:數據庫

虛擬機線程
(VM thread)
這個線程等待 JVM 到達安全點操做出現。這些操做必需要在獨立的線程裏執行,由於當
堆修改沒法進行時,線程都須要 JVM 位於安全點。這些操做的類型有: stop-the
world 垃圾回收、線程棧 dump、線程暫停、線程偏向鎖(biased locking)解除。
週期性任務線程 這線程負責定時器事件(也就是中斷),用來調度週期性操做的執行。
GC 線程 這些線程支持 JVM 中不一樣的垃圾回收活動。
編譯器線程 這些線程在運行時將字節碼動態編譯成本地平臺相關的機器碼。
信號分發線程 這個線程接收發送到 JVM 的信號並調用適當的 JVM 方法處理。


JVM 內存區域主要分爲線程私有區域【程序計數器、虛擬機棧、本地方法區】、線程共享區
域【JAVA 堆、方法區】、直接內存。編程

   程序計數器(線程私有)
一塊較小的內存空間, 是當前線程所執行的字節碼的行號指示器,每條線程都要有一個獨立的
程序計數器,這類內存也稱爲「線程私有」 的內存。
正在執行 java 方法的話,計數器記錄的是虛擬機字節碼指令的地址(當前指令的地址) 。如
果仍是 Native 方法,則爲空。
這個內存區域是惟一一個在虛擬機中沒有規定任何 OutOfMemoryError 狀況的區域。數組

虛擬機棧(線程私有)
是描述java方法執行的內存模型,每一個方法在執行的同時都會建立一個棧幀(Stack Frame)
用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。 每個方法從調用直至執行完成
的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
棧幀( Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態連接
(Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。 棧幀隨着方法調用而創
13/04/2018 Page 23 of 283
建,隨着方法結束而銷燬——不管方法是正常完成仍是異常完成(拋出了在方法內未被捕獲的異
常)都算做方法結束
 緩存

本地方法區(線程私有)
本地方法區和 Java Stack 做用相似, 區別是虛擬機棧爲執行 Java 方法服務, 而本地方法棧則爲
Native 方法服務, 若是一個 VM 實現使用 C-linkage 模型來支持 Native 調用, 那麼該棧將會是一個
C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機棧合二爲一。
 安全

堆(Heap-線程共享) -運行時數據區
是被線程共享的一塊內存區域, 建立的對象和數組都保存在 Java 堆內存中,也是垃圾收集器進行
垃圾收集的最重要的內存區域。 因爲現代 VM 採用分代收集算法, 所以 Java 堆從 GC 的角度還能夠
細分爲: 新生代(Eden 區、 From Survivor 區和 To Survivor 區)和老年代。
 網絡

方法區/永久代(線程共享)
即咱們常說的永久代(Permanent Generation), 用於存儲被 JVM 加載的類信息、 常量、 靜
態變量、 即時編譯器編譯後的代碼等數據. HotSpot VM把GC分代收集擴展至方法區, 即便用Java
堆的永久代來實現方法區, 這樣 HotSpot 的垃圾收集器就能夠像管理 Java 堆同樣管理這部份內存,
而沒必要爲方法區開發專門的內存管理器(永久帶的內存回收的主要目標是針對常量池的回收和類型
的卸載, 所以收益通常很小)。
 數據結構

運行時常量池(Runtime Constant Pool)是方法區的一部分。 Class 文件中除了有類的版
本、字段、方法、接口等描述等信息外,還有一項信息是常量池
 多線程

JVM 運行時內存
Java 堆從 GC 的角度還能夠細分爲: 新生代(Eden 區、 From Survivor 區和 To Survivor 區)和老年
代。
新生代
是用來存放新生的對象。通常佔據堆的 1/3 空間。因爲頻繁建立對象,因此新生代會頻繁觸發
MinorGC 進行垃圾回收。新生代又分爲 Eden 區、 ServivorFrom、 ServivorTo 三個區
 

Eden 區
Java 新對象的出生地(若是新建立的對象佔用內存很大,則直接分配到老
年代)。當 Eden 區內存不夠的時候就會觸發 MinorGC,對新生代區進行
一次垃圾回收。
ServivorFrom
上一次 GC 的倖存者,做爲這一次 GC 的被掃描者。
ServivorTo
保留了一次 MinorGC 過程當中的倖存者。
MinorGC 的過程(複製->清空->互換)
MinorGC 採用複製算法。

eden、 servicorFrom 複製到 ServicorTo,年齡+1
首先,把 Eden 和 ServivorFrom 區域中存活的對象複製到 ServicorTo 區域(若是有對象的年
齡以及達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(若是 ServicorTo 不
夠位置了就放到老年區);
2: 清空 eden、 servicorFrom
而後,清空 Eden 和 ServicorFrom 中的對象;
3: ServicorTo 和 ServicorFrom 互換
最後, ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成爲下一次 GC 時的 ServicorFrom
區。
老年代
主要存放應用程序中生命週期長的內存對象。
老年代的對象比較穩定,因此 MajorGC 不會頻繁執行。在進行 MajorGC 前通常都先進行
了一次 MinorGC,使得有新生代的對象晉身入老年代,致使空間不夠用時才觸發。當沒法找到足
夠大的連續空間分配給新建立的較大對象時也會提早觸發一次 MajorGC 進行垃圾回收騰出空間。
MajorGC 採用標記清除算法:首先掃描一次全部老年代,標記出存活的對象,而後回收沒
有標記的對象。 MajorGC 的耗時比較長,由於要掃描再回收。 MajorGC 會產生內存碎片,爲了減
少內存損耗,咱們通常須要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的
時候,就會拋出 OOM(Out of Memory)
 

永久代
指內存的永久保存區域,主要存放 Class 和 Meta(元數據)的信息,Class 在被加載的時候被
放入永久區域, 它和和存放實例的區域不一樣,GC 不會在主程序運行期對永久區域進行清理。因此這
也致使了永久代的區域會隨着加載的 Class 的增多而脹滿,最終拋出 OOM 異常。
 

JAVA8 與元數據
在 Java8 中, 永久代已經被移除,被一個稱爲「元數據區」(元空間)的區域所取代。元空間
的本質和永久代相似,元空間與永久代之間最大的區別在於: 元空間並不在虛擬機中,而是使用
本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制。 類的元數據放入 native
memory, 字符串池和類的靜態變量放入 java 堆中, 這樣能夠加載多少類的元數據就再也不由
MaxPermSize 控制, 而由系統的實際可用空間來控制。
 

 

 

 

 

線程私有數據區域生命週期與線程相同, 依賴用戶線程的啓動/結束 而 建立/銷燬(在 Hotspot
VM 內, 每一個線程都與操做系統的本地線程直接映射, 所以這部份內存區域的存/否跟隨本地線程的
生/死對應)。

線程共享區域隨虛擬機的啓動/關閉而建立/銷燬。


直接內存並非 JVM 運行時數據區的一部分, 但也會被頻繁的使用: 在 JDK 1.4 引入的 NIO 提
供了基於 Channel 與 Buffer 的 IO 方式, 它可使用 Native 函數庫直接分配堆外內存, 而後使用
DirectByteBuffer 對象做爲這塊內存的引用進行操做(詳見: Java I/O 擴展), 這樣就避免了在 Java
堆和 Native 堆中來回複製數據, 所以在一些場景中能夠顯著提升性能

 

 

 

 

2.4.垃圾回收與算法
2.4.1. 如何肯定垃圾
2.4.1.1. 引用計數法
在 Java 中,引用和對象是有關聯的。若是要操做對象則必須用引用進行。所以,很顯然一個簡單
的辦法是經過引用計數來判斷一個對象是否能夠回收。簡單說,即一個對象若是沒有任何與之關
聯的引用, 即他們的引用計數都不爲 0, 則說明對象不太可能再被用到,那麼這個對象就是可回收
對象。

可達性分析
爲了解決引用計數法的循環引用問題, Java 使用了可達性分析的方法。經過一系列的「GC roots」
對象做爲起點搜索。若是在「GC roots」和一個對象之間沒有可達路徑,則稱該對象是不可達的。
13/04/2018 Page 27 of 283
要注意的是,不可達對象不等價於可回收對象, 不可達對象變爲可回收對象至少要通過兩次標記
過程。兩次標記後仍然是可回收對象,則將面臨回收
 

標記清除算法(Mark-Sweep)
最基礎的垃圾回收算法,分爲兩個階段,標註和清除。標記階段標記出全部須要回收的對象,清
除階段回收被標記的對象所佔用的空間。該算法最大的問題是內存碎片化嚴重,後續可能發生大對象不能找到可
利用空間的問題。

2.4.3. 複製算法(copying)
爲了解決 Mark-Sweep 算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分爲等大小
的兩塊。每次只使用其中一塊,當這一塊內存滿後將尚存活的對象複製到另外一塊上去,把已使用
的內存清掉,如圖
這種算法雖然實現簡單,內存效率高,不易產生碎片,可是最大的問題是可用內存被壓縮到了原
本的一半。且存活對象增多的話, Copying 算法的效率會大大下降
 

2.4.4. 標記整理算法(Mark-Compact)
結合了以上兩個算法,爲了不缺陷而提出。標記階段和 Mark-Sweep 算法相同, 標記後不是清
理對象,而是將存活對象移向內存的一端。而後清除端邊界外的對象。

2.4.5. 分代收集算法
分代收集法是目前大部分 JVM 所採用的方法,其核心思想是根據對象存活的不一樣生命週期將內存
劃分爲不一樣的域,通常狀況下將 GC 堆劃分爲老生代(Tenured/Old Generation)和新生代(Young
Generation)。老生代的特色是每次垃圾回收時只有少許對象須要被回收,新生代的特色是每次垃
圾回收時都有大量垃圾須要被回收,所以能夠根據不一樣區域選擇不一樣的算法
 

2.4.5.1. 新生代與複製算法
目前大部分 JVM 的 GC 對於新生代都採起 Copying 算法,由於新生代中每次垃圾回收都要
回收大部分對象,即要複製的操做比較少,但一般並非按照 1: 1 來劃分新生代。通常將新生代
劃分爲一塊較大的 Eden 空間和兩個較小的 Survivor 空間(From Space, To Space),每次使用
Eden 空間和其中的一塊 Survivor 空間,當進行回收時,將該兩塊空間中還存活的對象複製到另
一塊 Survivor 空間中。
 

2.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 的對象會被
移到老生代中。
 

 

 

2.5.JAVA 四中引用類型
2.5.1. 強引用
在 Java 中最多見的就是強引用, 把一個對象賦給一個引用變量,這個引用變量就是一個強引
用。當一個對象被強引用變量引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即
使該對象之後永遠都不會被用到 JVM 也不會回收。所以強引用是形成 Java 內存泄漏的主要緣由之
一。Object o=new Object();

2.5.2. 軟引用
軟引用須要用 SoftReference 類來實現,對於只有軟引用的對象來講,當系統內存足夠時它
不會被回收,當系統內存空間不足時它會被回收。軟引用一般用在對內存敏感的程序中。

2.5.3. 弱引用
弱引用須要用 WeakReference 類來實現,它比軟引用的生存期更短,對於只有弱引用的對象
來講,只要垃圾回收機制一運行,無論 JVM 的內存空間是否足夠,總會回收該對象佔用的內存。
 

虛引用須要 PhantomReference 類來實現,它不能單獨使用,必須和引用隊列聯合使用。 虛
引用的主要做用是跟蹤對象被垃圾回收的狀態。
 

2.6.GC 分代收集算法 VS 分區收集算法
 

分代收集算法
當前主流 VM 垃圾收集都採用」分代收集」 (Generational Collection)算法, 這種算法會根據
對象存活週期的不一樣將內存劃分爲幾塊, 如 JVM 中的 新生代、老年代、永久代, 這樣就能夠根據
各年代特色分別採用最適當的 GC 算法
2.6.1.1. 在新生代-複製算法
每次垃圾收集都能發現大批對象已死, 只有少許存活. 所以選用複製算法, 只須要付出少許
存活對象的複製成本就能夠完成收集


2.6.1.2. 在老年代-標記整理算法
由於對象存活率高、沒有額外空間對它進行分配擔保, 就必須採用「標記—清理」或「標
記—整理」 算法來進行回收, 沒必要進行內存複製, 且直接騰出空閒內存
 

2.6.2. 分區收集算法
分區算法則將整個堆空間劃分爲連續的不一樣小區間, 每一個小區間獨立使用, 獨立回收. 這樣作的
好處是能夠控制一次回收多少個小區間 , 根據目標停頓時間, 每次合理地回收若干個小區間(而不是
整個堆), 從而減小一次 GC 所產生的停頓。
 

 

2.7.GC 垃圾收集器
Java 堆內存被劃分爲新生代和年老代兩部分,新生代主要使用複製和標記-清除垃圾回收算法;
年老代主要使用標記-整理垃圾回收算法,所以 java 虛擬中針對新生代和年老代分別提供了多種不
同的垃圾收集器, JDK1.6 中 Sun HotSpot 虛擬機的垃圾收集器以下:


2.7.1. Serial 垃圾收集器(單線程、 複製算法)
Serial(英文連續) 是最基本垃圾收集器,使用複製算法,曾經是JDK1.3.1 以前新生代惟一的垃圾
收集器。 Serial 是一個單線程的收集器,它不但只會使用一個 CPU 或一條線程去完成垃圾收集工
做,而且在進行垃圾收集的同時,必須暫停其餘全部的工做線程,直到垃圾收集結束。
Serial 垃圾收集器雖然在收集垃圾過程當中須要暫停全部其餘的工做線程,可是它簡單高效,對於限
定單個 CPU 環境來講,沒有線程交互的開銷,能夠得到最高的單線程垃圾收集效率,所以 Serial
垃圾收集器依然是 java 虛擬機運行在 Client 模式下默認的新生代垃圾收集器
 

2.7.2. ParNew 垃圾收集器(Serial+多線程)
ParNew 垃圾收集器實際上是 Serial 收集器的多線程版本,也使用複製算法,除了使用多線程進行垃
圾收集以外,其他的行爲和 Serial 收集器徹底同樣, ParNew 垃圾收集器在垃圾收集過程當中一樣也
要暫停全部其餘的工做線程
 

ParNew 收集器默認開啓和 CPU 數目相同的線程數,能夠經過-XX:ParallelGCThreads 參數來限
制垃圾收集器的線程數。 【Parallel:平行的】
ParNew 雖然是除了多線程外和Serial 收集器幾乎徹底同樣,可是ParNew垃圾收集器是不少 java
虛擬機運行在 Server 模式下新生代的默認垃圾收集器。
 

2.7.3. Parallel Scavenge 收集器(多線程複製算法、高效)
Parallel Scavenge 收集器也是一個新生代垃圾收集器,一樣使用複製算法,也是一個多線程的垃
圾收集器, 它重點關注的是程序達到一個可控制的吞吐量(Thoughput, CPU 用於運行用戶代碼
的時間/CPU 總消耗時間,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)),
高吞吐量能夠最高效率地利用 CPU 時間,儘快地完成程序的運算任務,主要適用於在後臺運算而
不須要太多交互的任務。 自適應調節策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個
重要區別
 

2.7.4. Serial Old 收集器(單線程標記整理算法 )
Serial Old 是 Serial 垃圾收集器年老代版本,它一樣是個單線程的收集器,使用標記-整理算法,
這個收集器也主要是運行在 Client 默認的 java 虛擬機默認的年老代垃圾收集器。
在 Server 模式下,主要有兩個用途:
1. 在 JDK1.5 以前版本中與新生代的 Parallel Scavenge 收集器搭配使用。
2. 做爲年老代中使用 CMS 收集器的後備垃圾收集方案。
 

2.7.5. Parallel Old 收集器(多線程標記整理算法)
Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多線程的標記-整理算法,在 JDK1.6
纔開始提供。
在 JDK1.6 以前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只
能保證新生代的吞吐量優先,沒法保證總體的吞吐量, Parallel Old 正是爲了在年老代一樣提供吞
吐量優先的垃圾收集器, 若是系統對吞吐量要求比較高,能夠優先考慮新生代 Parallel Scavenge
和年老代 Parallel Old 收集器的搭配策略。
 

2.7.6. CMS 收集器(多線程標記清除算法)
Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾
回收停頓時間, 和其餘年老代使用標記-整理算法不一樣,它使用多線程的標記-清除算法。
最短的垃圾收集停頓時間能夠爲交互比較高的程序提升用戶體驗。
CMS 工做機制相比其餘的垃圾收集器來講更復雜,整個過程分爲如下 4 個階段:
 

初始標記
只是標記一下 GC Roots 能直接關聯的對象,速度很快,仍然須要暫停全部的工做線程。
 

併發標記
進行 GC Roots 跟蹤的過程,和用戶線程一塊兒工做,不須要暫停工做線程
 

從新標記
爲了修正在併發標記期間,因用戶程序繼續運行而致使標記產生變更的那一部分對象的標記
記錄,仍然須要暫停全部的工做線程。
 

併發清除
清除 GC Roots 不可達對象,和用戶線程一塊兒工做,不須要暫停工做線程。因爲耗時最長的並
發標記和併發清除過程當中,垃圾收集線程能夠和用戶如今一塊兒併發工做, 因此整體上來看
CMS 收集器的內存回收和用戶線程是一塊兒併發地執行。
 

2.7.7. G1 收集器
Garbage first 垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與 CMS 收集器, G1 收
集器兩個最突出的改進是:
1. 基於標記-整理算法,不產生內存碎片。
2. 能夠很是精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。
G1 收集器避免全區域垃圾收集,它把堆內存劃分爲大小固定的幾個獨立區域,而且跟蹤這些區域
的垃圾收集進度,同時在後臺維護一個優先級列表,每次根據所容許的收集時間, 優先回收垃圾
最多的區域。區域劃分和優先級區域回收機制,確保 G1 收集器能夠在有限時間得到最高的垃圾收
集效率。
 

2.8. JAVA IO/NIO
2.8.1. 阻塞 IO 模型
最傳統的一種 IO 模型,即在讀寫數據過程當中會發生阻塞現象。當用戶線程發出 IO 請求以後,內
核會去查看數據是否就緒,若是沒有就緒就會等待數據就緒,而用戶線程就會處於阻塞狀態,用
戶線程交出 CPU。當數據就緒以後,內核會將數據拷貝到用戶線程,並返回結果給用戶線程,用
13/04/2018 Page 35 of 283
戶線程才解除 block 狀態。典型的阻塞 IO 模型的例子爲: data = socket.read();若是數據沒有就
緒,就會一直阻塞在 read 方法。
 

2.8.2. 非阻塞 IO 模型
當用戶線程發起一個 read 操做後,並不須要等待,而是立刻就獲得了一個結果。 若是結果是一個
error 時,它就知道數據尚未準備好,因而它能夠再次發送 read 操做。一旦內核中的數據準備
好了,而且又再次收到了用戶線程的請求,那麼它立刻就將數據拷貝到了用戶線程,而後返回。
因此事實上,在非阻塞 IO 模型中,用戶線程須要不斷地詢問內核數據是否就緒,也就說非阻塞 IO
不會交出 CPU,而會一直佔用 CPU。 典型的非阻塞 IO 模型通常以下

可是對於非阻塞 IO 就有一個很是嚴重的問題, 在 while 循環中須要不斷地去詢問內核數據是否就
緒,這樣會致使 CPU 佔用率很是高,所以通常狀況下不多使用 while 循環這種方式來讀取數據
 

2.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 模型來講, 一旦事件響應體很大,那麼就會致使後續的事件
遲遲得不處處理,而且會影響新的事件輪詢
不過要注意的是,多路複用 IO 模型是經過輪詢的方式來檢測是否有事件到達,而且對到達的事件
逐一進行響應。所以對於多路複用 IO 模型來講, 一旦事件響應體很大,那麼就會致使後續的事件
遲遲得不處處理,而且會影響新的事件輪詢


2.8.4. 信號驅動 IO 模型
在信號驅動 IO 模型中,當用戶線程發起一個 IO 請求操做,會給對應的 socket 註冊一個信號函
數,而後用戶線程會繼續執行,當內核數據就緒時會發送一個信號給用戶線程,用戶線程接收到
信號以後,便在信號函數中調用 IO 讀寫操做來進行實際的 IO 請求操做。

 

2.8.5. 異步 IO 模型
異步 IO 模型纔是最理想的 IO 模型,在異步 IO 模型中,當用戶線程發起 read 操做以後,馬上就
能夠開始去作其它的事。而另外一方面,從內核的角度,當它受到一個 asynchronous read 以後,
它會馬上返回,說明 read 請求已經成功發起了,所以不會對用戶線程產生任何 block。而後,內
核會等待數據準備完成,而後將數據拷貝到用戶線程,當這一切都完成以後,內核會給用戶線程
發送一個信號,告訴它 read 操做完成了。也就說用戶線程徹底不須要實際的整個 IO 操做是如何
進行的, 只須要先發起一個請求,當接收內核返回的成功信號時表示 IO 操做已經完成,能夠直接
去使用數據了。
也就說在異步 IO 模型中, IO 操做的兩個階段都不會阻塞用戶線程,這兩個階段都是由內核自動完
成,而後發送一個信號告知用戶線程操做已完成。用戶線程中不須要再次調用 IO 函數進行具體的
讀寫。這點是和信號驅動模型有所不一樣的,在信號驅動模型中,當用戶線程接收到信號表示數據
已經就緒,而後須要用戶線程調用 IO 函數進行實際的讀寫操做;而在異步 IO 模型中,收到信號
表示 IO 操做已經完成,不須要再在用戶線程中調用 IO 函數進行實際的讀寫操做。
 

2.8.1. JAVA IO 包

字節流與字符流

我本身網上看到的:

https://www.jianshu.com/p/828051b6a50f

問:字節流與字符流有什麼區別?

答:計算機中的一切最終都是以二進制字節形式存在的,對於咱們常常操做的字符串,在寫入時其實都是先獲得了其對應的字節,而後將字節寫入到輸出流,在讀取時其實都是先讀到的是字節,而後將字節直接使用或者轉換爲字符給咱們使用。因爲字節和字符兩種操做的需求比較普遍,因此 Java 專門提供了字符流與字節流相關 IO 類。

對於程序運行的底層設備來講永遠都只接受字節數據,因此當咱們往設備寫數據時不管是字節仍是字符最終都是寫的字節流。字符流是字節流的包裝類,因此當咱們將字符流向字節流轉換時要注意編碼問題(由於字符串轉成字節數組的實質是轉成該字符串的某種字節編碼)。

字符流和字節流的使用很是類似,可是實際上字節流的操做不會通過緩衝區(內存)而是直接操做文本自己的,而字符流的操做會先通過緩衝區(內存)而後經過緩衝區再操做文件。

問:什麼是緩衝區?有什麼做用?

答:緩衝區就是一段特殊的內存區域,不少狀況下當程序須要頻繁地操做一個資源(如文件或數據庫)則性能會很低,因此爲了提高性能就能夠將一部分數據暫時讀寫到緩存區,之後直接今後區域中讀寫數據便可,這樣就顯著提高了性能。

對於 Java 字符流的操做都是在緩衝區操做的,因此若是咱們想在字符流操做中主動將緩衝區刷新到文件則可使用 flush() 方法操做。

問:字節流和字符流哪一個好?怎麼選擇?

答:大多數狀況下使用字節流會更好,由於字符流是字節流的包裝,而大多數時候 IO 操做都是直接操做磁盤文件,因此這些流在傳輸時都是以字節的方式進行的(圖片等都是按字節存儲的)。

而若是對於操做須要經過 IO 在內存中頻繁處理字符串的狀況使用字符流會好些,由於字符流具有緩衝區,提升了性能。

 

 

2.8.2. JAVA NIO
NIO 主要有三大核心部分: Channel(通道), Buffer(緩衝區), Selector。傳統 IO 基於字節流和字
符流進行操做, 而 NIO 基於 Channel 和 Buffer(緩衝區)進行操做,數據老是從通道讀取到緩衝區
中,或者從緩衝區寫入到通道中。 Selector(選擇區)用於監聽多個通道的事件(好比:鏈接打開,
數據到達)。所以,單個線程能夠監聽多個數據通道。

 

 

NIO 的緩衝區
Java IO 面向流意味着每次從流中讀一個或多個字節,直至讀取全部字節,它們沒有被緩存在任何
地方。此外,它不能先後移動流中的數據。若是須要先後移動從流中讀取的數據, 須要先將它緩
存到一個緩衝區。 NIO 的緩衝導向方法不一樣。數據讀取到一個它稍後處理的緩衝區,須要時可在
緩衝區中先後移動。這就增長了處理過程當中的靈活性。可是,還須要檢查是否該緩衝區中包含所
有您須要處理的數據。並且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏還沒有處理的
數據
 

NIO 的非阻塞
IO 的各類流是阻塞的。這意味着,當一個線程調用 read() 或 write()時,該線程被阻塞,直到有
一些數據被讀取,或數據徹底寫入。該線程在此期間不能再幹任何事情了。 NIO 的非阻塞模式,
使一個線程從某通道發送請求讀取數據,可是它僅能獲得目前可用的數據,若是目前沒有數據可
用時,就什麼都不會獲取。而不是保持線程阻塞,因此直至數據變的能夠讀取以前,該線程能夠
繼續作其餘的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不須要等待它
徹底寫入,這個線程同時能夠去作別的事情。 線程一般將非阻塞 IO 的空閒時間用於在其它通道上
執行 IO 操做,因此一個單獨的線程如今能夠管理多個輸入和輸出通道(channel)。
 

2.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 進行陳述的。
 

2.8.4. Buffer
Buffer,故名思意, 緩衝區,其實是一個容器,是一個連續數組。 Channel 提供從文件、
網絡讀取數據的渠道,可是讀取或寫入的數據都必須經由 Buffer。

 

 

上面的圖描述了從一個客戶端向服務端發送數據,而後服務端接收數據的過程。客戶端發送
數據時,必須先將數據存入 Buffer 中,而後將 Buffer 中的內容寫入通道。服務端這邊接收數據必
須經過 Channel 將數據讀入到 Buffer 中,而後再從 Buffer 中取出數據來處理。
在 NIO 中, Buffer 是一個頂層父類,它是一個抽象類,經常使用的 Buffer 的子類有:
ByteBuffer、 IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、 FloatBuffer、
ShortBuffer
 

2.8.5. Selector
Selector 類是 NIO 的核心類, Selector 可以檢測多個註冊的通道上是否有事件發生,若是有事
件發生,便獲取事件而後針對每一個事件進行相應的響應處理。這樣一來,只是用一個單線程就可
以管理多個通道,也就是管理多個鏈接。這樣使得只有在鏈接真正有讀寫事件發生時,纔會調用
函數來進行讀寫,就大大地減小了系統開銷,而且沒必要爲每一個鏈接都建立一個線程,不用去維護
多個線程,而且避免了多線程之間的上下文切換致使的開銷。
 

2.9.JVM 類加載機制

JVM 類加載機制分爲五個部分:加載,驗證,準備,解析,初始化,下面咱們就分別來看一下這
五個過程。

 

 

加載是類加載過程當中的一個階段, 這個階段會在內存中生成一個表明這個類的 java.lang.Class 對
象, 做爲方法區這個類的各類數據的入口。注意這裏不必定非得要從一個 Class 文件獲取,這裏既
能夠從 ZIP 包中讀取(好比從 jar 包和 war 包中讀取),也能夠在運行時計算生成(動態代理),
也能夠由其它文件生成(好比將 JSP 文件轉換成對應的 Class 類)
 

2.9.1.2. 驗證
這一階段的主要目的是爲了確保 Class 文件的字節流中包含的信息是否符合當前虛擬機的要求,並
且不會危害虛擬機自身的安全。
 

準備
準備階段是正式爲類變量分配內存並設置類變量的初始值階段,即在方法區中分配這些變量所使
用的內存空間。注意這裏所說的初始值概念,好比一個類變量定義爲:

public static int v = 8080;

實際上變量 v 在準備階段事後的初始值爲 0 而不是 8080, 將 v 賦值爲 8080 的 put static 指令是
程序被編譯後, 存放於類構造器<client>方法之中。
可是注意若是聲明爲:

public static final int v = 8080;

在編譯階段會爲 v 生成 ConstantValue 屬性,在準備階段虛擬機會根據 ConstantValue 屬性將 v
賦值爲 8080。

2.9.1.4. 解析
解析階段是指虛擬機將常量池中的符號引用替換爲直接引用的過程。符號引用就是 class 文件中
的:

CONSTANT_Class_info
2. CONSTANT_Field_info
3. CONSTANT_Method_info
等類型的常量。
 

2.9.1.5. 符號引用
 符號引用與虛擬機實現的佈局無關, 引用的目標並不必定要已經加載到內存中。 各類虛擬
機實現的內存佈局能夠各不相同,可是它們能接受的符號引用必須是一致的,由於符號引
用的字面量形式明肯定義在 Java 虛擬機規範的 Class 文件格式中。
 

直接引用
 直接引用能夠是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。若是有
了直接引用,那引用的目標一定已經在內存中存在。
 

2.9.1.7. 初始化
初始化階段是類加載最後一個階段,前面的類加載階段以後,除了在加載階段能夠自定義類加載
器之外,其它操做都由 JVM 主導。到了初始階段,纔開始真正執行類中定義的 Java 程序代碼。
 

2.9.1.8. 類構造器<client>
初始化階段是執行類構造器<client>方法的過程。 <client>方法是由編譯器自動收集類中的類變
量的賦值操做和靜態語句塊中的語句合併而成的。虛擬機會保證子<client>方法執行以前,父類
的<client>方法已經執行完畢, 若是一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯
器能夠不爲這個類生成<client>()方法。
注意如下幾種狀況不會執行類初始化:
1. 經過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
2. 定義對象數組,不會觸發該類的初始化。
3. 常量在編譯期間會存入調用類的常量池中,本質上並無直接引用定義常量的類,不會觸
發定義常量所在的類。
4. 經過類名獲取 Class 對象,不會觸發類的初始化。
5. 經過 Class.forName 加載指定類時,若是指定參數 initialize 爲 false 時,也不會觸發類初
始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
6. 經過 ClassLoader 默認的 loadClass 方法,也不會觸發初始化動做。
 

2.9.2. 類加載器
虛擬機設計團隊把加載動做放到 JVM 外部實現,以便讓應用程序決定如何獲取所需的類, JVM 提
供了 3 種類加載器:
 

2.9.2.1. 啓動類加載器(Bootstrap ClassLoader)
1. 負責加載 JAVA_HOME\lib 目錄中的, 或經過-Xbootclasspath 參數指定路徑中的, 且被
虛擬機承認(按文件名識別, 如 rt.jar) 的類。

 

2.9.2.2. 擴展類加載器(Extension ClassLoader)
2. 負責加載 JAVA_HOME\lib\ext 目錄中的,或經過 java.ext.dirs 系統變量指定路徑中的類
庫。
2.9.2.3. 應用程序類加載器(Application ClassLoader):
3. 負責加載用戶路徑(classpath)上的類庫。
JVM 經過雙親委派模型進行類的加載, 固然咱們也能夠經過繼承 java.lang.ClassLoader
實現自定義的類加載器。

2.9.3. 雙親委派
當一個類收到了類加載請求,他首先不會嘗試本身去加載這個類,而是把這個請求委派給父
類去完成,每個層次類加載器都是如此,所以全部的加載請求都應該傳送到啓動類加載其中,
只有當父類加載器反饋本身沒法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的
Class), 子類加載器纔會嘗試本身去加載。
採用雙親委派的一個好處是好比加載位於 rt.jar 包中的類 java.lang.Object,無論是哪一個加載
器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不一樣的類加載
器最終獲得的都是一樣一個 Object 對象。

 

2.9.4. OSGI( 動態模型系統)
OSGi(Open Service Gateway Initiative),是面向 Java 的動態模型系統,是 Java 動態化模塊化系
統的一系列規範。
OSGi(Open Service Gateway Initiative),是面向 Java 的動態模型系統,是 Java 動態化模塊化系
統的一系列規範。
2.9.4.1. 動態改變構造
OSGi 服務平臺提供在多種網絡設備上無需重啓的動態改變構造的功能。爲了最小化耦合度和促使
這些耦合度可管理, OSGi 技術提供一種面向服務的架構,它能使這些組件動態地發現對方。
 

2.9.4.2. 模塊化編程與熱插拔 OSGi 旨在爲實現 Java 程序的模塊化編程提供基礎條件,基於 OSGi 的程序極可能能夠實現模塊級 的熱插拔功能,當程序升級更新時,能夠只停用、從新安裝而後啓動程序的其中一部分,這對企 業級程序開發來講是很是具備誘惑力的特性。 OSGi 描繪了一個很美好的模塊化開發目標,並且定義了實現這個目標的所須要服務與架構,同時 也有成熟的框架進行實現支持。但並不是全部的應用都適合採用 OSGi 做爲基礎架構,它在提供強大 功能同時,也引入了額外的複雜度,由於它不遵照了類加載的雙親委託模型。  

相關文章
相關標籤/搜索