java程序員應該熟悉的底層知識

不求甚解,觀其大略!

彙編語言(機器語言)的執行過程

彙編語言的本質:機器語言的助記符號,就是機器語言

​ 比如 move --> 10001000 在彙編語言中的move在執行的時候就會在對應的表中找到對應的記錄並變成10001000 的value值

計算機通電–>CPU讀取內存中的程序(電信號輸入)–>時鐘發生器不斷震顫通電–>推動CPU內部一行一行的執行(執行多少步取決於指令需要的時鐘週期)–>計算完成–>寫回(電信號)–>寫給顯卡輸出(sout,或者圖形)

量子計算機

量子比特,同時表示1,0

其實我不太理解爲什麼同時表示1和0就能那麼誇張的比只表示1或者0快

不是代表一個固定態,而是代表了所有的可能態。

傳統的計算機都是使用二進制的,一個比特就是0或者1,而量子比特也是使用二進制,但它特別就在一個量子比特可以同時是0或者1,這就叫量子疊加。所以,兩個量子比特就可以同時表示00、01、10、11這四個值。

具體就不再深入了,不是特別能理解。但是到此爲止。

CPU的組成

PC -> Program Counter 程序計數器(記錄當前指令地址)

Registers -> 暫存器,暫時存儲CPU計算要用到的數據

ALU -> 邏輯運算單元 Arithmetic & Logic Unit

CU -> 控制單元 Control Unit

MMU -> 內存管理單元 Memory Management Unit

緩存

緩存一致性協議:

https://www.cnblogs.com/z00377750/p/9180644.html

MESI協議

CPU中每個緩存行都使用4中狀態進行標記(額外使用2位)

M:被修改(Modified)

該緩存行只被緩存在該CPU這個呢,並且是被修改過的(dirty),即與主存中的數據不一致,該緩存行需要在未來的某一個時間點(允許其他CPU讀取主存中相應內存之前)寫回到主存 。當被寫會到主存之後。狀態會改爲E(獨享的)

E:獨享的(Exclusive)

該緩存行只被緩存在該CPU中,他是未被修改過的(clean),與主存中的數據一致。該狀態可以在任何時刻當有其他CPU讀取時變成共享狀態S(shard)

同樣的,當CPU修改了緩存行中的內容時,該狀態可以變成M(被修改)狀態

S:共享的(Shared)

該狀態意味着該緩存行被多個CPU緩存,並且各個緩存的數據與主存數據一致(clean),當有一個CPU修改緩存行中的值。其他的CPU中的該緩存行可以唄作廢(變成無效狀態I(invalid)

I:無效的(invalid)

該緩存是無效的(可能是其他的CPU修改了該緩存行)

CPU讀請求:除了invalid狀態,其他的狀態都能滿足CPU的讀請求,如果是invalid,那麼會在主存中讀取,並且變成shared或者exclusive

CPU寫請求:只能在modified或者exclusive狀態下能進行,如果是shared狀態,需要變成invalid(不允許不同CPU修改同一個緩存行,即使數據處於不同的地方)。改操作通常使用廣播的方式來完成

緩存能將任何一個非M狀態直接作廢,但是如果是M狀態,那麼必須先寫會到主存

如果有CPU讀取M對應的主存(M的緩存行負責監聽),那麼會先將M寫會到主存在執行其他CPU讀取M對應的主存的操作

處於S的緩存行會監聽該緩存行變成invalid的操作或變成E的操作,並且會把自己的緩存行變成invalid

處於E的緩存行會監聽主存中對應的位置,如果有其他的CPU讀取會將狀態變成shared

緩存行

緩存行越大,局部性空間效率越高,但是讀取時間越慢

緩存行越小,局部性空間效率越低,但是讀取時間越快

Intel CPU選取了一個折中值 64字節

package com.example.demo;

/**
 * 測試緩存行 64字節 位於同一個緩存行
 *
 * @Author: xiaobin
 * @Date: 2020/5/26 12:58
 */
public class T03_CacheLinePadding {

    public static volatile long[] arr = new long[2];

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10_0000_0000L; i++) {
                arr[0] = i;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10_0000_0000L; i++) {
                arr[1] = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }

}
package com.example.demo;

