歡迎閱讀Java工程師知識體系文章
曾經多次想過梳理Java工程師知識體系,但一直以來都沒有實踐,理由很多:1.沒時間,天天工作還得加班;2.太累,工作加上基本生活勞動;3.拿來主義,網上有很多牛人整理過了搜索一下就可以獲得等等,可是終究還是無法抗拒內心的慾望,勇敢開始了這第一步。這是我第一次落實整理 Java工程師知識體系 ,歡迎閱讀,如有紕漏歡迎指正,如有疑問歡迎留言交流。
硬件基礎
程序的機器級表示
程序編碼
在《【深入理解計算機系統·筆記】GCC編譯過程理解》一文中已詳細講解過如何獲取C語言文件的預編譯文件、彙編文件、機器碼文件,這裏我們需要使用到彙編文件,因此需要用到的命令是:
gcc -S xxx.c -o xxx.s
另外還可以加入-Og 選項(GCC 4.8以上版本支持)來告訴編譯器生成符合原始C代碼整體結構的及其代碼的優化等級,使用較高級別優化產生的代碼會嚴重變形,以至於難以理解,因此我們使用-Og優化級別作爲學習工具,其他-O1,-O2是較高級別的優化,在編譯實際使用的程序時比較推薦。例如:
gcc -Og -S xxx.c -o xxx.s
注:彙編語言只是一種助記符,如需變成可執行的機器碼,還需要經過彙編、鏈接操作。
首先我們再來看一個簡單的C程序addtwonum.c:
#include <stdio.h>
int x;
int y;
int addtwonum()
{
x = 1;
y = 2;
return x + y;
}
int main()
{
int result;
result = addtwonum();
printf("result 爲: %d", result);
return 0;
}
程序編寫了一個函數addtwonum(),對兩個整型數進行求和,在執行以下命令後:
gcc -Og -S addtwonum.c -o addtwonum.s
我們得到如下文件內容,其中以“.”開頭的行都是指導彙編器和鏈接器工作的僞指令,其他的每一行都是一條可執行指令的彙編表示:
.file "addtwonum.c"
.text
.globl addtwonum
.type addtwonum, @function
addtwonum:
.LFB11:
.cfi_startproc
movl $1, x(%rip)
movl $2, y(%rip)
movl $3, %eax
ret
.cfi_endproc
.LFE11:
.size addtwonum, .-addtwonum
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "result \344\270\272\357\274\232 %d"
.LC1:
.string "x \344\270\272\357\274\232 %d"
.LC2:
.string "y \344\270\272\357\274\232 %d"
.text
.globl main
.type main, @function
main:
.LFB12:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $0, %eax
call addtwonum
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl x(%rip), %esi
movl $.LC1, %edi
movl $0, %eax
call printf
movl y(%rip), %esi
movl $.LC2, %edi
movl $0, %eax
call printf
movl $0, %eax
addq $8, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE12:
.size main, .-main
.comm y,4,4
.comm x,4,4
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39)"
.section .note.GNU-stack,"",@progbits
另外如果您已有一個機器碼文件,通常是“.o”爲後綴的文件,您也可以使用反彙編器來獲取彙編文件,Linux系統常用的反彙編工具是OBJDUMP,將addtwonum.o反彙編的命令如下:
objdump -d addtwonum.o
addtwonum.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <addtwonum>:
0: c7 05 00 00 00 00 01 movl $0x1,0x0(%rip) # a <addtwonum+0xa>
7: 00 00 00
a: c7 05 00 00 00 00 02 movl $0x2,0x0(%rip) # 14 <addtwonum+0x14>
11: 00 00 00
14: b8 03 00 00 00 mov $0x3,%eax
19: c3 retq
000000000000001a <main>:
1a: 48 83 ec 08 sub $0x8,%rsp
1e: b8 00 00 00 00 mov $0x0,%eax
23: e8 00 00 00 00 callq 28 <main+0xe>
28: 89 c6 mov %eax,%esi
2a: bf 00 00 00 00 mov $0x0,%edi
2f: b8 00 00 00 00 mov $0x0,%eax
34: e8 00 00 00 00 callq 39 <main+0x1f>
39: 8b 35 00 00 00 00 mov 0x0(%rip),%esi # 3f <main+0x25>
3f: bf 00 00 00 00 mov $0x0,%edi
44: b8 00 00 00 00 mov $0x0,%eax
49: e8 00 00 00 00 callq 4e <main+0x34>
4e: 8b 35 00 00 00 00 mov 0x0(%rip),%esi # 54 <main+0x3a>
54: bf 00 00 00 00 mov $0x0,%edi
59: b8 00 00 00 00 mov $0x0,%eax
5e: e8 00 00 00 00 callq 63 <main+0x49>
63: b8 00 00 00 00 mov $0x0,%eax
68: 48 83 c4 08 add $0x8,%rsp
6c: c3 retq
文件的最左側是命令行索引號,十六進制數字是對應彙編代碼的機器碼指令,最右邊纔是彙編代碼,這時我們得到的彙編代碼與使用GCC彙編命令產生的彙編代碼有所差異,“call”和“ret”指令後面多了“q”,這個“q”是大小寫指示符,在大多數情況下可以省略。
數據格式
由於最初是從16位系統結構發展成爲32位以及後來的64位的,Intel用術語“字(word)”表示16位數據類型,稱32位數爲“雙字(duoble words)”,64位數爲“四字(quad words)”。下表給出x86-64系統中C語言基本數據類型的表示。
C聲明 | Intel數據類型 | 彙編代碼後綴 | 大小(字節) |
---|---|---|---|
char | 字節 | b | 1 |
short | 字 | w | 2 |
int | 雙字 | l | 4 |
long | 四字 | q | 8 |
char* | 四字 | q | 8 |
float | 單精度 | s | 4 |
double | 雙精度 | l | 8 |
大多數GCC生成的彙編代碼指令都有一個字符的後綴,表明操作數的大小,例如,數據傳送指令有四種變種:movb(傳送字節),movw(傳送字),movl(傳送雙字),movq(傳送四字)。
訪問信息
在彙編語言中,彙編指令之後很多類似“%rsp”,“%eax”以及“$0x8”格式的內容,這些內容的具體含義是什麼呢?
指令後帶“%”的都是指計算機系統的寄存器,在X86-64的中央處理單元(CPU)中,有16個64位通用目的寄存器,這些寄存器用來存儲整數數據和指針,所有寄存器名字如下表,其%ax到%sp的8個16位寄存器是8086系統中的寄存器,從%eax到%esp的8個32位寄存器是IA32架構系統的寄存器,從%rax到%r15的16個64位寄存器纔是X86-64處理器的寄存器。新一代寄存器對老的寄存器是兼容的,當在高位寄存器系統中運行低位程序時,寄存器對應的低位位置會被使用,高位自動填充0。
除了帶“%”號的內容,我們還看到其他格式的數據,其含義如下:
想要更詳細的瞭解彙編語言,請查看其它資料
內存管理
虛擬內存
在計算機系統中進程與其他進程共享CPU和主存,因此存在一個進程寫了另一個進程的內存問題,這會引發令人迷惑的錯誤。爲了有效的管理內存並減少出錯,現代系統提供了一種對主存的抽象概念——虛擬內存。
虛擬內存三個重要能力:
(1)將主存視爲磁盤空間的高速緩存,在主存中只保存活動區域,並根據需要在磁盤和主存間來回傳送數據,高效利用主存;
(2)爲每個進程提供一致的地址空間,屏蔽了對硬件操作管理的細節,簡化了內存管理;
(3)保護每個進程的地址空間不被其他進程破壞。
傳統的物理尋址
應用範圍:早期的PC、數字信號處理器、嵌入式微控制器、Cray超級計算機等。
現代的虛擬尋址
應用範圍:現代計算機系統
虛擬地址與物理地址之間的映射關係:
多個虛擬地址可以指向同一個物理地址,這樣共享內存就變得很容易了。
一些重要概念
DRAM緩存:指虛擬內存系統的緩存,它在主存中緩存虛擬頁。
SRAM緩存:指CPU與主存之間的L1/L2/L3高級緩存。
頁表:常駐內存主要是用來標識(PTE位)虛擬頁是否已緩存到DRAM的數組。
頁命中:目標地址的虛擬頁已被緩存至內存中,通過目標地址能夠找到已被緩存的虛擬頁被稱之爲頁命中。
缺頁:DRAM緩存不命中稱爲缺頁,缺頁會拋出缺頁異常,缺頁異常又會觸發異常處理程序去選中一個犧牲頁,並從磁盤複製所需虛擬頁去替換犧牲頁。
分配頁面:操作系統分配一個新的虛擬頁給程序的過程,例如,調用malloc函數,操作系統首先會爲程序在磁盤創建一個虛擬頁,並更新頁表內容,讓其中一個頁表條目指向這個磁盤虛擬頁。
局部性:局部性原則的含義是CPU要執行的命令往往就在局部範圍內,這樣就保證了程序在任意時刻都趨於在一個較小的活動頁面集合上工作,從而避免了大概率的缺頁。只要程序有好的時間局部性,虛擬內存系統就能夠很好的工作。如果程序的工作集大小超過了物理內存的大小,就會頻繁發生頁面的換進換出(稱之爲抖動),程序運行就會變得很慢。
虛擬內存作爲緩存的工具
對應存儲層的分塊的概念,VM系統將虛擬內存也按固定大小分割爲虛擬頁(VP),對應的物理內存被分割爲物理頁(PP),物理頁也稱之爲頁幀。
虛擬頁任何時刻都被分爲三個不相交的子集:
(1) 未分配的:VM系統還未分配(未創建)的頁,沒有任何數據與之相關聯,因此不佔用任何磁盤空間。
(2) 未緩存的:已被創建的虛擬頁,但是還沒有緩存到物理內存。
(3) 已緩存的:已被創建並被緩存到物理內存中的虛擬頁。
虛擬內存作爲內存管理工具
在《【深入理解計算機系統·筆記】計算機系統中的重要概念》筆記中描述過計算機系統中一些重要的抽象概念,虛擬內存也是計算機中核心的抽象概念。早期的虛擬地址比物理地址要少,那時虛擬內存主要是爲內存管理提供支撐。在內存管理方面,虛擬內存的出現主要起到以下作用:
1.簡化鏈接:獨立的虛擬地址空間允許每一個進程的內存映像使用相應的基本格式,而不用去管代碼和數據實際存放在物理內存的何處。
2.簡化加載:上文示例過系統在分配頁面時,malloc函數只是在磁盤創建了虛擬頁,然後讓頁表指向虛擬頁,並未立即將虛擬頁緩存至物理內存,只有當頁面第一次被引用時,CPU發起取指引用後才虛擬內存纔會按需調入數據頁,這樣可以有效提高物理內存的使用效率。另外虛擬內存允許將一組連續的虛擬頁映射到任意一個文件中的任意位置,這也被稱之爲內存映射,Linux系統提供的mmap函數就是做內存映射工作的。
3.簡化共享:前面也講到多個虛擬地址可以指向同一個物理地址,這樣就能很方便多個進程共享物理內存了。
4.簡化內存分配:連續的虛擬內存對應的物理內存可以是不連續的,虛擬內存爲用戶進程提供了一個簡單的分配額外內存的機制。
虛擬內存作爲內存保護工具
任何現代計算機系統必須爲操作系統提供手段來控制對內存系統的訪問,不應該允許用戶進程去修改它的的只讀代碼段,而且也不允許它讀取或修改任何內核中的代碼和數據結構。也不允許它讀或寫其他進程的私有內存。虛擬內存提供的獨立地址空間將這些需求變得很容易。
動態分配內存
雖然可以在運行前使用低級的mmap和munmap來創建和刪除虛擬內存的區域,但是我們也會在運行時獲取額外虛擬內存,這是就需要用到動態內存分配器。
動態內存分配器維護着一個進程的虛擬內存區域,我們通常稱之爲堆(heap),分配器將堆視爲一組不同大小的塊(block)的集合,每個塊中是一個連續的虛擬內存片(chunk),要麼是已分配,要麼是空閒的。已分配的顯式地保留給應用程序使用,空閒的塊可以用來分配。一個已分配的塊保持自己已分配的狀態,直到它被釋放,要麼是程序自己釋放(C語言中調用free函數),要麼是內存分配器自身隱式釋放(Java中的垃圾回收器)。
因此內存分配器分爲顯示分配器和隱式分配器,C標準庫提供malloc和free函數,以及C++中的new和delete都是屬於顯式分配,而Lisp、ML及Java語言的垃圾回收器就是隱式分配器。
動態分配器的要求
- 處理任意請求序列:一個應用可以有任意的分配請求和釋放請求序列,只要滿足約束條件:每個釋放的請求必須對應一個已分配的塊。
- 立即響應請求:分配器必須立即響應分配請求,因此不允許分配器爲了提高性能重新排列或者緩衝請求。
- 只使用堆:爲了使分配器是可擴展的,分配器使用的任何非標量數據結構都必須保存在堆裏。
- 對齊塊:分配器必須對齊塊,使得它們可以保存任何類型的數據對象。
- 不修改已分配的塊:分配器只能操作或者改變空閒塊,特別是,一旦塊被分配了,就不允許修改或移動它了。因此,壓縮已分配的塊的技術是不被允許的。
動態分配器的目標
1.最大化吞吐率:分配器的吞吐率是指每時間單位可處理的請求次數(包含分配和釋放請求)。
2.最大化內存利用率:一個系統中被所有進程分配的虛擬內存的全部數量時受磁盤上交換空間的數量限制的。虛擬內存是一個有限的空間,必須高效利用才能讓其發揮最大的價值。
垃圾收集
在諸如C malloc包這樣的顯式分配器中,應用通過調用malloc和free函數來分配或釋放堆塊,應用需要負責釋放所有不在需要的已分配的塊。未能釋放已分配的塊是一種常見的編程錯誤。
而垃圾收集器是一種動態內存分配器,它自動釋放程序不再需要的已分配塊。這些塊被稱爲垃圾(garbage),垃圾收集最早可追溯到John McCarthy在20世紀60年代早期在MIT開發的Lisp系統,後來成爲Java、ML、Perl等現代語言重要的一部分。
垃圾收集器將內存視爲一張有向可達圖,該圖的節點被分成一組根節點(root node)和一組堆節點(heap node),每個堆節點對應於堆中的一個已分配的塊,根節點對應於這樣一種不在堆中的位置,它們包含指向堆節點的指針,這些位置可以是寄存器、棧裏的變量、或者是虛擬內存中讀寫數據區域內的全局變量。
操作系統
Linux基礎
Windows基礎
進程
內存管理
Linux虛擬內存系統
虛擬內存系統要求硬件和內核軟件之間需要緊密協作。
Linux將虛擬內存組織成一些區域(也稱爲段),一個區域(area)就是已經存在着的(已分配的)虛擬內存的連續片(chunk),代碼段、數據段、共享庫、以及用戶棧都是不同的區域。每個存在着的虛擬頁都保存在一個區域中,而不屬於任意個區域的虛擬頁是不存在,並且不能被進程引用。
Linux內核會爲每一個進程維護一個單獨的任務結構(task_struct),任務結構中包含的元素或指向內核運行該進程所需的信息(例如:PID,指向用戶棧的指針,可執行目標文件的名字,以及程序計數器等)
想進一步瞭解任務結構的詳細信息請進入Linux系統的sched.h文件,task_struct結構體的定義是在內核中的sched.h中,其路徑一般在:/usr/src/kernels/x.xx.x-xxx.xx.x.xxx.x86_64/include/linux
任務結構中的一個條目指向mm_struct,它描述了虛擬內存的當前狀態,重要的字段pdg和mmap ,pdg指向第一級頁表的基址,而mmap指向一個vm_area_struct(區域結構)的鏈表,vm_area_struct描述了當前虛擬地址空間的一個區域。其中:
vm_end:指向所描述區域的結束位置;
vm_start:指向所描述區域的開始位置;
vm_prot:描述這個區域內包含所有頁的讀寫許可權限;
vm_flags:描述這個區域的頁面是否是與其他進程共享的,或者是私有的;
vm_next:指向鏈表中的下一個區域結構。
系統級I/O
異常控制
網絡基礎
TCP/IP協議
安全防護
Java基礎
內存管理
JVM內存模型
JVM對內存的管理是基於計算機系統的虛擬內存的,相關虛擬內存知識請查看以下目錄內容:
1.【硬件基礎】–>【內存管理】–>【虛擬內存】
2.【操作系統】–>【內存管理】–>【Linux虛擬內存系統】
堆(Heap):對於絕大多數應用來說,這塊區域是 JVM 所管理的內存中最大的一塊。這塊區域是線程共享的,主要存放對象實例和數組(目前由於編譯器的優化,對象在堆上分配已經沒有那麼絕對了,參見:https://www.cnblogs.com/aiqiqi/p/10650394.html)。內部會劃分出多個線程私有的分配緩衝區。可以位於物理上不連續的空間,但是邏輯上(虛擬內存)要連續。
方法區(Method Area):屬於共享內存區域,存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
本地方法棧(Native Method Stack):區別於Java 虛擬機棧的是,Java 虛擬機棧爲虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。方法執行完畢後相應的棧幀也會出棧並釋放內存空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種異常。
虛擬機棧(VM Stack):線程私有,生命週期和線程一致。每個方法在執行時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行結束,就對應着一個棧幀從虛擬機棧中入棧到出棧的過程。局部變量表主要存放了編譯器可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)和對象引用(reference類型,它不同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)
Java 虛擬機棧會出現兩種異常:StackOverFlowError 和 OutOfMemoryError。
StackOverFlowError:若Java虛擬機棧的內存大小不允許動態擴展,那麼當線程請求棧的深度超過當前Java虛擬機棧的最大深度的時候,就拋出StackOverFlowError異常。
OutOfMemoryError:若 Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出OutOfMemoryError異常。
程序計數器(PC):存儲指令地址,順序執行時自動加1,或由轉移指令指定需要轉去的指令地址,與線程一一對應,程序計數器是唯不會出現 OutOfMemoryError 的內存區域。程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都需要依賴這個計數器來完。
垃圾回收機制
哪些對象屬於垃圾?JVM提供了一些算法去判定,常見的判定方法有:引用計數法和可達性分析。
- 引用計數法
JVM爲每一個已創建的對象分配一個引用計數器,用來存儲該對象被引用的個數,當被引用的個數爲0時,意味着該對象已沒被使用,即可當做“垃圾”,而當有位置引用它時,引用計數器加1,不再引用時將引用計數器減1。這種算法存在一個弊端,那就是無法檢測“循環引用”,即兩個對象相互引用,它們的引用計數都不爲0,因此無法回收,實際上這兩個對象已沒有其他位置引用,是可以釋放的對象。該方法並沒有被Java採用。
- 可達性分析
可達性分析基本思路是把所有引用的對象想象成一棵樹,從樹的根結點 GC Roots 出發,持續遍歷找出所有被連接的對象,這些對象則被稱爲“可達”對象,或稱“存活”對象。不能到達的則被視爲“垃圾”,成爲可回收對象。
GC Roots對象(非堆對象)的位置:
(1).虛擬機棧中引用的對象;
(2).方法區中靜態屬性引用的對象;
(3).方法區中常量引用的對象;
(4).本地方法棧中JNI引用的對象;