JVM是怎麼和作業系統互動的?
來源:阿里巴巴中介軟體
肉眼看計算機是由 CPU 、記憶體、顯示器這些硬體裝置組成,但大部分人從事的是軟體開發工作。計算機底層原理就是連通硬體和軟體的橋樑,理解計算機底層原理才能在程式設計這條路上越走越快,越走越輕鬆。從作業系統層面去理解高階程式語言的執行過程,會發現好多軟體設計都是同一種套路,很多語言特性都依賴於底層機制,今天為你一一揭秘。
根據馮·諾依曼思想,計算機採用二進位制作為數制基礎,必須包含:運算器、控制器、儲存裝置,以及輸入輸出裝置,如下圖所示。
當程式被載入進記憶體後,指令就在記憶體中了,這個時候說的記憶體是獨立於 CPU 外的主存裝置,也就是 PC 機中的記憶體條,指令指標暫存器IP 指向記憶體中下一條待執行指令的地址,控制單元根據 IP暫存器的指向,將主存中的指令裝載到指令暫存器。
enter image description here
這裡解釋下上圖中 CPU 內部整合的儲存單元 SRAM ,正好和主存中的 DRAM 對應, RAM 是隨機訪問記憶體,就是給一個地址就能訪問到資料,而磁碟這種儲存媒介必須順序訪問,而 RAM 又分為動態和靜態兩種,靜態 RAM 由於整合度較低,一般容量小,速度快,而動態 RAM 整合度較高,主要透過給電容充電和放電實現,速度沒有靜態 RAM 快,所以一般將動態 RAM 做為主存,而靜態 RAM 作為 CPU 和主存之間的快取記憶體 (cache),用來遮蔽 CPU 和主存速度上的差異,也就是我們經常看到的 L1 , L2 快取。每一級別快取速度變低,容量變大。
下圖展示了儲存器的層次化架構,以及 CPU 訪問主存的過程,這裡有兩個知識點,一個是多級快取之間為保證資料的一致性,而推出的快取一致性協議,具體可以參考這篇文章,另外一個知識點是, cache 和主存的對映,首先要明確的是 cahce 快取的單位是快取行,對應主存中的一個記憶體塊,並不是一個變數,這個主要是因為 CPU 訪問的空間侷限性:被訪問的某個儲存單元,在一個較短時間內,很有可能再次被訪問到,以及空間侷限性:被訪問的某個儲存單元,在較短時間內,他的相鄰儲存單元也會被訪問到。
而對映方式有很多種,類似於 cache 行號 = 主存塊號 mod cache總行數 ,這樣每次獲取到一個主存地址,根據這個地址計算出在主存中的塊號就可以計算出在 cache 中的行號。
Java 是一門高階語言,這類語言不能直接執行在硬體上,必須執行在能夠識別 Java 語言特性的虛擬機器上,而 Java 程式碼必須透過 Java 編譯器將其轉換成虛擬機器所能識別的指令序列,也稱為 Java 位元組碼,之所以稱為位元組碼是因為 Java 位元組碼的操作指令(OpCode)被固定為一個位元組,以下為 System.out.println("Hello world") 編譯後的位元組碼:
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
最左列是偏移;中間列是給虛擬機器讀的位元組碼;最右列是高階語言的程式碼,下面是透過組合語言轉換成的機器指令,中間是機器碼,第三列為對應的機器指令,最後一列是對應的彙編程式碼:
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 ; 呼叫 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
中斷
從 Linux 記憶體管理角度理解 JVM 記憶體模型
程式上下文
虛擬儲存
enter image description here
裝入位 表示對於頁是否在主存,如果地址頁每頁表示,資料還在磁碟
存放位置 建立虛擬頁和物理頁的對映,用於地址轉換,如果為null表示是一個未分配頁 修改位 用來儲存資料是否修改過 許可權位 用來控制是否有讀寫許可權 禁止快取位 主要用來保證 cache 主存 磁碟的資料一致性
記憶體對映
下面我們貼一段 rocketmq 訊息儲存模組的程式碼,位於 MappedFile 類中,這個類是 rocketMq 訊息儲存的核心類感興趣的可以自行研究,下面兩個方法一個是建立檔案對映,一個是預熱檔案,每預熱 1000 個資料頁,就讓出 CPU 許可權。
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(); } } } //檔案預熱,OS_PAGE_SIZE = 4kb 相當於每 4kb 就寫一個 byte 0 ,將所有的頁都載入到記憶體,真正使用的時候就不會發生缺頁異常了 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 許可權,供其他更高優先順序的執行緒執行,此執行緒從執行中轉換為就緒 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 中物件的記憶體佈局
在linux中只要知道一個變數的起始地址就可以讀出這個變數的值,因為從這個起始地址起前8位記錄了變數的大小,也就是可以定位到結束地址,在 Java 中我們可以透過 Field.get(object) 的方式獲取變數的值,也就是反射,最終是透過 UnSafe 類來實現的。我們可以分析下具體程式碼。
Field 物件的 getInt方法 先安全檢查 ,然後呼叫 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在所在物件中的地址的偏移量 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 呼叫unsafe中的方法 public int getInt(Object var1) throws IllegalArgumentException { return unsafe.getInt(this.base, this.fieldOffset); }
當然記憶體對齊另外一個原因是為了讓欄位只出現在同一個 CPU 的快取行中,如果欄位不對齊,就有可能出現一個欄位的一部分在快取行 1 中,而剩下的一半在 快取行 2 中,這樣該欄位的讀取需要替換兩個快取行,而欄位的寫入會導致兩個快取行上快取的其他資料都無效,這樣會影響程式效能。
jdk6 --- 32 位系統下
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 透過註解
public class VolatileLong {
public volatile long value = 0L;
}
NPTL和 Java 的執行緒模型
按照教科書的定義,程式是資源管理的最小單位,而執行緒是 CPU 排程執行的最小單位,執行緒的出現是為了減少程式的上下文切換(執行緒的上下文切換比程式小很多),以及更好適配多核心 CPU 環境,例如一個程式下多個執行緒可以分別在不同的 CPU 上執行,而多執行緒的支援,既可以放在Linux核心實現,也可以在核外實現,如果放在核外,只需要完成執行棧的切換,排程開銷小,但是這種方式無法適應多 CPU 環境,底層的程式還是執行在一個 CPU 上,另外由於對使用者程式設計要求高,所以目前主流的作業系統都是在核心支援執行緒,而在Linux中,執行緒是一個輕量級程式,只是最佳化了執行緒排程的開銷。
執行緒的狀態
BLOCKED (on object monitor) 透過 synchronized(obj) 同步塊獲取鎖的時候,等待其他執行緒釋放物件鎖,dump 檔案會顯示 waiting to lock <0x00000000e1c9f108> TIMED WAITING (on object monitor) 和 WAITING (on object monitor) 在獲取鎖後,呼叫了 object.wait() 等待其他執行緒呼叫 object.notify(),兩者區別是是否帶超時時間 TIMED WAITING (sleeping) 程式呼叫了 thread.sleep(),這裡如果 sleep(0) 不會進入阻塞狀態,會直接從執行轉換為就緒 TIMED WAITING (parking) 和 WAITING (parking) 程式呼叫了 Unsafe.park(),執行緒被掛起,等待某個條件發生,waiting on condition
執行緒的同步
執行緒同步出現的根本原因是訪問公共資源需要多個操作,而這多個操作的執行過程不具備原子性,被任務排程器分開了,而其他執行緒會破壞共享資源,所以需要在臨界區做執行緒的同步,這裡我們先明確一個概念,就是臨界區,他是指多個任務訪問共享資源如記憶體或檔案時候的指令,他是指令並不是受訪問的資源。
定時器已經是現代軟體中不可缺少的一部分,例如每隔5秒去查詢一下狀態,是否有新郵件,實現一個鬧鐘等, Java 中已經有現成的 api 供使用,但是如果你想設計更高效,更精準的定時器任務,就需要了解底層的硬體知識,比如實現一個分散式任務排程中介軟體,你可能要考慮到各個應用間時鐘同步的問題。
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); }
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); }
實時時鐘 RTC ,用於長時間存放系統時間的裝置,即使關機也可以依靠主機板中的電池繼續計時。Linux 啟動的時候會從 RTC 中讀取時間和日期作為初始值,之後在執行期間透過其他計時器去維護系統時間。 可程式設計間隔定時器 PIT ,該計數器會有一個初始值,每過一個時鐘週期,該初始值會減1,當該初始值被減到0時,就透過導線向 CPU 傳送一個時鐘中斷, CPU 就可以執行對應的中斷程式,也就是回撥對應的任務 時間戳計數器 TSC , 所有的 Intel8086 CPU 中都包含一個時間戳計數器對應的暫存器,該暫存器的值會在每次 CPU 收到一個時鐘週期的中斷訊號後就會加 1 。他比 PIT 精度高,但是不能程式設計,只能讀取。
計算機的外部裝置有滑鼠、鍵盤、印表機、網路卡等,通常我們將外部裝置和和主存之間的資訊傳遞稱為 I/O 操作 , 按操作特性可以分為,輸出型裝置,輸入型裝置,儲存裝置。現代裝置都採用通道方式和主存進行互動,通道是一個專門用來處理IO任務的裝置, CPU 在處理主程式時遇到I/O請求,啟動指定通道上選址的裝置,一旦啟動成功,通道開始控制裝置進行操作,而 CPU 可以繼續執行其他任務,I/O 操作完成後,通道發出 I/O 操作結束的中斷,處理器轉而處理 IO 結束後的事件。其他處理 IO 的方式,例如輪詢、中斷、DMA,在效能上都不見通道,這裡就不介紹了。當然 Java 程式和外部裝置通訊也是透過系統呼叫完成,這裡也不在繼續深入了。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69940568/viewspace-2666276/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java 和作業系統互動細節Java作業系統
- Java和作業系統互動細節Java作業系統
- 作業系統是什麼?Linux是什麼作業系統?作業系統Linux
- Java 與底層作業系統的互動細節Java作業系統
- 地面互動投影系統原理是什麼?
- Netflix是怎麼做互動劇的?
- 無作業系統時的裝置驅動和有作業系統時的裝置驅動作業系統
- 外甥問我什麼是作業系統?作業系統
- 製造業ERP系統運作流程是怎樣的
- Linux是什麼樣的作業系統?是免費的嗎?Linux作業系統
- gops 是怎麼和 Go 的執行時進行互動的?Go
- 探索作業系統:核心、啟動和系統呼叫的奧秘作業系統
- 作業系統--怎麼實現中斷作業系統
- Linux作業系統的安全性怎麼樣?Linux作業系統
- 作業系統——裝置驅動和檔案系統作業系統
- Linux作業系統怎麼樣?Linux技術怎麼學Linux作業系統
- linux是什麼作業系統 linux和windows的區別詳細說明Linux作業系統Windows
- 伺服器作業系統windows和linux怎麼選擇合適自己的伺服器作業系統WindowsLinux
- 在Linux中,什麼是Linux作業系統,它的特點是什麼?Linux作業系統
- # MySQL server 層和儲存引擎層是怎麼互動資料的?MySqlServer儲存引擎
- 都談一談你是怎麼在 Linux 桌面作業系統上做 UI 自動化測試的?Linux作業系統UI
- Linux作業系統的優勢是什麼?Linux入門Linux作業系統
- 什麼是Linux?作為熱門的作業系統你瞭解嗎?Linux作業系統
- Linux是什麼作業系統?你瞭解多少?Linux作業系統
- Linux命令列作用和意義是什麼?學習linux作業系統Linux命令列作業系統
- 作業系統啟動的過程作業系統
- IBM的作業系統和硬體IBM作業系統
- 大學四年我是怎麼寫作業系統和計算機網路的?掏心掏肺的分享!作業系統計算機網路
- 樹莓派作業系統安裝和啟動樹莓派作業系統
- 飛機上一般是什麼作業系統?作業系統
- 什麼是Linux作業系統?有哪些主要特點?Linux作業系統
- Unix和Linux作業系統有什麼區別Linux作業系統
- 雲伺服器裡面的作業系統怎麼選?伺服器作業系統
- 作業系統(1)——作業系統概述作業系統
- 作業系統(一):作業系統概述作業系統
- jvm、gc、作業系統等基礎知識總結JVMGC作業系統
- Linux作業系統的認識和使用Linux作業系統
- 【作業系統2】作業系統啟動過程與異常/中斷,系統呼叫作業系統