溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

JVM和操作系統(tǒng)交互的方法是什么

發(fā)布時(shí)間:2021-10-13 10:21:40 來源:億速云 閱讀:99 作者:iii 欄目:編程語言

這篇文章主要講解了“JVM和操作系統(tǒng)交互的方法是什么”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“JVM和操作系統(tǒng)交互的方法是什么”吧!

肉眼看計(jì)算機(jī)是由 CPU 、內(nèi)存、顯示器這些硬件設(shè)備組成,但大部分人從事的是軟件開發(fā)工作。計(jì)算機(jī)底層原理就是連通硬件和軟件的橋梁,理解計(jì)算機(jī)底層原理才能在程序設(shè)計(jì)這條路上越走越快,越走越輕松。從操作系統(tǒng)層面去理解高級(jí)編程語言的執(zhí)行過程,會(huì)發(fā)現(xiàn)好多軟件設(shè)計(jì)都是同一種套路,很多語言特性都依賴于底層機(jī)制,今天為你一一揭秘。

結(jié)合 CPU 理解一行 Java 代碼是怎么執(zhí)行的

根據(jù)馮·諾依曼思想,計(jì)算機(jī)采用二進(jìn)制作為數(shù)制基礎(chǔ),必須包含:運(yùn)算器、控制器、存儲(chǔ)設(shè)備,以及輸入輸出設(shè)備,如下圖所示。

JVM和操作系統(tǒng)交互的方法是什么

enter image description here

我們先來分析 CPU 的工作原理,現(xiàn)代 CPU 芯片中大都集成了,控制單元,運(yùn)算單元,存儲(chǔ)單元。控制單元是 CPU 的控制中心, CPU 需要通過它才知道下一步做什么,也就是執(zhí)行什么指令,控制單元又包含:指令寄存器(IR ),指令譯碼器( ID )和操作控制器( OC )。

當(dāng)程序被加載進(jìn)內(nèi)存后,指令就在內(nèi)存中了,這個(gè)時(shí)候說的內(nèi)存是獨(dú)立于 CPU 外的主存設(shè)備,也就是 PC 機(jī)中的內(nèi)存條,指令指針寄存器IP 指向內(nèi)存中下一條待執(zhí)行指令的地址,控制單元根據(jù) IP寄存器的指向,將主存中的指令裝載到指令寄存器。

這個(gè)指令寄存器也是一個(gè)存儲(chǔ)設(shè)備,不過他集成在 CPU 內(nèi)部,指令從主存到達(dá) CPU 后只是一串 010101 的二進(jìn)制串,還需要通過譯碼器解碼,分析出操作碼是什么,操作數(shù)在哪,之后就是具體的運(yùn)算單元進(jìn)行算術(shù)運(yùn)算(加減乘除),邏輯運(yùn)算(比較,位移)。而 CPU 指令執(zhí)行過程大致為:取址(去主存獲取指令放到寄存器),譯碼(從主存獲取操作數(shù)放入高速緩存 L1 ),執(zhí)行(運(yùn)算)。

JVM和操作系統(tǒng)交互的方法是什么

enter image description here

這里解釋下上圖中 CPU 內(nèi)部集成的存儲(chǔ)單元 SRAM ,正好和主存中的 DRAM 對(duì)應(yīng), RAM 是隨機(jī)訪問內(nèi)存,就是給一個(gè)地址就能訪問到數(shù)據(jù),而磁盤這種存儲(chǔ)媒介必須順序訪問,而 RAM 又分為動(dòng)態(tài)和靜態(tài)兩種,靜態(tài) RAM 由于集成度較低,一般容量小,速度快,而動(dòng)態(tài) RAM 集成度較高,主要通過給電容充電和放電實(shí)現(xiàn),速度沒有靜態(tài) RAM 快,所以一般將動(dòng)態(tài) RAM 做為主存,而靜態(tài) RAM 作為 CPU 和主存之間的高速緩存 (cache),用來屏蔽 CPU 和主存速度上的差異,也就是我們經(jīng)常看到的 L1 , L2 緩存。每一級(jí)別緩存速度變低,容量變大。


下圖展示了存儲(chǔ)器的層次化架構(gòu),以及 CPU 訪問主存的過程,這里有兩個(gè)知識(shí)點(diǎn),一個(gè)是多級(jí)緩存之間為保證數(shù)據(jù)的一致性,而推出的緩存一致性協(xié)議,具體可以參考這篇文章,另外一個(gè)知識(shí)點(diǎn)是, cache 和主存的映射,首先要明確的是 cahce 緩存的單位是緩存行,對(duì)應(yīng)主存中的一個(gè)內(nèi)存塊,并不是一個(gè)變量,這個(gè)主要是因?yàn)?nbsp; CPU 訪問的空間局限性:被訪問的某個(gè)存儲(chǔ)單元,在一個(gè)較短時(shí)間內(nèi),很有可能再次被訪問到,以及空間局限性:被訪問的某個(gè)存儲(chǔ)單元,在較短時(shí)間內(nèi),他的相鄰存儲(chǔ)單元也會(huì)被訪問到。


而映射方式有很多種,類似于 cache 行號(hào) = 主存塊號(hào) mod cache總行數(shù) ,這樣每次獲取到一個(gè)主存地址,根據(jù)這個(gè)地址計(jì)算出在主存中的塊號(hào)就可以計(jì)算出在 cache 中的行號(hào)。

JVM和操作系統(tǒng)交互的方法是什么


下面我們接著聊 CPU 的指令執(zhí)行。取址、譯碼、執(zhí)行,這是一個(gè)指令的執(zhí)行過程,所有指令都會(huì)嚴(yán)格按照這個(gè)順序執(zhí)行。但是多個(gè)指令之間其實(shí)是可以并行的,對(duì)于單核 CPU 來說,同一時(shí)刻只能有一條指令能夠占有執(zhí)行單元運(yùn)行。這里說的執(zhí)行是 CPU 指令處理 (取指,譯碼,執(zhí)行) 三步驟中的第三步,也就是運(yùn)算單元的計(jì)算任務(wù)。

所以為了提升 CPU 的指令處理速度,所以需要保證運(yùn)算單元在執(zhí)行前的準(zhǔn)備工作都完成,這樣運(yùn)算單元就可以一直處于運(yùn)算中,而剛剛的串行流程中,取指,解碼的時(shí)候運(yùn)算單元是空閑的,而且取指和解碼如果沒有命中高速緩存還需要從主存取,而主存的速度和 CPU 不在一個(gè)級(jí)別上,所以指令流水線 可以大大提高 CPU 的處理速度,下圖是一個(gè)3級(jí)流水線的示例圖,而現(xiàn)在的奔騰 CPU 都是32級(jí)流水線,具體做法就是將上面三個(gè)流程拆分的更細(xì)。

JVM和操作系統(tǒng)交互的方法是什么

enter image description here

除了指令流水線, CPU 還有分支預(yù)測(cè),亂序執(zhí)行等優(yōu)化速度的手段。好了,我們回到正題,一行 Java 代碼是怎么執(zhí)行的?

