Java知识总结(五)
JAVA BIO/NIO
同步
發起一個請求或任務,被調用者在未完成請求或任務前,不會返回結果。
需要一直等待該請求返回或任務完成反饋的結果,在這期間不能夠去做其他的事情。
比如:你打電話給書店老板詢問書籍,老板幫你去找書,你需要一直等待,等待書店老板給你回復。
異步
發起一個請求或任務之后,被調用者會理刻返回表示已經接受請求或任務,但是并沒有返回結果,就接著去做別的事情,發出的請求或任務完成時,被調用者會返回結果。
比如:你打電話給書店老板詢問書籍,你讓他查到了再打電話給你,然后你掛斷電話,期間你可以干其他事情,等到老本找到了書籍然后給你打電話。
阻塞
發起請求后,調用者需要一直等待結果返回,也就是當前的線程會被掛起,無法去做其他請求。
非阻塞
發起請求后,調用者不需要等待結果返回,可以去做別的事情。
BIO(Blocking I/O)(最傳統的同步阻塞IO模型)
典型的同步阻塞IO模型:data = socket.read();
當應用程序發出請求時,先去判斷內核中的數據是否準備完成,如果沒有準備完成,該應用程序就會被阻塞(讓出cpu資源),等到內核數據準備完成,將數據拷貝給應用程序,應用程序解除block狀態。
基于字節流和字符流操作,數據流的特點是單向性,要么只讀、要么只寫。
server端要為每一個連接建立一個線程,這樣的好處是每個線程可以專注自身的I/O操作并且編程簡單。但是這種模型并不適合連接數過多情況。
NIO(Non - Blocking I/O)(同步非阻塞IO模型)
當應用程序發出read請求后,不需要等待,它會馬上得到來自內核的返回,如果返回的結果是error(數據沒有準備好),那么應用程序就繼續向內核發出請求,再次去確認,這樣不停做循環,一旦內核準好數據同時應用程序發來請求,那么就將數據拷貝給應用程序,返回。
這樣帶來的問題:線程沒有進入阻塞狀態,它就不會讓出cpu資源,導致cpu的占用率很高。
NIO的組成包括:Channel(通道)、 Buffer(緩沖區)、Selector。
Channel
-
Channel和Stream(流)是同一個級別的,區別在于:Stream是單向的,而Channel是雙向的,既可以用來讀也可以用來寫操作。
-
Channel的主要實現:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
-
Buffer
是一個容器,連續的數組用來存儲數據。
Channel提供從文件、網絡讀取數據的渠道,但是讀寫操作都必須由Buffer來操作。
上面是一個從客戶端向服務器端發送數據的過程。
客戶端發出數據經過Buffer寫入傳給Channel,讀入數據經過Channel將數據讀入Buffer傳給服務端。
-
Selector
是NIO的核心類,通過Selector去檢測多個Channel中是否有數據的發生(讀或寫請求),如果對應的Channel上有真正的請求發生,那么就去處理該Channel上的請求。Selector本身也是一個線程,設定一個線程專門去管理多個Channel的請求任務,而不需要為每一個Channel去建立對應的線程,避免了多個線程之間的上下文切換,大大減少了系統的開銷。
1對應文件IO、2對應文件UDP、3、4對應文件TCP(Server和 Client)
多路復用IO模型
此模型的本質還是NIO模型,在NIO中的通過Selector實現在一個線程輪詢多個通道的數據,需要先將用戶線程中需要輪詢的socket注冊到Selector中,用Selector去輪詢多個socketChannel是否有請求到達,一旦請求到達,Selector.select返回,最后完成I/O數據的傳輸這個過程用戶線程是處于阻塞狀態的。注意:socket配置也是非阻塞的。
相比于NIO,多路復用IO用戶線程首先需要在Reactor中注冊一個事件處理器,然后Reactor(相當于上文提到的selector)負責輪詢各個通道是否有新的數據到來,當有新的數據到來時,Reactor通過先前注冊的事件處理器通知用戶線程有數據可讀,此時用戶線程向內核發起讀取IO數據的請求,用戶線程阻塞直至數據讀取完成。
多路復用IO模型效率高于NIO模型原因在于:NIO中socket輪詢是在用戶線程中的,而多路復用是在內核中。
信號驅動IO模型
當用戶線程發起一個I/O請求時,給對應的socket注冊一個信號函數,用戶不會立刻得到結果(內核中數據還沒有準備好),而是繼續去做別的事情,當內核中數據準備號時,給用戶發送一個信號給用戶線程,用戶線程通過在信號函數中調用I/O操作進行實際的讀寫操作。
異步IO模型
該模型是最理想模型。它實現的流程:當用戶線程發起read操作之后,就去做它自己的事情了,內核接收到用戶線程的請求后,立刻返回,表明該請求已經受理,這個過程不會對用戶線程造成任何阻塞。因為內核在完成數據準備后就將數據拷貝給用戶線程,返回用戶線程信息表明數據已經傳輸完畢,read操作已經完成,不需要用戶線程再去調用IO操作。只需要先發起一個請求,當接收內核返回的成功信號時表示 IO 操作已經完成,可以直接去使用數據了。
和信號驅動模型的差別就在這里,信號驅動模型在內核完成數據準備之后,告訴用戶線程數據已經準備完畢,需要你自己來調用IO操作拿到數據。
Java IO
IO分類
- 按照流的流向分,可以分為輸?流和輸出流;
- 按照操作單元劃分,可以劃分為字節流和字符流;
- 按照流的??劃分為節點流和處理流。
InputStream/Reader: 所有的輸?流的基類,前者是字節輸?流,后者是字符輸?流。
OutputStream/Writer: 所有輸出流的基類,前者是字節輸出流,后者是字符輸出流。
不管是文件讀寫還是網絡發送接收,信息的最小存儲單元都是字節,那為什么I/O流操作要分為字節流操作和字符流操作呢?
字符流是由 Java 虛擬機將字節轉換得到的,問題就出在這個過程還算是?常耗時,并且,如果我們不知道編碼類型就很容易出現亂碼問題。所以, I/O 流就?脆提供了?個直接操作字符的接??便我們平時對字符進?流操作。
JVM類加載機制
Java程序運行時,必須經過編譯和運行兩個步驟。首先將后綴名為.java的源文件進行編譯,最終生成后綴名為.class的字節碼文件。然后Java虛擬機將編譯好的字節碼文件加載到內存(這個過程被稱為類加載,是由加載器完成的),然后虛擬機針對加載到內存的java類進行解釋執行,顯示結果。
JVM類加載大致分為三個過程:加載、連接、初始化。
類加載器
在類加載的過程中,只有加載階段可以自定義類加載器,而其他階段由JVM主導。
因此加載的階段被放到了JVM外部實現,便于讓應用程序決定如何獲取所需的類。
JVM中提供了三種類加載器:
-
啟動類加載器(Bootstrap ClassLoader)
最頂層的類加載器,由c++實現。
負責加載:JAVA_HOME\lib 目錄中的jar包或被 -Xbootclasspath 參數指定的路徑中的所有類
-
擴展類加載器(Extension ClassLoader)
繼承自:java.lang.ClassLoader
負責加載 JAVA_HOME\lib\ext 目錄中的,或通過 java.ext.dirs 系統變量指定路徑中的類庫。
-
應用程序類加載器(Application ClassLoader/ System Class Loader ):
(這里的Application ClassLoader 和System Class Loader 是同一個類加載器,只是叫法不同)
繼承自:java.lang.ClassLoader
?向我們?戶的加載器,負責加載當前應?classpath下的所有jar包和類。
可以通過繼承 java.lang.ClassLoader實現自定義的類加載器,重寫findClass方法加載指定路徑上的class。
雙親委派模型
每一個類都有對應的類加載器。當類收到一個加載請求時,先去判斷這個類是否已經被加載,被加載過的類會直接返回,否則嘗試加載。
加載過程:類本身不會主動去加載,它會將請求委派給父類加載器loadClass()處理,父類則會委派給父類的父類,因此所有的請求都會傳到頂層的類加載器中(Bootstrap ClassLoader),只有當父類中的加載器無法進行加載時,自己才會來處理類加載。
注意:類加載器之間的“??”關系也不是通過繼承來體現的,是由“優先級”來決定
//源碼private final ClassLoader parent;protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// ?先,檢查請求的類是否已經被加載過Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {//?加載器不為空,調??加載loadClass()?法處理c = parent.loadClass(name, false);} else {//?加載器為空,使?啟動類加載器BootstrapClassLoader 加載c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//拋出異常說明?類加載器?法完成加載請求}if (c == null) {long t1 = System.nanoTime();//??嘗試加載c = findClass(name);// this is the defining class loader; record thestatssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}好處:避免類的重復加載(JVM 區分不同類的方式不僅僅根據類名,相同的類文件被不同的類加載器加載產生的是兩個不同的類)
比如:加載rs.jar包中的類java.lang.Object,無論加載器加載那個類,最終委派給啟動類加載器進行類的加載,保證了不同的類加載器最終得到的是同一個Ojbect對象。
連接的過程在細分為:驗證、準備、解析
-
加載
這個階段會在內存中生成代表該類的java.lang.Class對象(類對象),作為方法區這個類的各種數據的入口。
注意:獲取類的過程并不一定要從Class文件中,也可以從jar包或war包中獲取,也可以運行時計算生成(動態代理)。
-
連接
-
驗證
確保class文件的字節流中的信息符合JVM的要求,不會危害JVM的安全
-
準備
正式為類中的變量分配內存以及為其設置初始值階段(在方法區中為這些類變量分配內存)。
舉例:
public static int v = 8080; /* 定義的靜態變量 v 這里初始化值的過程并不是直接將 8080 賦值給 v ,v 變量在準備階段的初始化值是 0 , 將其賦值為 8080 的是put static 指令,該指令被編譯后存放在類構造器<client>方法中。 */public final static int v = 8080; /* 如果聲明靜態變量被final關鍵字所修飾,在編譯階段生成的v 的ConstantValue屬性,在準備階段中v被賦值為 8080 */ -
解析
JVM將常量池中符號引用替換直接引用的過程。
-
符號引用
一組符號所描述的引用目標,符號的形式是字面量。
如:在Class文件中它以CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info等類型的常量出現。
在Java中,一個java類將會編譯成一個class文件。在編譯時,java類并不知道所引用的變量(基本數據類型、局部變量、方法等等)實際地址,因此只能使用符號引用來代替。比如org.simple.People類引用了org.simple.Language類,在編譯時People類并不知道Language類的實際內存地址,因此只能使用符號org.simple.Language。
各種JVM實現內存的布局可能不同,但是它們識別符號引用是一致的。
-
直接引用
指向目標的指針,相對偏移量(指向實例變量、實例方法的直接引用都是偏移量),間接定位到目標的句柄。
直接引用與JVM布局有關,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般不會相同。
如果有了直接引用,那目標必定存在于內存中。
-
-
-
初始化
類加載的最后一個階段,除了在類加載階段可以自定義類加載器,其他的階段都是由JVM主導控制。
這個階段才是正真的執行字節碼文件,根據字節碼文件的內容對類的各個字段進行賦值。
類構造器
初始化階段是類構造器方法執行的過程,方法是由編譯器收集類中的類變量賦值操作和靜態代碼塊合成的,JVM會保證方法執行前其父類的方法已經執行完畢。如果一個類中沒有靜態變量、靜態語句塊,那么編譯器可以不為這個類生成方法。
Java對象創建的過程
對象的創建分為五個過程
-
類加載檢查
當JVM接收到new指令時,首先會先去檢查該指令能否在常量池中找到對應該類的符號引用,并檢查該類是否已經被加載、連接,初始化過。如果沒有先進行類的加載。
-
分配內存
在類加載檢查完成后,在java堆中為新生的對象分配內存。
分配內存方式有兩種:指針碰撞、空閑列表。
分配方式選擇根據:java堆內存是否規整
指針碰撞:
- 適用場景:java堆內存規整(沒有內存碎片化)
- GC收集器:Serial (單線程、復制算法)和 PerNew(多線程、復制算法)。
- 原理:將內存分為兩塊,中間有一個分界值指針,用過的內存放一邊,沒有用過的內存放一邊,將對象放入到沒有用過的內存中
空閑列表:
- 適用場景:Java堆內存碎片化
- GC收集器:CMS(多線程+標記清除算法)
- 原理:將列表中沒有用過的內存標記下來,找到適合新生對象大小的位置放入即可,更新列表。
分配內存需要考慮到線程安全的問題:
對象的創建很頻繁,因此JVM需要保證線程的安全:
- TLAB:為每個線程預先在Eden區分配一塊內存,JVM給對象分配內存時先在TLAB中分配,當TLAB中內存不夠或者使用完之后,就采用CAS+失敗重試的方法。
- CAS+失敗重試:CAS 是樂觀鎖的一種實現方式。樂觀鎖:每次不加鎖而是假設沒有沖突去完成某個操作,如果因為沖突失敗就重試,直到成功為止。此方法保證更新操作的原子性。
-
初始化零值
內存分配完成后,JVM將分配到的內存空間初始化零值(不包含對象頭),這一操作保證了對象中的成員變量在不賦初值就可以使用。
-
設置對象頭
對象頭中的信息包含:
markword(標記字段)
- 對象的哈希碼
- 對象對應的GC分代年齡
- 鎖狀態標志、線程持有的鎖、
klass(Class對象指針)
- 對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例
數組長度(只有數組對象有)
如果對象是一個數組, 那在對象頭中還必須有一塊數據用于記錄數組長度(例如:int)。
-
執行構造方法
從JVM角度來看一個對象已經產生,從java程序角度來看,創建對象才開始,執行構造方法,按照意愿將對象初始化數據之后這個對象才能夠真正得到使用。
圖上有點小錯誤:分配內存中:采用指針碰撞是復制算法,不是標記整理
??蜕献龅降念}目:
(單選題)以下代碼的輸出結果是?public class B{public static B t1 = new B();public static B t2 = new B();{System. out.println( "構造塊");}static{System.out.println("靜態塊");}public static void main( String[] args){B t = new B( );}}//正確答案:構造塊、構造塊、靜態塊、構造塊 //現從主函數Main中入手,執行 B t = new B( );也就是創建對象,創建對象之前需要先進行類加載過程,而類加載的過程需要檢查類是否進行加載,現在進行類的加載過程。 /*類中定義了靜態域:靜態變量,靜態代碼塊,靜態方法。 靜態域執行的過程:按照其定義的變量、代碼塊、方法的順序來。 因此這里先執行public static B t1 = new B();這個代碼,而這也是創建對象的語句,創建對象之前也是需要進行類加載檢查,而B類在前就已經加載過了,注意:類中的靜態域只在類第一次加載的過程中執行,因此這里不會再在進行靜態域的加載,跳過靜態域到構造塊和構造方法的執行過程,完成t1對象創建,因此控制臺輸出:構造塊。 接著回到 public static B t2 = new B();的過程,繼續創建對象t2,這個過程和t1是一樣的因此,輸出:構造塊。當兩個靜態變量都完成時,接下來執行靜態代碼塊,輸出:靜態塊。 執行構造塊和構造方法,最后創建對象t,輸出:構造塊。 */對象訪問定位的兩種方式
建立對象之后就是要調用對象干事,java中通過棧上的reference數據來操作堆上的具體對象。
-
句柄
在java堆中開辟一塊內存用作句柄池,句柄本身是指向對象的實例數據和類型數據,引用是指向句柄。
-
直接指針
通過指針直接指向java堆中對象的地址,堆中對象的布局有所改變,在對象的實例數據中存放指向對象類型數據的地址。
句柄訪問對象的好處就是:當對象的地址改變時,引用的地址不要改變,需要改變的是指向對象的句柄。
缺點:通過句柄這個間接訪問對象開銷要高一些。
直接指針:訪問的速度快于句柄,對象地址發生改變時,引用也要發生改變。
總結
以上是生活随笔為你收集整理的Java知识总结(五)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Fiddler(五)设置代理 HTTPS
- 下一篇: Educoder——Java入门:方法的