Java學習———你需要知道的Java虛擬機知識點(JVM)整理詳細

聲明:此篇博文是博主在網上閱讀大量文章,博文後。參考資料後整理總結的!文中涉及到的圖片來自網上,如有侵權部分,聯繫博主刪除!

目錄

JVM

1.基本概念:

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

下圖是粗略的JVM邏輯圖:
在這裏插入圖片描述

2. 運行過程:

我們都知道Java源文件,通過編譯器,能夠生產相應的.Class文件,也就是字節碼文件,而字節碼文件又通過Java虛擬機中的解釋器,編譯成特定機器上的機器碼 。

也就是如下:

① Java源文件—->編譯器—->字節碼文件

② 字節碼文件—->JVM—->機器碼

每一種平臺的解釋器是不同的,但是實現的虛擬機是相同的,這也就是Java爲什麼能夠跨平臺的原因了 ,當一個程序從開始運行,這時虛擬機就開始實例化了,多個程序啓動就會存在多個虛擬機實例。程序退出或者關閉,則虛擬機實例消亡,多個虛擬機實例之間數據不能共享。

在這裏插入圖片描述

3. 線程

這裏所說的線程指程序執行過程中的一個線程實體。JVM 允許一個應用併發執行多個線程。Hotspot JVM 中的 Java 線程與原生操作系統線程有直接的映射關係。當線程本地存儲、緩衝區分配、同步對象、棧、程序計數器等準備好以後,就會創建一個操作系統原生線程。Java 線程結束,原生線程隨之被回收。操作系統負責調度所有線程,並把它們分配到任何可用的 CPU 上。當原生線程初始化完畢,就會調用 Java 線程的 run() 方法。當線程結束時,會釋放原生線程和 Java 線程的所有資源。

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

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

4. JVM內存區域

在這裏插入圖片描述

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

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

線程共享區域隨虛擬機的啓動/關閉而創建/銷燬。
直接內存並不是JVM運行時數據區的一部分, 但也會被頻繁的使用: 在JDK 1.4引入的NIO提供了基於Channel與Buffer的IO方式, 它可以使用Native函數庫直接分配堆外內存, 然後使用DirectByteBuffer對象作爲這塊內存的引用進行操作(詳見: Java I/O 擴展), 這樣就避免了在Java堆和Native堆中來回複製數據, 因此在一些場景中可以顯著提高性能。
在這裏插入圖片描述

4.1. 程序計數器 ( 線程私有 )

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

4.2. 虛擬機棧 ( 線程私有 )

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

在這裏插入圖片描述

4.3. 本地方法區 ( 線程私有 )

本地方法區和Java Stack作用類似, 區別是虛擬機棧爲執行Java方法服務, 而本地方法棧則爲Native方法服務, 如果一個VM實現使用C-linkage模型來支持Native調用, 那麼該棧將會是一個C棧,但HotSpot VM直接就把本地方法棧和虛擬機棧合二爲一。

4.4. 堆( Heap- 線程共享) - 運行時數據區

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

4.5. 方法區 / 永久代(線程共享)

即我們常說的 永久代(Permanent Generation) , 用於存儲 被JVM加載的類信息常量靜態變量即時編譯器編譯後的代碼 等數據. HotSpot VM把GC分代收集擴展至方法區, 即 使用Java堆的永久代來實現方法區 , 這樣HotSpot的垃圾收集器就可以像管理Java堆一樣管理這部分內存,而不必爲方法區開發專門的內存管理器(永久帶的內存回收的主要目標是針對 常量池的回收類型的卸載 , 因此收益一般很小)。
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。 Java虛擬機對Class文件的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用於存儲哪種數據都必須符合規範上的要求,這樣纔會被虛擬機認可、裝載和執行。

5. JVM運行時內存

Java堆從GC的角度還可以細分爲: 新生代 (Eden區、From Survivor區和To Survivor區)和 老年代。

在這裏插入圖片描述

5.1. 新生代

是用來存放新生的對象。一般佔據堆的1/3空間。由於頻繁創建對象,所以新生代會頻繁觸發MinorGC進行垃圾回收。新生代又分爲 Eden,ServivorFrom、ServivorTo三個區。

5.1.1. Eden區

Java新對象的出生地(如果新創建的對象佔用內存很大,則直接分配到老
年代)。當Eden區內存不夠的時候就會觸發MinorGC,對新生代區進行
一次垃圾回收。

5.1.2. ServivorFrom