一行代碼能夠執(zhí)行,必須要有可以執(zhí)行的上下文環(huán)境,包括:指令寄存器、數(shù)據(jù)寄存器、??臻g等內(nèi)存資源,然后這行代碼必須作為一個(gè)執(zhí)行流能夠被操作系統(tǒng)的任務(wù)調(diào)度器識(shí)別,并給他分配 CPU 資源,當(dāng)然這行代碼所代表的指令必須是 CPU 可以解碼識(shí)別的,所以一行 Java 代碼必須被解釋成對(duì)應(yīng)的 CPU 指令才能執(zhí)行。下面我們看下System.out.println("Hello world")這行代碼的轉(zhuǎn)譯過程。

Java 是一門高級(jí)語言,這類語言不能直接運(yùn)行在硬件上,必須運(yùn)行在能夠識(shí)別 Java 語言特性的虛擬機(jī)上,而 Java 代碼必須通過 Java 編譯器將其轉(zhuǎn)換成虛擬機(jī)所能識(shí)別的指令序列,也稱為 Java 字節(jié)碼,之所以稱為字節(jié)碼是因?yàn)?Java 字節(jié)碼的操作指令(OpCode)被固定為一個(gè)字節(jié),以下為 System.out.println("Hello world") 編譯后的字節(jié)碼:

0x00:  b2 00 02         getstatic  Java .lang.System.out
0x03:  12 03            ldc "Hello, World!"
0x05:  b6 00 04         invokevirtual  Java .io.PrintStream.println
0x08:  b1               return

最左列是偏移;中間列是給虛擬機(jī)讀的字節(jié)碼;最右列是高級(jí)語言的代碼,下面是通過匯編語言轉(zhuǎn)換成的機(jī)器指令,中間是機(jī)器碼,第三列為對(duì)應(yīng)的機(jī)器指令,最后一列是對(duì)應(yīng)的匯編代碼:

0x00:  55                    push   rbp
0x01:  48 89 e5              mov    rbp,rsp
0x04:  48 83 ec 10           sub    rsp,0x10
0x08:  48 8d 3d 3b 00 00 00  lea    rdi,[rip+0x3b]
                                    ; 加載 "Hello, World!\n"
0x0f:  c7 45 fc 00 00 00 00  mov    DWORD PTR [rbp-0x4],0x0
0x16:  b0 00                 mov    al,0x0
0x18:  e8 0d 00 00 00        call   0x12
                                    ; 調(diào)用 printf 方法
0x1d:  31 c9                 xor    ecx,ecx
0x1f:  89 45 f8              mov    DWORD PTR [rbp-0x8],eax
0x22:  89 c8                 mov    eax,ecx
0x24:  48 83 c4 10           add    rsp,0x10
0x28:  5d                    pop    rbp
0x29:  c3                    ret
JVM 通過類加載器加載 class 文件里的字節(jié)碼后,會(huì)通過解釋器解釋成匯編指令,最終再轉(zhuǎn)譯成 CPU 可以識(shí)別的機(jī)器指令,解釋器是軟件來實(shí)現(xiàn)的,主要是為了實(shí)現(xiàn)同一份 Java 字節(jié)碼可以在不同的硬件平臺(tái)上運(yùn)行,而將匯編指令轉(zhuǎn)換成機(jī)器指令由硬件直接實(shí)現(xiàn),這一步速度是很快的,當(dāng)然 JVM 為了提高運(yùn)行效率也可以將某些熱點(diǎn)代碼(一個(gè)方法內(nèi)的代碼)一次全部編譯成機(jī)器指令后然后在執(zhí)行,也就是和解釋執(zhí)行對(duì)應(yīng)的即時(shí)編譯(JIT), JVM 啟動(dòng)的時(shí)候可以通過 -Xint 和 -Xcomp 來控制執(zhí)行模式。

從軟件層面上, class 文件被加載進(jìn)虛擬機(jī)后,類信息會(huì)存放在方法區(qū),在實(shí)際運(yùn)行的時(shí)候會(huì)執(zhí)行方法區(qū)中的代碼,在 JVM 中所有的線程共享堆內(nèi)存和方法區(qū),而每個(gè)線程有自己獨(dú)立的 Java 方法棧,本地方法棧(面向 native 方法),PC寄存器(存放線程執(zhí)行位置),當(dāng)調(diào)用一個(gè)方法的時(shí)候, Java 虛擬機(jī)會(huì)在當(dāng)前線程對(duì)應(yīng)的方法棧中壓入一個(gè)棧幀,用來存放 Java 字節(jié)碼操作數(shù)以及局部變量,這個(gè)方法執(zhí)行完會(huì)彈出棧幀,一個(gè)線程會(huì)連續(xù)執(zhí)行多個(gè)方法,對(duì)應(yīng)不同的棧幀的壓入和彈出,壓入棧幀后就是 JVM 解釋執(zhí)行的過程了。

JVM和操作系統(tǒng)交互的方法是什么

enter image description here
中斷
剛剛說到, CPU 只要一上電就像一個(gè)永動(dòng)機(jī), 不停的取指令,運(yùn)算,周而復(fù)始,而中斷便是操作系統(tǒng)的靈魂,故名思議,中斷就是打斷 CPU 的執(zhí)行過程,轉(zhuǎn)而去做點(diǎn)別的。

例如系統(tǒng)執(zhí)行期間發(fā)生了致命錯(cuò)誤,需要結(jié)束執(zhí)行,例如用戶程序調(diào)用了一個(gè)系統(tǒng)調(diào)用的方法,例如mmp等,就會(huì)通過中斷讓 CPU 切換上下文,轉(zhuǎn)到內(nèi)核空間,例如一個(gè)等待用戶輸入的程序正在阻塞,而當(dāng)用戶通過鍵盤完成輸入,內(nèi)核數(shù)據(jù)已經(jīng)準(zhǔn)備好后,就會(huì)發(fā)一個(gè)中斷信號(hào),喚醒用戶程序把數(shù)據(jù)從內(nèi)核取走,不然內(nèi)核可能會(huì)數(shù)據(jù)溢出,當(dāng)磁盤報(bào)了一個(gè)致命異常,也會(huì)通過中斷通知 CPU ,定時(shí)器完成時(shí)鐘滴答也會(huì)發(fā)時(shí)鐘中斷通知 CPU 。

中斷的種類,我們這里就不做細(xì)分了,中斷有點(diǎn)類似于我們經(jīng)常說的事件驅(qū)動(dòng)編程,而這個(gè)事件通知機(jī)制是怎么實(shí)現(xiàn)的呢,硬件中斷的實(shí)現(xiàn)通過一個(gè)導(dǎo)線和 CPU 相連來傳輸中斷信號(hào),軟件上會(huì)有特定的指令,例如執(zhí)行系統(tǒng)調(diào)用創(chuàng)建線程的指令,而 CPU 每執(zhí)行完一個(gè)指令,就會(huì)檢查中斷寄存器中是否有中斷,如果有就取出然后執(zhí)行該中斷對(duì)應(yīng)的處理程序。

陷入內(nèi)核 : 我們?cè)谠O(shè)計(jì)軟件的時(shí)候,會(huì)考慮程序上下文切換的頻率,頻率太高肯定會(huì)影響程序執(zhí)行性能,而陷入內(nèi)核是針對(duì) CPU 而言的, CPU 的執(zhí)行從用戶態(tài)轉(zhuǎn)向內(nèi)核態(tài),以前是用戶程序在使用 CPU ,現(xiàn)在是內(nèi)核程序在使用 CPU ,這種切換是通過系統(tǒng)調(diào)用產(chǎn)生的。