/**
 * 位於不同緩存行
 *
 * @Author: xiaobin
 * @Date: 2020/5/26 13:02
 */
public class T04_CacheLinePadding {
    public static volatile long[] arr = new long[16];

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10_0000_0000L; i++) {
                arr[0] = i;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10_0000_0000L; i++) {
                arr[8] = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}

但是我在以上跑,前者時間僅僅是後者時間的1/3。比較奇怪

緩存行對齊

對於有些特別敏感的數字,會存在線程高競爭的訪問,爲了保證不發生僞共享,可以使用緩存行對齊的編程方式

在jdk1.7中,很多采用long padding提高效率。就是前面加8個long的聲明,後面加8個long的聲明

jdk1.8加入了@Contended註解。需要加上:JVM -XX:-RestrictContended

亂序執行

計算機爲了執行效率,可能會發生指令重拍,沒有依賴關係的命令可能重新排序

禁止亂序

CPU層面:Intel -> 原語(mfence Ifence sfence)或者鎖總線

JVM層次:8個hanppens-before原則 4個內存屏障(LL LS SL SS)

as-if-serial:不管硬件什麼順序,單線程執行的結果不變,看上去就像serial

合併寫

Write Combining Buffer

一般是4個字節

由於ALU的速度太快,所以在寫入L1的同時,寫入一個WC buffer,滿了之後,再直接更新到L2

NUMA

Non Uniform Memory Access

ZGC -NUMA aware

分配內存會優先分配給靠近CPU的內存

OS

啓動過程

通電 -> bios uefi工作 -> 自檢 -> 到硬盤的固定位置加載bootloader(一般是磁盤的第一個扇區)-> 讀取可配置信息 -> CMOS

內核分類

微內核 -彈性部署 5G 物聯網(The Internet of Things,簡稱lot)

宏內核 -pc phone

外核 -科研 實驗中 爲應用定製操作系統(不懂)

用戶態和內核態(重點)

CPU分不同的指令級別

Linux內核跑在ring 0級,用戶程序跑在ring 3級,對於系統的關鍵訪問,需要經過kernel的同意,保證系統的健壯性

內核執行操作 --> 200多個系統調用 read write fork…

JVM --> 站在OS的角度看,就是一個普通的進程

進程 線程 纖程 和中斷

面試高頻:進程和線程有什麼區別

答案:進程就是一個程序運行起來的狀態,線程就是一個進程中的不同執行路徑

專業回答:進程就是OS分配資源的基本單位,線程就是執行調度的基本單位。分配資源最重要的就是:獨立的內存空間,線程調度執行(線程共享進程的內存空間,沒有自己獨立的內存空間)

纖程:用戶態的線程,線程中的線程,切換和調度不需要經過OS

優勢:

  1. 佔用資源少 OS:線程 1M 纖程:4k
  2. 切換比較簡單
  3. 可以啓動非常多10W+

目前支持內置纖程的語言:go kotlin scala python(lib)

Java中對於纖程的支持:沒有內置,盼望內置

利用Quaser庫(不成熟)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>mashibing.com</groupId>
    <artifactId>HelloFiber</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/co.paralleluniverse/quasar-core -->
        <dependency>
            <groupId>co.paralleluniverse</groupId>
            <artifactId>quasar-core</artifactId>
            <version>0.8.0</version>
        </dependency>
    </dependencies>

</project>
package com.example.demo;

/**
 * @Author: xiaobin
 * @Date: 2020/5/26 19:18
 */
public class HelloFiber {
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                calc();
            }
        };
        int size = 20000;
        Thread[] threads = new Thread[size];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(r);
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    private static void calc() {
        int result = 0;
        for (int m = 0; m < 10000; m++) {
            for (int i = 0; i < 200; i++) {
                result += i;
            }
        }
    }
}