上一次GC的倖存者,作爲這一次GC的被掃描者。

5.1.3. ServivorTo

保留了一次MinorGC過程中的倖存者。

5.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區。

5.2. 老年代

主要存放應用程序中生命週期長的內存對象。老年代的對象比較穩定,所以MajorGC不會頻繁執行。在進行MajorGC前一般都先進行了一次MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次MajorGC進行垃圾回收騰出空間。

MajorGC採用標記清除算法:首先掃描一次所有老年代,標記出存活的對象,然後回收沒有標記的對象。MajorGC的耗時比較長,因爲要掃描再回收。MajorGC會產生內存碎片,爲了減少內存損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出OOM(Out of Memory)異常。

5.3. 永久代

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

5.3.1. JAVA8與元數據

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

6. 垃圾回收與算法

在這裏插入圖片描述

6.1. 如何確定垃圾

6.1.1. 引用計數法

在Java中,引用和對象是有關聯的。如果要操作對象則必須用引用進行。因此,很顯然一個簡單的辦法是通過引用計數來判斷一個對象是否可以回收。簡單說,即一個對象如果沒有任何與之關聯的引用,即他們的引用計數都不爲 0 ,則說明對象不太可能再被用到,那麼這個對象就是可回收對象。

6.1.2. 可達性分析

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

6.2. 標記清除算法(Mark-Sweep)

最基礎的垃圾回收算法,分爲兩個階段,標註和清除。標記階段標記出所有需要回收的對象,清除階段回收被標記的對象所佔用的空間。
如下圖:
在這裏插入圖片描述
從圖中我們就可以發現,該算法最大的問題是內存碎片化嚴重,後續可能發生大對象不能找到可利用空間的問題。

6.3. 複製算法( copying )

爲了解決Mark-Sweep算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分爲等大小的兩塊。每次只使用其中一塊,當這一塊內存滿後將尚存活的對象複製到另一塊上去,把已使用的內存清掉,
如圖:
在這裏插入圖片描述

這種算法雖然實現簡單,內存效率高,不易產生碎片,但是最大的問題是可用內存被壓縮到了原
本的一半。且存活對象增多的話,Copying算法的效率會大大降低。

6.4. 標記整理算法 (Mark-Compact)

結合了以上兩個算法,爲了避免缺陷而提出。標記階段和Mark-Sweep算法相同,標記後不是清理對象,而是將存活對象移向內存的一端。然後清除端邊界外的對象。
如圖:
在這裏插入圖片描述

6.5. 分代收集算法

分代收集法是目前大部分JVM所採用的方法,其核心思想是根據對象存活的不同生命週期將內存劃分爲不同的域,一般情況下將GC堆劃分爲老生代(Tenured/Old Generation)和新生代(YoungGeneration)。老生代的特點是每次垃圾回收時只有少量對象需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的算法。

6.5.1. 新生代與複製算法

目前大部分JVM的GC對於新生代都採取Copying算法,因爲新生代中每次垃圾回收都要回收大部分對象,即要複製的操作比較少,但通常並不是按照 1 : 1 來劃分新生代。一般將新生代劃分爲一塊較大的Eden空間和兩個較小的Survivor空間(From Space, To Space),每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將該兩塊空間中還存活的對象複製到另一塊Survivor空間中。
在這裏插入圖片描述

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

7. JAVA 四中引用類型

7.1. 強引用

在Java中最常見的就是強引用,把一個對象賦給一個引用變量,這個引用變量就是一個強引用。當一個對象被強引用變量引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即使該對象以後永遠都不會被用到JVM也不會回收。因此強引用是造成Java內存泄漏的主要原因之一。

7.2. 軟引用

軟引用需要用SoftReference 類來實現,對於只有軟引用的對象來說,當系統內存足夠時它不會被回收,當系統內存空間不足時它會被回收。軟引用通常用在對內存敏感的程序中。

7.3. 弱引用

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

7.4. 虛引用

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

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

8.1. 分代收集算法

當前主流VM垃圾收集都採用”分代收集”(Generational Collection)算法, 這種算法會根據對象存活週期的不同將內存劃分爲幾塊, 如JVM中的 新生代、老年代、永久代,這樣就可以根據各年代特點分別採用最適當的GC算法

8.1.1. 在新生代-複製算法

每次垃圾收集都能發現大批對象已死, 只有少量存活. 因此選用複製算法, 只需要付出少量存活對象的複製成本就可以完成收集.

8.1.2. 在老年代-標記整理算法