系統(tǒng)調(diào)用是執(zhí)行操作系統(tǒng)底層的程序,Linux的設(shè)計(jì)者,為了保護(hù)操作系統(tǒng),將進(jìn)程的執(zhí)行狀態(tài)用內(nèi)核態(tài)和用戶態(tài)分開,同一個(gè)進(jìn)程中,內(nèi)核和用戶共享同一個(gè)地址空間,一般 4G 的虛擬地址,其中 1G 給內(nèi)核態(tài), 3G 給用戶態(tài)。在程序設(shè)計(jì)的時(shí)候我們要盡量減少用戶態(tài)到內(nèi)核態(tài)的切換,例如創(chuàng)建線程是一個(gè)系統(tǒng)調(diào)用,所以我們有了線程池的實(shí)現(xiàn)。

從 Linux 內(nèi)存管理角度理解 JVM 內(nèi)存模型


進(jìn)程上下文

我們可以將程序理解為一段可執(zhí)行的指令集合,而這個(gè)程序啟動(dòng)后,操作系統(tǒng)就會(huì)為他分配 CPU ,內(nèi)存等資源,而這個(gè)正在運(yùn)行的程序就是我們說的進(jìn)程,進(jìn)程是操作系統(tǒng)對(duì)處理器中運(yùn)行的程序的一種抽象。

而為進(jìn)程分配的內(nèi)存以及 CPU 資源就是這個(gè)進(jìn)程的上下文,保存了當(dāng)前執(zhí)行的指令,以及變量值,而 JVM 啟動(dòng)后也是linux上的一個(gè)普通進(jìn)程,進(jìn)程的物理實(shí)體和支持進(jìn)程運(yùn)行的環(huán)境合稱為上下文,而上下文切換就是將當(dāng)前正在運(yùn)行的進(jìn)程換下,換一個(gè)新的進(jìn)程到處理器運(yùn)行,以此來讓多個(gè)進(jìn)程并發(fā)的執(zhí)行,上下文切換可能來自操作系統(tǒng)調(diào)度,也有可能來自程序內(nèi)部,例如讀取IO的時(shí)候,會(huì)讓用戶代碼和操作系統(tǒng)代碼之間進(jìn)行切換。

JVM和操作系統(tǒng)交互的方法是什么

enter image description here
虛擬存儲(chǔ)
當(dāng)我們同時(shí)啟動(dòng)多個(gè) JVM 執(zhí)行:System.out.println(new Object()); 將會(huì)打印這個(gè)對(duì)象的 hashcode ,hashcode 默認(rèn)為內(nèi)存地址,最后發(fā)現(xiàn)他們打印的都是  Java .lang.Object@4fca772d ,也就是多個(gè)進(jìn)程返回的內(nèi)存地址竟然是一樣的。

通過上面的例子我們可以證明,linux中每個(gè)進(jìn)程有單獨(dú)的地址空間,在此之前,我們先了解下 CPU 是如何訪問內(nèi)存的?

假設(shè)我們現(xiàn)在還沒有虛擬地址,只有物理地址,編譯器在編譯程序的時(shí)候,需要將高級(jí)語言轉(zhuǎn)換成機(jī)器指令,那么 CPU 訪問內(nèi)存的時(shí)候必須指定一個(gè)地址,這個(gè)地址如果是一個(gè)絕對(duì)的物理地址,那么程序就必須放在內(nèi)存中的一個(gè)固定的地方,而且這個(gè)地址需要在編譯的時(shí)候就要確認(rèn),大家應(yīng)該想到這樣有多坑了吧。

如果我要同時(shí)運(yùn)行兩個(gè) office word 程序,那么他們將操作同一塊內(nèi)存,那就亂套了,偉大的計(jì)算機(jī)前輩設(shè)計(jì)出,讓 CPU 采用 段基址 + 段內(nèi)偏移地址 的方式訪問內(nèi)存,其中段基地址在程序啟動(dòng)的時(shí)候確認(rèn),盡管這個(gè)段基地址還是絕對(duì)的物理地址,但終究可以同時(shí)運(yùn)行多個(gè)程序了, CPU  采用這種方式訪問內(nèi)存,就需要段基址寄存器和段內(nèi)偏移地址寄存器來存儲(chǔ)地址,最終將兩個(gè)地址相加送上地址總線。

而內(nèi)存分段,相當(dāng)于每個(gè)進(jìn)程都會(huì)分配一個(gè)內(nèi)存段,而且這個(gè)內(nèi)存段需要是一塊連續(xù)的空間,主存里維護(hù)著多個(gè)內(nèi)存段,當(dāng)某個(gè)進(jìn)程需要更多內(nèi)存,并且超出物理內(nèi)存的時(shí)候,就需要將某個(gè)不常用的內(nèi)存段換到硬盤上,等有充足內(nèi)存的時(shí)候在從硬盤加載進(jìn)來,也就是 swap 。每次交換都需要操作整個(gè)段的數(shù)據(jù)。

首先連續(xù)的地址空間是很寶貴的,例如一個(gè) 50M 的內(nèi)存,在內(nèi)存段之間有空隙的情況下,將無法支持 5 個(gè)需要 10M 內(nèi)存才能運(yùn)行的程序,如何才能讓段內(nèi)地址不連續(xù)呢? 答案是內(nèi)存分頁。

在保護(hù)模式下,每一個(gè)進(jìn)程都有自己獨(dú)立的地址空間,所以段基地址是固定的,只需要給出段內(nèi)偏移地址就可以了,而這個(gè)偏移地址稱為線性地址,線性地址是連續(xù)的,而內(nèi)存分頁將連續(xù)的線性地址和和分頁后的物理地址相關(guān)聯(lián),這樣邏輯上的連續(xù)線性地址可以對(duì)應(yīng)不連續(xù)的物理地址。

物理地址空間可以被多個(gè)進(jìn)程共享,而這個(gè)映射關(guān)系將通過頁表( page table)進(jìn)行維護(hù)。 標(biāo)準(zhǔn)頁的尺寸一般為 4KB ,分頁后,物理內(nèi)存被分成若干個(gè) 4KB 的數(shù)據(jù)頁,進(jìn)程申請(qǐng)內(nèi)存的時(shí)候,可以映射為多個(gè) 4KB 大小的物理內(nèi)存,而應(yīng)用程序讀取數(shù)據(jù)的時(shí)候會(huì)以頁為最小單位,當(dāng)需要和硬盤發(fā)生交換的時(shí)候也是以頁為單位。

現(xiàn)代計(jì)算機(jī)多采用虛擬存儲(chǔ)技術(shù),虛擬存儲(chǔ)讓每個(gè)進(jìn)程以為自己獨(dú)占整個(gè)內(nèi)存空間,其實(shí)這個(gè)虛擬空間是主存和磁盤的抽象,這樣的好處是,每個(gè)進(jìn)程擁有一致的虛擬地址空間,簡(jiǎn)化了內(nèi)存管理,進(jìn)程不需要和其他進(jìn)程競(jìng)爭(zhēng)內(nèi)存空間。