package com.example.demo;

import com.sun.xml.internal.ws.api.pipe.Fiber;

/**
 * @Author: xiaobin
 * @Date: 2020/5/26 19:23
 */
public class HelloFiber2 {
    public static void main(String[] args) throws  Exception {
        long start = System.currentTimeMillis();


        int size = 10000;

        Fiber<Void>[] fibers = new Fiber[size];

        for (int i = 0; i < fibers.length; i++) {
            fibers[i] = new Fiber<Void>(new SuspendableRunnable() {
                public void run() throws SuspendExecution, InterruptedException {
                    calc();
                }
            });
        }

        for (int i = 0; i < fibers.length; i++) {
            fibers[i].start();
        }

        for (int i = 0; i < fibers.length; i++) {
            fibers[i].join();
        }

        long end = System.currentTimeMillis();
        System.out.println(end - start);


    }

    static void calc() {
        int result = 0;
        for (int m = 0; m < 10000; m++) {
            for (int i = 0; i < 200; i++) result += i;

        }
    }
}

目前是10000個fiber對應一個線程,還可以通過將其分成10份對應10個線程來提高效率

纖程的應用場景

纖程 vs 線程池 :很短的計算任務,不需要和內核打交道,併發量高!

殭屍進程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>

int main() {
        pid_t pid = fork();

        if (0 == pid) {
                printf("child id is %d\n", getpid());
                printf("parent id is %d\n", getppid());
        } else {
                while(1) {}
        }
}

子進程已經死了,並且已經回收了子進程的系統資源,但是父進程還只有子進程的引用

孤兒進程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>

int main() {
        pid_t pid = fork();

        if (0 == pid) {
                printf("child ppid is %d\n", getppid());
                sleep(10);
                printf("parent ppid is %d\n", getppid());
        } else {
                printf("parent id is %d\n", getpid());
                sleep(5);
                exit(0);
        }
}

父進程死了,但是子進程還在,一般會掛到進程id爲1的進程下,或者圖形界面就會掛到圖形界面的進程下

進程調度

2.6採用CFS調度策略: Completely Fair Scheduler

按照優先級分配時間片的比例,記錄每個進程的執行時間,如果有一個進程執行時間不到他應該分配的比例,優先執行

默認調度策略:

實時優先級分高低: FIFO (優先級高的先執行)優先級一樣 RR(Round Robin)

普通進程:CFS

中斷

硬件和操作系統內核打交道的一種方式

軟終端(80中斷) == 系統調用

系統調用: int 0x80 或者 sysenter原語

通過ax寄存器填入調用號

參數通過 bx cx dx si di傳入內核

返回值通過ax返回

Java 讀網絡 -> Jvm read() --> c read() --> 內核空間 --> System_call()(系統調用處理程序) --> sys_read()

從彙編的角度理解軟中斷

yum install nasm

;hello.asm
;write(int fd, const void *buffer, size_t nbytes)
;fd 文件描述符 file descriptor - linux下一切皆文件

section data
    msg db "Hello", 0xA
    len equ $ - msg

section .text
global _start
_start:

    mov edx, len
    mov ecx, msg
    mov ebx, 1 ;文件描述符1 std_out
    mov eax, 4 ;write函數系統調用號 4
    int 0x80

    mov ebx, 0
    mov eax, 1 ;exit函數系統調用號
    int 0x80

編譯:nasm -f elf hello.asm -o hello.o

鏈接:ld -m elf_i386 -o hello hello.o

一個程序的執行,要麼處於用戶態,要麼處於內核態

內存管理

內存管理的發展歷程

DOS時代: 同一時間只能有一個進程在運行(也有一些特殊的算法可以支持多進程)

windows 9x 多個進程裝入內存,但是存在 內存不夠 相互打擾