因爲對象存活率高、沒有額外空間對它進行分配擔保, 就必須採用“標記—清理”或“標記—整理”算法來進行回收, 不必進行內存複製, 且直接騰出空閒內存.

8.2. 分區收集算法

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

9. GC垃圾收集器

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

在這裏插入圖片描述

9.1. Serial 垃圾收集器(單線程、複製算法)

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

9.2. ParNew 垃圾收集器( Serial+ 多線程)

ParNew垃圾收集器其實是Serial收集器的多線程版本,也使用複製算法,除了使用多線程進行垃圾收集之外,其餘的行爲和Serial收集器完全一樣,ParNew垃圾收集器在垃圾收集過程中同樣也要暫停所有其他的工作線程。
ParNew收集器默認開啓和CPU數目相同的線程數,可以通過-XX:ParallelGCThreads參數來限制垃圾收集器的線程數。【Parallel:平行的】
ParNew雖然是除了多線程外和Serial收集器幾乎完全一樣,但是ParNew垃圾收集器是很多java虛擬機運行在Server模式下新生代的默認垃圾收集器。

9.3. Parallel Scavenge 收集器(多線程複製算法、高效)

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

9.4. Serial Old 收集器(單線程標記整理算法 )

Serial Old是Serial垃圾收集器年老代版本,它同樣是個單線程的收集器,使用標記-整理算法,這個收集器也主要是運行在Client默認的java虛擬機默認的年老代垃圾收集器。
在Server模式下,主要有兩個用途:

  1. 在JDK1.5之前版本中與新生代的Parallel Scavenge收集器搭配使用。
  2. 作爲年老代中使用CMS收集器的後備垃圾收集方案。新生代Serial與年老代Serial Old搭配垃圾收集過程圖:
    在這裏插入圖片描述
    新生代Parallel Scavenge收集器與ParNew收集器工作原理類似,都是多線程的收集器,都使用的是複製算法,在垃圾收集過程中都需要暫停所有的工作線程。新生代Parallel Scavenge/ParNew與年老代Serial Old搭配垃圾收集過程圖:
    在這裏插入圖片描述

9.5. Parallel Old 收集器(多線程標記整理算法)

Parallel Old收集器是Parallel Scavenge的年老代版本,使用多線程的標記-整理算法,在JDK1.6纔開始提供。在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保證新生代的吞吐量優先,無法保證整體的吞吐量,Parallel Old正是爲了在年老代同樣提供吞吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,可以優先考慮新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。
新生代Parallel Scavenge和年老代Parallel Old收集器搭配運行過程圖:
在這裏插入圖片描述

9.6. CMS 收集器(多線程標記清除算法)

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

9.6.1. 初始標記

只是標記一下GC Roots能直接關聯的對象,速度很快,仍然需要暫停所有的工作線程。

9.6.2. 併發標記

進行GC Roots跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。

9.6.3. 重新標記

爲了修正在併發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,仍然需要暫停所有的工作線程。

9.6.4. 併發清除

清除GC Roots不可達對象,和用戶線程一起工作,不需要暫停工作線程。由於耗時最長的併發標記和併發清除過程中,垃圾收集線程可以和用戶現在一起併發工作,所以總體上來看CMS收集器的內存回收和用戶線程是一起併發地執行。
CMS收集器工作過程:
在這裏插入圖片描述

9.7. G1 收集器

Garbage first垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與CMS收集器,G1收集器兩個最突出的改進是:

  1. 基於標記-整理算法,不產生內存碎片。
  2. 可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。
    G1收集器避免全區域垃圾收集,它把堆內存劃分爲大小固定的幾個獨立區域,並且跟蹤這些區域的垃圾收集進度,同時在後臺維護一個優先級列表,每次根據所允許的收集時間,優先回收垃圾最多的區域。區域劃分和優先級區域回收機制,確保G1收集器可以在有限時間獲得最高的垃圾收
    集效率。

10. JAVA IO/NIO

10.1. 阻塞 IO 模型

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

10.2. 非阻塞 IO 模型

當用戶線程發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。如果結果是一個error時,它就知道數據還沒有準備好,於是它可以再次發送read操作。一旦內核中的數據準備好了,並且又再次收到了用戶線程的請求,那麼它馬上就將數據拷貝到了用戶線程,然後返回。所以事實上,在非阻塞IO模型中,用戶線程需要不斷地詢問內核數據是否就緒,也就說非阻塞IO不會交出CPU,而會一直佔用CPU。典型的非阻塞IO模型一般如下:

while(true){
	data = socket.read();
	if(data!= error){
	處理數據
	break;
	}
 }

但是對於非阻塞IO就有一個非常嚴重的問題,在while循環中需要不斷地去詢問內核數據是否就緒,這樣會導致CPU佔用率非常高,因此一般情況下很少使用while循環這種方式來讀取數據。

10.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模型來說,一旦事件響應體很大,那麼就會導致後續的事件遲遲得不到處理,並且會影響新的事件輪詢。

10.4. 信號驅動 IO 模型

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

10.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

10.6. JAVA IO 包

在這裏插入圖片描述

10.7. JAVA NIO

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

在這裏插入圖片描述
NIO和傳統IO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。

10.7.1. NIO的緩衝區

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

10.7.2. NIO的非阻塞

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

在這裏插入圖片描述

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

10.9. Buffer

Buffer,故名思意,緩衝區,實際上是一個容器,是一個連續數組。Channel提供從文件、網絡讀取數據的渠道,但是讀取或寫入的數據都必須經由Buffer。
在這裏插入圖片描述
上面的圖描述了從一個客戶端向服務端發送數據,然後服務端接收數據的過程。客戶端發送數據時,必須先將數據存入Buffer中,然後將Buffer中的內容寫入通道。服務端這邊接收數據必須通過Channel將數據讀入到Buffer中,然後再從Buffer中取出數據來處理。

在NIO中,Buffer是一個頂層父類,它是一個抽象類,常用的Buffer的子類有:
ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、
ShortBuffer

10.10. Selector

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

11. JVM 類加載機制

JVM類加載機制分爲五個部分:加載,驗證,準備,解析,初始化,下面我們就分別來看一下這五個過程。
在這裏插入圖片描述

11.1. 加載

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

11.2. 驗證

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

11.3. 準備

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

11.4. 解析

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

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

11.5. 符號引用

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

11.6. 直接引用

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

11.7. 初始化

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

11.8. 類構造器

初始化階段是執行類構造器方法的過程。方法是由編譯器自動收集類中的類變量的賦值操作和靜態語句塊中的語句合併而成的。虛擬機會保證子方法執行之前,父類的方法已經執行完畢,如果一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯
器可以不爲這個類生成()方法。

注意以下幾種情況不會執行類初始化:

  1. 通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
  2. 定義對象數組,不會觸發該類的初始化。
  3. 常量在編譯期間會存入調用類的常量池中,本質上並沒有直接引用定義常量的類,不會觸 發定義常量所在的類。
  4. 通過類名獲取Class對象,不會觸發類的初始化。
  5. 通過Class.forName加載指定類時,如果指定參數initialize爲false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
  6. 通過ClassLoader默認的loadClass方法,也不會觸發初始化動作。

11.9. 類加載器

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

11.9.1. 啓動類加載器(Bootstrap ClassLoader)
  1. 負責加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。
11.9.2. 擴展類加載器(Extension ClassLoader)
  1. 負責加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變量指定路徑中的類庫。
11.9.3. 應用程序類加載器(Application ClassLoader):
  1. 負責加載用戶路徑(classpath)上的類庫。
    JVM通過雙親委派模型進行類的加載,當然我們也可以通過繼承java.lang.ClassLoader實現自定義的類加載器。
    在這裏插入圖片描述

11.10. 雙親委派

當一個類收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父類去完成,每一個層次類加載器都是如此,因此所有的加載請求都應該傳送到啓動類加載其中,只有當父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的
Class),子類加載器纔會嘗試自己去加載。

採用雙親委派的一個好處是比如加載位於rt.jar包中的類java.lang.Object,不管是哪個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個Object對象。
在這裏插入圖片描述

11.11. OSGI (動態模型系統)

OSGi(Open Service Gateway Initiative),是面向Java的動態模型系統,是Java動態化模塊化系統的一系列規範。

11.11.1. 動態改變構造

OSGi 服務平臺提供在多種網絡設備上無需重啓的動態改變構造的功能。爲了最小化耦合度和促使這些耦合度可管理,OSGi技術提供一種面向服務的架構,它能使這些組件動態地發現對方。

11.11.2. 模塊化編程與熱插拔

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

博主後記:

希望看到此篇博文的小夥伴,如果發現有什麼不對的地方,歡迎在下方留言指正!博主一定虛心接受並改正!大家一起共同進步。博主整理不易,如果對你有所幫助,可以給博主一個贊👍。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章