因?yàn)樗仟?dú)占的,也保護(hù)了各自進(jìn)程不被其他進(jìn)程破壞,另外,他把主存看成磁盤的一個(gè)緩存,主存中僅保存活動(dòng)的程序段和數(shù)據(jù)段,當(dāng)主存中不存在數(shù)據(jù)的時(shí)候發(fā)生缺頁中斷,然后從磁盤加載進(jìn)來,當(dāng)物理內(nèi)存不足的時(shí)候會(huì)發(fā)生 swap 到磁盤。頁表保存了虛擬地址和物理地址的映射,頁表是一個(gè)數(shù)組,每個(gè)元素為一個(gè)頁的映射關(guān)系,這個(gè)映射關(guān)系可能是和主存地址,也可能和磁盤,頁表存儲(chǔ)在主存,我們將存儲(chǔ)在高速緩沖區(qū) cache  中的頁表稱為快表 TLAB 。

JVM和操作系統(tǒng)交互的方法是什么

enter image description here

裝入位 表示對(duì)于頁是否在主存,如果地址頁每頁表示,數(shù)據(jù)還在磁盤

  • 存放位置 建立虛擬頁和物理頁的映射,用于地址轉(zhuǎn)換,如果為null表示是一個(gè)未分配頁
  • 修改位 用來存儲(chǔ)數(shù)據(jù)是否修改過
  • 權(quán)限位 用來控制是否有讀寫權(quán)限
  • 禁止緩存位 主要用來保證 cache 主存 磁盤的數(shù)據(jù)一致性
內(nèi)存映射
正常情況下,我們讀取文件的流程為,先通過系統(tǒng)調(diào)用從磁盤讀取數(shù)據(jù),存入操作系統(tǒng)的內(nèi)核緩沖區(qū),然后在從內(nèi)核緩沖區(qū)拷貝到用戶空間,而內(nèi)存映射,是將磁盤文件直接映射到用戶的虛擬存儲(chǔ)空間中,通過頁表維護(hù)虛擬地址到磁盤的映射,通過內(nèi)存映射的方式讀取文件的好處有,因?yàn)闇p少了從內(nèi)核緩沖區(qū)到用戶空間的拷貝,直接從磁盤讀取數(shù)據(jù)到內(nèi)存,減少了系統(tǒng)調(diào)用的開銷,對(duì)用戶而言,仿佛直接操作的磁盤上的文件,另外由于使用了虛擬存儲(chǔ),所以不需要連續(xù)的主存空間來存儲(chǔ)數(shù)據(jù)。

JVM和操作系統(tǒng)交互的方法是什么

enter image description here

在 Java 中,我們使用 MappedByteBuffer 來實(shí)現(xiàn)內(nèi)存映射,這是一個(gè)堆外內(nèi)存,在映射完之后,并沒有立即占有物理內(nèi)存,而是訪問數(shù)據(jù)頁的時(shí)候,先查頁表,發(fā)現(xiàn)還沒加載,發(fā)起缺頁異常,然后在從磁盤將數(shù)據(jù)加載進(jìn)內(nèi)存,所以一些對(duì)實(shí)時(shí)性要求很高的中間件,例如rocketmq,消息存儲(chǔ)在一個(gè)大小為1G的文件中,為了加快讀寫速度,會(huì)將這個(gè)文件映射到內(nèi)存后,在每個(gè)頁寫一比特?cái)?shù)據(jù),這樣就可以把整個(gè)1G文件都加載進(jìn)內(nèi)存,在實(shí)際讀寫的時(shí)候就不會(huì)發(fā)生缺頁了,這個(gè)在rocketmq內(nèi)部叫做文件預(yù)熱。

下面我們貼一段 rocketmq 消息存儲(chǔ)模塊的代碼,位于 MappedFile 類中,這個(gè)類是 rocketMq 消息存儲(chǔ)的核心類感興趣的可以自行研究,下面兩個(gè)方法一個(gè)是創(chuàng)建文件映射,一個(gè)是預(yù)熱文件,每預(yù)熱 1000 個(gè)數(shù)據(jù)頁,就讓出 CPU 權(quán)限。

 private void init(final String fileName, final int fileSize) throws IOException {
        this.fileName = fileName;
        this.fileSize = fileSize;
        this.file = new File(fileName);
        this.fileFromOffset = Long.parseLong(this.file.getName());
        boolean ok = false;

        ensureDirOK(this.file.getParent());

        try {
            this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
            this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
            TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
            TOTAL_MAPPED_FILES.incrementAndGet();
            ok = true;
        } catch (FileNotFoundException e) {
            log.error("create file channel " + this.fileName + " Failed. ", e);
            throw e;
        } catch (IOException e) {
            log.error("map file " + this.fileName + " Failed. ", e);
            throw e;
        } finally {
            if (!ok && this.fileChannel != null) {
                this.fileChannel.close();
            }
        }
    }


//文件預(yù)熱,OS_PAGE_SIZE = 4kb 相當(dāng)于每 4kb 就寫一個(gè) byte 0 ,將所有的頁都加載到內(nèi)存,真正使用的時(shí)候就不會(huì)發(fā)生缺頁異常了
 public void warmMappedFile(FlushDiskType type, int pages) {
        long beginTime = System.currentTimeMillis();
        ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
        int flush = 0;
        long time = System.currentTimeMillis();
        for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
            byteBuffer.put(i, (byte) 0);
            // force flush when flush disk type is sync
            if (type == FlushDiskType.SYNC_FLUSH) {
                if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
                    flush = i;
                    mappedByteBuffer.force();
                }
            }

            // prevent gc
            if (j % 1000 == 0) {
                log.info("j={}, costTime={}", j, System.currentTimeMillis() - time);
                time = System.currentTimeMillis();
                try {
                // 這里sleep(0),讓線程讓出 CPU 權(quán)限,供其他更高優(yōu)先級(jí)的線程執(zhí)行,此線程從運(yùn)行中轉(zhuǎn)換為就緒
                    Thread.sleep(0);
                } catch (InterruptedException e) {
                    log.error("Interrupted", e);
                }
            }
        }

        // force flush when prepare load finished
        if (type == FlushDiskType.SYNC_FLUSH) {
            log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}",
                this.getFileName(), System.currentTimeMillis() - beginTime);
            mappedByteBuffer.force();
        }
        log.info("mapped file warm-up done. mappedFile={}, costTime={}", this.getFileName(),
            System.currentTimeMillis() - beginTime);

        this.mlock();
    }
JVM 中對(duì)象的內(nèi)存布局

在linux中只要知道一個(gè)變量的起始地址就可以讀出這個(gè)變量的值,因?yàn)閺倪@個(gè)起始地址起前8位記錄了變量的大小,也就是可以定位到結(jié)束地址,在 Java 中我們可以通過 Field.get(object) 的方式獲取變量的值,也就是反射,最終是通過 UnSafe 類來實(shí)現(xiàn)的。我們可以分析下具體代碼。

 Field 對(duì)象的 getInt方法  先安全檢查 ,然后調(diào)用 FieldAccessor
    @CallerSensitive
    public int getInt(Object obj)
        throws IllegalArgumentException, IllegalAccessException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        return getFieldAccessor(obj).getInt(obj);
    }


 獲取field在所在對(duì)象中的地址的偏移量 fieldoffset
    UnsafeFieldAccessorImpl(Field var1) {
            this.field = var1;
            if(Modifier.isStatic(var1.getModifiers())) {
                this.fieldOffset = unsafe.staticFieldOffset(var1);
            } else {
                this.fieldOffset = unsafe.objectFieldOffset(var1);
            }

            this.isFinal = Modifier.isFinal(var1.getModifiers());
     }


 UnsafeStaticIntegerFieldAccessorImpl 調(diào)用unsafe中的方法
     public int getInt(Object var1) throws IllegalArgumentException {
          return unsafe.getInt(this.base, this.fieldOffset);
     }