爲了解決這兩個問題,誕生了現再的內存管理系統:虛擬地址 分頁裝入 軟硬件結合尋址

1.分頁(解決內存不夠用問題)內存中分成固定大小的頁框(4K),把程序(硬盤)分成4K大小的塊,用到哪一塊就將哪一塊加載到內存,如果加載的過程中內存滿了,就將最不常用的那一塊放到swap分區,把新的一塊加載進來,這個就是著名的LRU算法

  1. LRU算法 LeetCode 146題,頭條要求手撕,阿里去年也要求手撕
  2. Least Recently Used 最不常用
  3. 哈希表(保證查找操作 O(1) + 鏈表(保證排序操作和新增操作O(1)))
  4. 雙向鏈表(保證左邊指針指向右邊塊)

2.虛擬內存(解決相互打擾的問題)

1. DOS Win31 ...相互幹掉,甚至幹掉操作系統
2. 爲了保證不相互影響,讓進程工作在虛擬內存,程序使用的是虛擬內存,而不是直接的物理地址,這樣A進程就永遠不知道B進程的空間
3. 虛擬內存有多大? 尋址空間 = 2^64 ,比物理內存大很多 單位是byte
4. 站在虛擬的角度,進程是獨享整個內存空間的
5. 內存映射: 偏移量+段的基地址 = 線性地址(虛擬地址)
6. 線性地址通過OS +MMU 內存管理單元 (硬件 Memory Management Unit)找到物理地址

3.缺頁中斷(不是很重要)

  1. 需要用到的頁內存中沒有,產生缺頁中斷(異常) ,有內核處理並加載

ZGC

算法叫做:Colored Pointer

GC信息記錄在指正上,而不是記錄在頭部 immediate memory use

42位指針 尋址空間4T JDK13 -> 16T 目前爲止最大16T 2^44

CPU如何區分一個立即數 和 一條指令

總線內部分爲:數據總線 地址總線 控制總線

地址總線目前:48位

顏色指針本質上包含了地址映射的概念

內核同步機制

關於同步理論的一些基本概念

  • 臨界區(critical area):訪問或操作共享數據的代碼段。簡易理解synchronized大括號中的部分(原子性)
  • 競爭條件(race conditions):兩個線程同時擁有臨界區的執行權
  • 數據不一致(data unconsistency ): 由競爭條件引起的數據破壞
  • 同步(synchronized):避免競爭條件
  • 鎖:完成同步的手段,上鎖解鎖必須具備原子性
  • 原子性
  • 有序性(禁止指令重拍)
  • 可見性(一個線程內的修改,另外一個線程可見)

互斥鎖 排它鎖 共享鎖 分段鎖

內核同步的常用方法

  1. 原子操作 -內核中類似於AtomicXXX,位於<Linux/types.h>
  2. 自旋鎖 -內核中通過彙編支持cas,位於<asm/spinlock.h>
  3. 讀-寫自旋 -類似於ReadWriteLock,可同時讀,只能一個寫,讀的時候是共享鎖,寫的時候是排它鎖
  4. 信號量 -類似於Semaphore(PV操作 down up操作 佔有和釋放) 重量級鎖,線程會進入wait,適合長時間持有鎖的情況
  5. 讀-寫信號量(多個寫,可以分段寫,比較少用)(分段鎖)
  6. 互斥體(mutex) – 特殊的信號量(二值信號量)
  7. 完成變量 – 特殊的信號量(A發出信號給B,B等待在完成變量上) vfork() 在子進程結束時通過完成變量叫醒父進程 類似於(Latch)
  8. BKL:大內核鎖(早期,現在已經不用)
  9. 順序鎖(2.6): -線程可以掛起的讀寫自旋鎖 序列計數器(從0開始,寫時增加(+1),寫完釋放(+1),讀前發現單數, 說明有寫線程,等待,讀前讀後序列一樣,說明沒有寫線程打斷)
  10. 禁止搶佔 – preempt_disable()
  11. 內存屏障 – 見volatile
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章