通過上面的代碼我們可以通過屬性相對(duì)對(duì)象起始地址的偏移量,來讀取和寫入屬性的值,這也是 Java 反射的原理,這種模式在jdk中很多場(chǎng)景都有用到,例如LockSupport.park中設(shè)置阻塞對(duì)象。 那么屬性的偏移量具體根據(jù)什么規(guī)則來確定的呢? 下面我們借此機(jī)會(huì)分析下 Java 對(duì)象的內(nèi)存布局。

在 Java 虛擬機(jī)中,每個(gè) Java 對(duì)象都有一個(gè)對(duì)象頭 (object header) ,由標(biāo)記字段和類型指針構(gòu)成,標(biāo)記字段用來存儲(chǔ)對(duì)象的哈希碼, GC 信息, 持有的鎖信息,而類型指針指向該對(duì)象的類 Class ,在 64 位操作系統(tǒng)中,標(biāo)記字段占有 64 位,而類型指針也占 64 位,也就是說一個(gè)  Java  對(duì)象在什么屬性都沒有的情況下要占有 16 字節(jié)的空間,當(dāng)前 JVM 中默認(rèn)開啟了壓縮指針,這樣類型指針可以只占 32 位,所以對(duì)象頭占 12 字節(jié), 壓縮指針可以作用于對(duì)象頭,以及引用類型的字段。

JVM 為了內(nèi)存對(duì)齊,會(huì)對(duì)字段進(jìn)行重排序,這里的對(duì)齊主要指  Java  虛擬機(jī)堆中的對(duì)象的起始地址為 8 的倍數(shù),如果一個(gè)對(duì)象用不到 8N 個(gè)字節(jié),那么剩下的就會(huì)被填充,另外子類繼承的屬性的偏移量和父類一致,以 Long 為例,他只有一個(gè)非 static 屬性 value ,而盡管對(duì)象頭只占有 12 字節(jié),而屬性 value 的偏移量只能是 16, 其中 4 字節(jié)只能浪費(fèi)掉,所以字段重排就是為了避免內(nèi)存浪費(fèi), 所以我們很難在 Java 字節(jié)碼被加載之前分析出這個(gè) Java 對(duì)象占有的實(shí)際空間有多大,我們只能通過遞歸父類的所有屬性來預(yù)估對(duì)象大小,而真實(shí)占用的大小可以通過  Java agent 中的 Instrumentation獲取。

當(dāng)然內(nèi)存對(duì)齊另外一個(gè)原因是為了讓字段只出現(xiàn)在同一個(gè) CPU 的緩存行中,如果字段不對(duì)齊,就有可能出現(xiàn)一個(gè)字段的一部分在緩存行 1 中,而剩下的一半在 緩存行 2 中,這樣該字段的讀取需要替換兩個(gè)緩存行,而字段的寫入會(huì)導(dǎo)致兩個(gè)緩存行上緩存的其他數(shù)據(jù)都無效,這樣會(huì)影響程序性能。

通過內(nèi)存對(duì)齊可以避免一個(gè)字段同時(shí)存在兩個(gè)緩存行里的情況,但還是無法完全規(guī)避緩存?zhèn)喂蚕淼膯栴},也就是一個(gè)緩存行中存了多個(gè)變量,而這幾個(gè)變量在多核 CPU 并行的時(shí)候,會(huì)導(dǎo)致競(jìng)爭(zhēng)緩存行的寫權(quán)限,當(dāng)其中一個(gè) CPU 寫入數(shù)據(jù)后,這個(gè)字段對(duì)應(yīng)的緩存行將失效,導(dǎo)致這個(gè)緩存行的其他字段也失效。

JVM和操作系統(tǒng)交互的方法是什么

enter image description here

在 Disruptor 中,通過填充幾個(gè)無意義的字段,讓對(duì)象的大小剛好在 64 字節(jié),一個(gè)緩存行的大小為64字節(jié),這樣這個(gè)緩存行就只會(huì)給這一個(gè)變量使用,從而避免緩存行偽共享,但是在 jdk7 中,由于無效字段被清除導(dǎo)致該方法失效,只能通過繼承父類字段來避免填充字段被優(yōu)化,而 jdk8 提供了注解@Contended 來標(biāo)示這個(gè)變量或?qū)ο髮ⅹ?dú)享一個(gè)緩存行,使用這個(gè)注解必須在 JVM 啟動(dòng)的時(shí)候加上 -XX:-RestrictContended 參數(shù),其實(shí)也是用空間換取時(shí)間。
jdk6  --- 32 位系統(tǒng)下    public final static class VolatileLong{        public volatile long value = 0L;        public long p1, p2, p3, p4, p5, p6; // 填充字段    }jdk7 通過繼承   public class VolatileLongPadding {       public volatile long p1, p2, p3, p4, p5, p6; // 填充字段   }   public class VolatileLong extends VolatileLongPadding {       public volatile long value = 0L;   }jdk8 通過注解   @Contended   public class VolatileLong {       public volatile long value = 0L;   }

NPTL和 Java 的線程模型


按照教科書的定義,進(jìn)程是資源管理的最小單位,而線程是 CPU 調(diào)度執(zhí)行的最小單位,線程的出現(xiàn)是為了減少進(jìn)程的上下文切換(線程的上下文切換比進(jìn)程小很多),以及更好適配多核心 CPU 環(huán)境,例如一個(gè)進(jìn)程下多個(gè)線程可以分別在不同的 CPU 上執(zhí)行,而多線程的支持,既可以放在Linux內(nèi)核實(shí)現(xiàn),也可以在核外實(shí)現(xiàn),如果放在核外,只需要完成運(yùn)行棧的切換,調(diào)度開銷小,但是這種方式無法適應(yīng)多 CPU 環(huán)境,底層的進(jìn)程還是運(yùn)行在一個(gè) CPU 上,另外由于對(duì)用戶編程要求高,所以目前主流的操作系統(tǒng)都是在內(nèi)核支持線程,而在Linux中,線程是一個(gè)輕量級(jí)進(jìn)程,只是優(yōu)化了線程調(diào)度的開銷。

而在 JVM 中的線程和內(nèi)核線程是一一對(duì)應(yīng)的,線程的調(diào)度完全交給了內(nèi)核,當(dāng)調(diào)用Thread.run 的時(shí)候,就會(huì)通過系統(tǒng)調(diào)用 fork() 創(chuàng)建一個(gè)內(nèi)核線程,這個(gè)方法會(huì)在用戶態(tài)和內(nèi)核態(tài)之間進(jìn)行切換,性能沒有在用戶態(tài)實(shí)現(xiàn)線程高,當(dāng)然由于直接使用內(nèi)核線程,所以能夠創(chuàng)建的最大線程數(shù)也受內(nèi)核控制。目前 Linux上 的線程模型為 NPTL ( Native POSIX Thread Library),他使用一對(duì)一模式,兼容 POSIX 標(biāo)準(zhǔn),沒有使用管理線程,可以更好地在多核 CPU 上運(yùn)行。
線程的狀態(tài)
對(duì)進(jìn)程而言,就三種狀態(tài),就緒,運(yùn)行,阻塞,而在 JVM 中,阻塞有四種類型,我們可以通過 jstack 生成 dump 文件查看線程的狀態(tài)。

  • BLOCKED (on object monitor)  通過 synchronized(obj) 同步塊獲取鎖的時(shí)候,等待其他線程釋放對(duì)象鎖,dump 文件會(huì)顯示 waiting to lock <0x00000000e1c9f108>
  • TIMED WAITING (on object monitor) 和 WAITING (on object monitor) 在獲取鎖后,調(diào)用了 object.wait() 等待其他線程調(diào)用 object.notify(),兩者區(qū)別是是否帶超時(shí)時(shí)間
  • TIMED WAITING (sleeping) 程序調(diào)用了 thread.sleep(),這里如果 sleep(0) 不會(huì)進(jìn)入阻塞狀態(tài),會(huì)直接從運(yùn)行轉(zhuǎn)換為就緒
  • TIMED WAITING (parking) 和 WAITING (parking) 程序調(diào)用了 Unsafe.park(),線程被掛起,等待某個(gè)條件發(fā)生,waiting on condition
而在 POSIX 標(biāo)準(zhǔn)中,thread_block 接受一個(gè)參數(shù) stat ,這個(gè)參數(shù)也有三種類型,TASK_BLOCKED, TASK_WAITING, TASK_HANGING,而調(diào)度器只會(huì)對(duì)線程狀態(tài)為 READY 的線程執(zhí)行調(diào)度,另外一點(diǎn)是線程的阻塞是線程自己操作的,相當(dāng)于是線程主動(dòng)讓出 CPU 時(shí)間片,所以等線程被喚醒后,他的剩余時(shí)間片不會(huì)變,該線程只能在剩下的時(shí)間片運(yùn)行,如果該時(shí)間片到期后線程還沒結(jié)束,該線程狀態(tài)會(huì)由 RUNNING 轉(zhuǎn)換為 READY ,等待調(diào)度器的下一次調(diào)度。

好了,關(guān)于線程就分析到這,關(guān)于 Java 并發(fā)包,核心都在 AQS 里,底層是通過 UnSafe類的 cas 方法,以及 park 方法實(shí)現(xiàn),后面我們?cè)谡視r(shí)間單獨(dú)分析,現(xiàn)在我們?cè)诳纯?Linux 的進(jìn)程同步方案。

POSIX表示可移植操作系統(tǒng)接口(Portable Operating System Interface of UNIX,縮寫為 POSIX ),POSIX標(biāo)準(zhǔn)定義了操作系統(tǒng)應(yīng)該為應(yīng)用程序提供的接口標(biāo)準(zhǔn)。

CAS 操作需要 CPU 支持,將比較 和 交換 作為一條指令來執(zhí)行, CAS 一般有三個(gè)參數(shù),內(nèi)存位置,預(yù)期原值,新值 ,所以UnSafe 類中的 compareAndSwap 用屬性相對(duì)對(duì)象初始地址的偏移量,來定位內(nèi)存位置。

線程的同步


線程同步出現(xiàn)的根本原因是訪問公共資源需要多個(gè)操作,而這多個(gè)操作的執(zhí)行過程不具備原子性,被任務(wù)調(diào)度器分開了,而其他線程會(huì)破壞共享資源,所以需要在臨界區(qū)做線程的同步,這里我們先明確一個(gè)概念,就是臨界區(qū),他是指多個(gè)任務(wù)訪問共享資源如內(nèi)存或文件時(shí)候的指令,他是指令并不是受訪問的資源。

POSIX 定義了五種同步對(duì)象,互斥鎖,條件變量,自旋鎖,讀寫鎖,信號(hào)量,這些對(duì)象在 JVM 中也都有對(duì)應(yīng)的實(shí)現(xiàn),并沒有全部使用 POSIX 定義的 api,通過 Java 實(shí)現(xiàn)靈活性更高,也避免了調(diào)用native方法的性能開銷,當(dāng)然底層最終都依賴于 pthread 的 互斥鎖 mutex 來實(shí)現(xiàn),這是一個(gè)系統(tǒng)調(diào)用,開銷很大,所以 JVM 對(duì)鎖做了自動(dòng)升降級(jí),基于AQS的實(shí)現(xiàn)以后在分析,這里主要說一下關(guān)鍵字 synchronized 。

當(dāng)聲明 synchronized 的代碼塊時(shí),編譯而成的字節(jié)碼會(huì)包含一個(gè) monitorenter 和 多個(gè) monitorexit (多個(gè)退出路徑,正常和異常情況),當(dāng)執(zhí)行 monitorenter 的時(shí)候會(huì)檢查目標(biāo)鎖對(duì)象的計(jì)數(shù)器是否為0,如果為0則將鎖對(duì)象的持有線程設(shè)置為自己,然后計(jì)數(shù)器加1,獲取到鎖,如果不為0則檢查鎖對(duì)象的持有線程是不是自己,如果是自己就將計(jì)數(shù)器加1獲取鎖,如果不是則阻塞等待,退出的時(shí)候計(jì)數(shù)器減1,當(dāng)減為0的時(shí)候清楚鎖對(duì)象的持有線程標(biāo)記,可以看出 synchronized 是支持可重入的。

剛剛說到線程的阻塞是一個(gè)系統(tǒng)調(diào)用,開銷大,所以 JVM 設(shè)計(jì)了自適應(yīng)自旋鎖,就是當(dāng)沒有獲取到鎖的時(shí)候, CPU 回進(jìn)入自旋狀態(tài)等待其他線程釋放鎖,自旋的時(shí)間主要看上次等待多長(zhǎng)時(shí)間獲取的鎖,例如上次自旋5毫秒沒有獲取鎖,這次就6毫秒,自旋會(huì)導(dǎo)致 CPU 空跑,另一個(gè)副作用就是不公平的鎖機(jī)制,因?yàn)樵摼€程自旋獲取到鎖,而其他正在阻塞的線程還在等待。除了自旋鎖, JVM 還通過 CAS 實(shí)現(xiàn)了輕量級(jí)鎖和偏向鎖來分別針對(duì)多個(gè)線程在不同時(shí)間訪問鎖和鎖僅會(huì)被一個(gè)線程使用的情況。后兩種鎖相當(dāng)于并沒有調(diào)用底層的信號(hào)量實(shí)現(xiàn)(通過信號(hào)量來控制線程A釋放了鎖例如調(diào)用了 wait(),而線程B就可以獲取鎖,這個(gè)只有內(nèi)核才能實(shí)現(xiàn),后面兩種由于場(chǎng)景里沒有競(jìng)爭(zhēng)所以也就不需要通過底層信號(hào)量控制),只是自己在用戶空間維護(hù)了鎖的持有關(guān)系,所以更高效。

JVM和操作系統(tǒng)交互的方法是什么

enter image description here

如上圖所示,如果線程進(jìn)入 monitorenter 會(huì)將自己放入該 objectmonitor 的 entryset 隊(duì)列,然后阻塞,如果當(dāng)前持有線程調(diào)用了 wait 方法,將會(huì)釋放鎖,然后將自己封裝成 objectwaiter 放入 objectmonitor 的 waitset 隊(duì)列,這時(shí)候 entryset 隊(duì)列里的某個(gè)線程將會(huì)競(jìng)爭(zhēng)到鎖,并進(jìn)入 active 狀態(tài),如果這個(gè)線程調(diào)用了 notify 方法,將會(huì)把 waitset 的第一個(gè) objectwaiter 拿出來放入 entryset (這個(gè)時(shí)候根據(jù)策略可能會(huì)先自旋),當(dāng)調(diào)用 notify 的那個(gè)線程執(zhí)行 moniterexit 釋放鎖的時(shí)候, entryset 里的線程就開始競(jìng)爭(zhēng)鎖后進(jìn)入 active 狀態(tài)。

為了讓應(yīng)用程序免于數(shù)據(jù)競(jìng)爭(zhēng)的干擾, Java  內(nèi)存模型中定義了 happen-before 來描述兩個(gè)操作的內(nèi)存可見性,也就是 X 操作 happen-before 操作 Y , 那么 X 操作結(jié)果 對(duì) Y 可見。

JVM 中針對(duì) volatile 以及 鎖 的實(shí)現(xiàn)有 happen-before 規(guī)則, JVM 底層通過插入內(nèi)存屏障來限制編譯器的重排序,以 volatile 為例,內(nèi)存屏障將不允許 在 volatile 字段寫操作之前的語句被重排序到寫操作后面 , 也不允許讀取 volatile 字段之后的語句被重排序帶讀取語句之前。插入內(nèi)存屏障的指令,會(huì)根據(jù)指令類型不同有不同的效果,例如在 monitorexit 釋放鎖后會(huì)強(qiáng)制刷新緩存,而 volatile 對(duì)應(yīng)的內(nèi)存屏障會(huì)在每次寫入后強(qiáng)制刷新到主存,并且由于 volatile 字段的特性,編譯器無法將其分配到寄存器,所以每次都是從主存讀取,所以 volatile 適用于讀多寫少得場(chǎng)景,最好只有個(gè)線程寫多個(gè)線程讀,如果頻繁寫入導(dǎo)致不停刷新緩存會(huì)影響性能。

關(guān)于應(yīng)用程序中設(shè)置多少線程數(shù)合適的問題,我們一般的做法是設(shè)置 CPU 最大核心數(shù) * 2 ,我們編碼的時(shí)候可能不確定運(yùn)行在什么樣的硬件環(huán)境中,可以通過 Runtime.getRuntime().availableProcessors() 獲取 CPU 核心。

但是具體設(shè)置多少線程數(shù),主要和線程內(nèi)運(yùn)行的任務(wù)中的阻塞時(shí)間有關(guān)系,如果任務(wù)中全部是計(jì)算密集型,那么只需要設(shè)置 CPU 核心數(shù)的線程就可以達(dá)到 CPU 利用率最高,如果設(shè)置的太大,反而因?yàn)榫€程上下文切換影響性能,如果任務(wù)中有阻塞操作,而在阻塞的時(shí)間就可以讓 CPU 去執(zhí)行其他線程里的任務(wù),我們可以通過 線程數(shù)量=內(nèi)核數(shù)量 / (1 - 阻塞率)這個(gè)公式去計(jì)算最合適的線程數(shù),阻塞率我們可以通過計(jì)算任務(wù)總的執(zhí)行時(shí)間和阻塞的時(shí)間獲得。

目前微服務(wù)架構(gòu)下有大量的RPC調(diào)用,所以利用多線程可以大大提高執(zhí)行效率,我們可以借助分布式鏈路監(jiān)控來統(tǒng)計(jì)RPC調(diào)用所消耗的時(shí)間,而這部分時(shí)間就是任務(wù)中阻塞的時(shí)間,當(dāng)然為了做到極致的效率最大,我們需要設(shè)置不同的值然后進(jìn)行測(cè)試。

Java 中如何實(shí)現(xiàn)定時(shí)任務(wù)

定時(shí)器已經(jīng)是現(xiàn)代軟件中不可缺少的一部分,例如每隔5秒去查詢一下狀態(tài),是否有新郵件,實(shí)現(xiàn)一個(gè)鬧鐘等, Java  中已經(jīng)有現(xiàn)成的 api 供使用,但是如果你想設(shè)計(jì)更高效,更精準(zhǔn)的定時(shí)器任務(wù),就需要了解底層的硬件知識(shí),比如實(shí)現(xiàn)一個(gè)分布式任務(wù)調(diào)度中間件,你可能要考慮到各個(gè)應(yīng)用間時(shí)鐘同步的問題。


Java 中我們要實(shí)現(xiàn)定時(shí)任務(wù),有兩種方式,一種通過 timer 類, 另外一種是 JUC 中的 ScheduledExecutorService ,不知道大家有沒有好奇 JVM 是如何實(shí)現(xiàn)定時(shí)任務(wù)的,難道一直輪詢時(shí)間,看是否時(shí)間到了,如果到了就調(diào)用對(duì)應(yīng)的處理任務(wù),但是這種一直輪詢不釋放 CPU 肯定是不可取的,要么就是線程阻塞,等到時(shí)間到了在來喚醒線程,那么 JVM 怎么知道時(shí)間到了,如何喚醒呢?

首先我們翻一下 JDK ,發(fā)現(xiàn)和時(shí)間相關(guān)的 API 大概有3處,而且這 3 處還都對(duì)時(shí)間的精度做了區(qū)分:

object.wait(long millisecond)  參數(shù)是毫秒,必須大于等于 0 ,如果等于 0 ,就一直阻塞直到其他線程來喚醒 ,timer 類就是通過 wait() 方法來實(shí)現(xiàn),下面我們看一下wait的另外一個(gè)方法:
public final void wait(long timeout, int nanos) throws InterruptedException {
     if (timeout < 0) {
         throw new IllegalArgumentException("timeout value is negative");
     }
     if (nanos < 0 || nanos > 999999) {
         throw new IllegalArgumentException(
                             "nanosecond timeout value out of range");
     }
     if (nanos > 0) {
         timeout++;
     }
     wait(timeout);
 }
這個(gè)方法是想提供一個(gè)可以支持納秒級(jí)的超時(shí)時(shí)間,然而只是粗暴的加 1 毫秒。

Thread.sleep(long millisecond) 目前一般通過這種方式釋放 CPU ,如果參數(shù)為 0 ,表示釋放 CPU 給更高優(yōu)先級(jí)的線程,自己從運(yùn)行狀態(tài)轉(zhuǎn)換為可運(yùn)行態(tài)等待 CPU 調(diào)度,他也提供了一個(gè)可以支持納秒級(jí)的方法實(shí)現(xiàn),跟 wait 額區(qū)別是它通過 500000 來分隔是否要加 1 毫秒。
public static void sleep(long millis, int nanos)
 throws InterruptedException {
     if (millis < 0) {
         throw new IllegalArgumentException("timeout value is negative");
     }
     if (nanos < 0 || nanos > 999999) {
         throw new IllegalArgumentException(
                             "nanosecond timeout value out of range");
     }
     if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
         millis++;
     }
     sleep(millis);
 }
LockSupport.park(long nans)   Condition.await()調(diào)用的該方法, ScheduledExecutorService 用的 condition.await() 來實(shí)現(xiàn)阻塞一定的超時(shí)時(shí)間,其他帶超時(shí)參數(shù)的方法也都通過他來實(shí)現(xiàn),目前大多定時(shí)器都是通過這個(gè)方法來實(shí)現(xiàn)的,該方法也提供了一個(gè)布爾值來確定時(shí)間的精度。

System.currentTimeMillis() 以及 System.nanoTime() 這兩種方式都依賴于底層操作系統(tǒng),前者是毫秒級(jí),經(jīng)測(cè)試 windows 平臺(tái)的頻率可能超過 10ms ,而后者是納秒級(jí)別,頻率在 100ns 左右,所以如果要獲取更精準(zhǔn)的時(shí)間建議用后者好了,api 了解完了,我們來看下定時(shí)器的底層是怎么實(shí)現(xiàn)的,現(xiàn)代PC機(jī)中有三種硬件時(shí)鐘的實(shí)現(xiàn),他們都是通過晶體振動(dòng)產(chǎn)生的方波信號(hào)輸入來完成時(shí)鐘信號(hào)同步的。
  • 實(shí)時(shí)時(shí)鐘 RTC ,用于長(zhǎng)時(shí)間存放系統(tǒng)時(shí)間的設(shè)備,即使關(guān)機(jī)也可以依靠主板中的電池繼續(xù)計(jì)時(shí)。Linux 啟動(dòng)的時(shí)候會(huì)從 RTC 中讀取時(shí)間和日期作為初始值,之后在運(yùn)行期間通過其他計(jì)時(shí)器去維護(hù)系統(tǒng)時(shí)間。
  • 可編程間隔定時(shí)器 PIT ,該計(jì)數(shù)器會(huì)有一個(gè)初始值,每過一個(gè)時(shí)鐘周期,該初始值會(huì)減1,當(dāng)該初始值被減到0時(shí),就通過導(dǎo)線向 CPU 發(fā)送一個(gè)時(shí)鐘中斷, CPU 就可以執(zhí)行對(duì)應(yīng)的中斷程序,也就是回調(diào)對(duì)應(yīng)的任務(wù)
  • 時(shí)間戳計(jì)數(shù)器 TSC , 所有的 Intel8086 CPU 中都包含一個(gè)時(shí)間戳計(jì)數(shù)器對(duì)應(yīng)的寄存器,該寄存器的值會(huì)在每次 CPU 收到一個(gè)時(shí)鐘周期的中斷信號(hào)后就會(huì)加 1 。他比 PIT 精度高,但是不能編程,只能讀取。
時(shí)鐘周期:硬件計(jì)時(shí)器在多長(zhǎng)時(shí)間內(nèi)產(chǎn)生時(shí)鐘脈沖,而時(shí)鐘周期頻率為1秒內(nèi)產(chǎn)生時(shí)鐘脈沖的個(gè)數(shù)。目前通常為1193180。

時(shí)鐘滴答:當(dāng)PIT中的初始值減到0的時(shí)候,就會(huì)產(chǎn)生一次時(shí)鐘中斷,這個(gè)初始值由編程的時(shí)候指定。

Linux啟動(dòng)的時(shí)候,先通過 RTC 獲取初始時(shí)間,之后內(nèi)核通過 PIT 中的定時(shí)器的時(shí)鐘滴答來維護(hù)日期,并且會(huì)定時(shí)將該日期寫入 RTC,而應(yīng)用程序的定時(shí)器主要是通過設(shè)置 PIT 的初始值設(shè)置的,當(dāng)初始值減到0的時(shí)候,就表示要執(zhí)行回調(diào)函數(shù)了,這里大家會(huì)不會(huì)有疑問,這樣同一時(shí)刻只能有一個(gè)定時(shí)器程序了,而我們?cè)趹?yīng)用程序中,以及多個(gè)應(yīng)用程序之間,肯定有好多定時(shí)器任務(wù),其實(shí)我們可以參考  ScheduledExecutorService 的實(shí)現(xiàn)。

只需要將這些定時(shí)任務(wù)按照時(shí)間做一個(gè)排序,越靠前待執(zhí)行的任務(wù)放在前面,第一個(gè)任務(wù)到了在設(shè)置第二個(gè)任務(wù)相對(duì)當(dāng)前時(shí)間的值,畢竟 CPU 同一時(shí)刻也只能運(yùn)行一個(gè)任務(wù),關(guān)于時(shí)間的精度問題,我們無法在軟件層面做的完全精準(zhǔn),畢竟 CPU 的調(diào)度不完全受用戶程序控制,當(dāng)然更大的依賴是硬件的時(shí)鐘周期頻率,目前 TSC 可以提高更高的精度。

現(xiàn)在我們知道了,Java 中的超時(shí)時(shí)間,是通過可編程間隔定時(shí)器設(shè)置一個(gè)初始值然后等待中斷信號(hào)實(shí)現(xiàn)的,精度上受硬件時(shí)鐘周期的影響,一般為毫秒級(jí)別,畢竟1納秒光速也只有3米,所以 JDK 中帶納秒?yún)?shù)的實(shí)現(xiàn)都是粗暴做法,預(yù)留著等待精度更高的定時(shí)器出現(xiàn),而獲取當(dāng)前時(shí)間 System.currentTimeMillis() 效率會(huì)更高,但他是毫秒級(jí)精度,他讀取的 Linux 內(nèi)核維護(hù)的日期,而 System.nanoTime()  會(huì)優(yōu)先使用 TSC ,性能稍微低一點(diǎn),但他是納秒級(jí),Random 類為了防止沖突就用nanoTime生成種子。Java 如何和外部設(shè)備通信

計(jì)算機(jī)的外部設(shè)備有鼠標(biāo)、鍵盤、打印機(jī)、網(wǎng)卡等,通常我們將外部設(shè)備和和主存之間的信息傳遞稱為 I/O 操作 , 按操作特性可以分為,輸出型設(shè)備,輸入型設(shè)備,存儲(chǔ)設(shè)備。現(xiàn)代設(shè)備都采用通道方式和主存進(jìn)行交互,通道是一個(gè)專門用來處理IO任務(wù)的設(shè)備, CPU 在處理主程序時(shí)遇到I/O請(qǐng)求,啟動(dòng)指定通道上選址的設(shè)備,一旦啟動(dòng)成功,通道開始控制設(shè)備進(jìn)行操作,而 CPU 可以繼續(xù)執(zhí)行其他任務(wù),I/O 操作完成后,通道發(fā)出 I/O 操作結(jié)束的中斷,處理器轉(zhuǎn)而處理 IO 結(jié)束后的事件。其他處理 IO 的方式,例如輪詢、中斷、DMA,在性能上都不見通道,這里就不介紹了。當(dāng)然  Java  程序和外部設(shè)備通信也是通過系統(tǒng)調(diào)用完成,這里也不在繼續(xù)深入了。

感謝各位的閱讀,以上就是“JVM和操作系統(tǒng)交互的方法是什么”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)JVM和操作系統(tǒng)交互的方法是什么這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

jvm
AI