線程與棧那些事

2021-02-14 張師傅的博客

這篇文章是介紹一下線程與棧相關的話題,文章比較長,主要會聊聊下面這些話題:

Hotspot 線程棧的 Guard 區域實現原理你可能沒有怎麼聽說過的 Yellow-Zone、Red-ZoneJava StackOverflowError 的實現原理

為了講清楚線程與棧的關係,我們要從進程和線程之間的關係講起,接下來開始第一部分。

第一部分:老生常談之進程線程

網上很多文章都說,線程比較輕量級 lightweight,進程比較重量級,首先我們來看看這兩者到底的區別和聯繫在哪裡。

clone 系統調用

在上層看來,進程和線程的區別確實有天壤之別,兩者的創建、管理方式都非常不一樣。在 linux 內核中,不管是進程還是線程都是使用同一個系統調用 clone,接下來我們先來看看 clone 的使用。為了表述的方便,接下來暫時用進程來表示進程和線程的概念。

clone 函數的函數籤名如下。

int clone(int (*fn)(void *),
void *child_stack,
int flags,
void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

參數釋義如下:

第一個參數 fn 表示 clone 生成的子進程會調用 fn 指定的函數,參數由第四個參數 arg 指定flags 參數非常關鍵,正是這個參數區分了生成的子進程與父進程如何共享資源(內存、打開文件描述符等)剩下的參數,ptid、tls、ctid 與線程實現有關,這裡先不展開

接下來我們來看一個實際的例子,看看 flag 對新生成的「進程」行為的影響。

clone 參數的影響

接下來演示 CLONE_VM 參數對父子進程行為的影響,這段代碼當運行時的命令行參數包含 "clone_vm" 時,給 clone 函數的 flags 會增加 CLONE_VM。代碼如下。

static int child_func(void *arg) {
char *buf = (char *)arg;
// 修改 buf 內容
strcpy(buf, "hello from child");
return 0;
}

const int STACK_SIZE = 256 * 1024;
int main(int argc, char **argv) {
char *stack = malloc(STACK_SIZE);

int clone_flags = 0;
// 如果第一個參數是 clone_vm,則給 clone_flags 增加 CLONE_VM 標記
if (argc > 1 && !strcmp(argv[1], "clone_vm")) {
clone_flags |= CLONE_VM;
}
char buf[] = "msg from parent";

if (clone(child_func, stack + STACK_SIZE, clone_flags, buf) == -1) {
exit(1);
}
sleep(1);
printf("in parent, buf:\"%s\"\n", buf);
return 0;
}

上面的代碼在 clone 調用時,將父進程的 buf 指針傳遞到 child 進程中,當不帶任何參數時,CLONE_VM 標記沒有被設置,表示不共享虛擬內存,父子進程的內存完全獨立,子進程的內存是父進程內存的拷貝,子進程對 buf 內存的寫入只是修改自己的內存副本,父進程看不到這一修改。

編譯運行結果如下。

$ ./clone_test

in parent, buf:"msg from parent"

可以看到 child 進程對 buf 的修改,父進程並沒有生效。

再來看看運行時增加 clone_vm 參數時結果:

$ ./clone_test clone_vm

in parent, buf:"hello from child"

可以看到這次 child 進程對 buf 修改,父進程生效了。當設置了 CLONE_VM 標記時,父子進程會共享內存,子進程對 buf 內存的修改也會直接影響到父進程。

講這個例子是為後面介紹進程和線程的區別打下基礎,接下來我們來看看進程和線程的本質區別是什麼。

進程與 clone

以下面的代碼為例。

pid_t gettid() {
return syscall(__NR_gettid);
}
int main() {
pid_t pid;
pid = fork();
if (pid == 0) {
printf("in child, pid: %d, tid:%d\n", getpid(), gettid());
} else {
printf("in parent, pid: %d, tid:%d\n", getpid(), gettid());
}
return 0;
}

使用 strace 運行輸出結果如下:

clone(child_stack=NULL,
flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD,
child_tidptr=0x7f75b83b4a10) = 16274

可以看到 fork 創建進程對應 clone 使用的 flags 中唯一需要值得注意的 flag 是 SIGCHLD,當設置這個 flag 以後,子進程退出時,系統會給父進程發送 SIGCHLD 信號,讓父進程使用 wait 等函數獲取到子進程退出的原因。

可以看到 fork 調用時,父子進程沒有共享內存、打開文件等資源,這樣契合進程是資源的封裝單位這個說法,資源獨立是進程的顯著特徵。接下來我們來看看線程與 clone 的關係。

線程與 clone

這裡以一段最簡單的 C 代碼來看看創建一個線程時,底層到底發生了什麼,代碼如下。

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

void *run(void *args) {
sleep(10000);
}
int main() {
pthread_t t1;
pthread_create(&t1, NULL, run, NULL);
pthread_join(t1, NULL);
return 0;
}

使用 gcc 編譯上面的代碼

gcc -o thread_test thread_test.c -lpthread

然後使用 strace 執行 thread_test,系統調用如下所示。

mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fefb3986000

clone(child_stack=0x7fefb4185fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fefb41869d0, tls=0x7fefb4186700, child_tidptr=0x7fefb41869d0) = 12629

mprotect(0x7fefb3986000, 4096, PROT_NONE) = 0

比較重要的是下面這些 flags 參數:

標記含義CLONE_VM共享虛擬內存CLONE_FS共享與文件系統相關的屬性CLONE_FILES共享打開文件描述符表CLONE_SIGHAND共享對信號的處置CLONE_THREAD置於父進程所屬的線程組中

可以看到,線程創建的本質是共享進程的虛擬內存、文件系統屬性、打開的文件列表、信號處理,以及將生成的線程加入父進程所屬的線程組中。

值得注意的是 mmap 申請的內存大小不是 8M 而是 8M + 4K

8392704 = 8 * 1024 * 1024 + 4096

為什麼會多這 4K,我們接下來的第二部分線程與棧中會詳細闡述。

第二部分:線程與棧

前面內容中,我們看到通過 strace 查看線程創建過程中的 8M 的棧大小,實際上會分配多 4k 的空間,這是一個很有意思的問題,我們來詳細看看。

線程與 Guard 區域

線程的棧是一個比較「奇怪」的產物,一方面線程的棧是線程獨有,裡面保存了線程運行狀態、局部變量、函數調用等信息。另外一方面,從資源管理的角度而言,所有線程的棧都屬於進程的內存資源,線程和父進程共享資源,進程中其它線程自然可以修改任意線程的棧內存。

以下面的代碼為例,這段代碼創建了兩個線程 t1、t2,對應的運行函數是 runnable1 和 runnable2。t1 線程將 buf 數組的地址複製給全局指針 p,t1 線程每隔 1s 列印一次 buf 數組的內容,t2 線程每隔 3s 修改一次 p 指針指向地址的內容。

#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
static char *p;
void *runnable1(void *args) {
char buf[10] = {0};
p = buf;
while (1) {
printf("buffer: %s\n", buf);
sleep(1);
}
}

void *runnable2(void *args) {
int index = 0;
while (1) {
if (p) {
strcpy(p, index++ % 2 == 0 ? "say hello" : "say world");
}
sleep(3);
}
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, runnable1, NULL);
pthread_create(&t2, NULL, runnable2, NULL);

pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}

編譯運行上面的代碼,結果輸出如下

$ ./thread_stack_test

buf:
buf:
buf:
buf: say hello
buf: say hello
buf: say hello
buf: say world
buf: say world
buf: say world
buf: say hello
buf: say hello

可以看到線程 2 直接修改了線程 1 棧中數組的內容。這種行為是 linux 中完全合法,不會報任何錯誤。如果可以這麼隨意的訪問到其它線程的內容是一個非常危險的事情,比如棧越界,將會造成其它線程的數據錯亂。

為了能減緩棧越界帶來的影響,作業系統引入了 stack guard 的概念,就是給每個線程棧多分配一頁(4k)或多頁內存,這片內存不可讀、不可寫、不可執行,只要訪問就會造成段錯誤。

我們以一個實際的例子來看棧越界,代碼如下所示。

static void *thread_illegal_access(void *arg) {
sleep(1);
char p[1];
int i;
for (i = 0; i < 1024; ++i) {
printf("[%d] access address: %p\n", i, &p[i * 1024]);
p[i * 1024] = 'a';
}
}
static void *thread_nothing(void *arg) {
sleep(1000);
return NULL;
}

int main() {
pthread_t t1;
pthread_t t2;

pthread_create(&t1, NULL, thread_nothing, NULL);
pthread_create(&t2, NULL, thread_illegal_access, NULL);

char str[100];
sprintf(str, "cat /proc/%d/maps > proc_map.txt", getpid());
system(str);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}

編譯上面的 c 文件,使用 strace 執行,部分系統調用如下所示。

// thread 1
mmap(NULL, 8392704,
PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK,
-1, 0) = 0x7f228d615000

mprotect(0x7f228d615000, 4096, PROT_NONE) = 0

clone(child_stack=0x7f228de14fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f228de159d0, tls=0x7f228de15700, child_tidptr=0x7f228de159d0) = 9696

// thread 2
mmap(NULL, 8392704,
PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f228ce14000
mprotect(0x7f228ce14000, 4096, PROT_NONE) = 0
clone(child_stack=0x7f228d613fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f228d6149d0, tls=0x7f228d614700, child_tidptr=0x7f228d6149d0) = 9697

在 linux 中,一個線程棧的默認大小是 8M(8388608),但是這裡 mmap 分配的內存塊大小卻是 8392704(8M+4k),這裡多出來的 4k 就是 stack guard 大小。

分配了 8M+4k 的內存以後,隨即使用 mprotect 將剛分配的內存塊的 4k 地址的權限改為了 PROT_NONE, PROT_NONE 表示拒絕所有訪問,不可讀、不可寫、不可執行。第二個線程創建的過程一模一樣,這裡不再贅述,兩個線程的內存布局如下所示。

$ ./thread_test
[0] access address: 0x7ffff6feef0b
[1] access address: 0x7ffff6fef30b
[2] access address: 0x7ffff6fef70b
[3] access address: 0x7ffff6fefb0b
[4] access address: 0x7ffff6feff0b
[5] access address: 0x7ffff6ff030b
[1] 18133 segmentation fault ./thread_test

我們可以看到最後 access 導致段錯誤的地址是 0x7ffff6ff030b,這個地址正好位於線程 1 的 guard 區域內。最後一個合法的範圍還處於 t2 的線程棧的合法區域中,如下所示。

Java 線程棧溢出是如何處理的

前面介紹過,Linux 的線程通過 4k 的 Guard 區域實現了棧溢出的簡單預防,只要讀寫 Guard 區域就會出現段錯誤。那有沒有想過 Java 是如何處理棧溢出的呢?

Java 線程的棧溢出時,進程不會退出,StackOverflowError 異常還可以被捕獲,程序可以繼續運行,以下面的代碼為例。

public class ThreadStackTest0 {
private static void foo(int i) {
foo(i + 1);
}
public static void main(String[] args) throws IOException {
System.out.println("in main");
new Thread(new Runnable() {
@Override
public void run() {
foo(0);
}
}).start();
System.in.read();
}
}

編譯運行上面的代碼,可以看到

$ javac ThreadStackTest0.java; java -cp . ThreadStackTest0

in main
Exception in thread "Thread-0" java.lang.StackOverflowError
at ThreadStackTest0.foo(ThreadStackTest0.java:8)
at ThreadStackTest0.foo(ThreadStackTest0.java:8)
// ...

首先解決第一個疑惑,Java 的普通線程有沒有 Linux 原生線程的那種 4k 的 Guard 區域呢?

首先來說答案,答案是沒有的。Hotspot 源碼中創建線程的代碼在 os_linux.cpp 中,

bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {
// ...
// glibc guard page
pthread_attr_setguardsize(&attr, os::Linux::default_guard_size(thr_type));
// ...
}

guard 區域的大小是由 os::Linux::default_guard_size 這個方法確定的,這個方法的內部實現比較簡單,判斷線程的類型是不是普通的 Java 線程,如果是的話,guard 的大小設置為 0,如果不是則設置為 4k。

什麼是 Java 的普通線程呢?除了用戶手動 new Thread() 方式創建的 java 線程,其實還有不少 JVM 運行需要的額外的輔助線程,比如 GC 線程、編譯線程、watcher 線程等。從源碼調試的結果可以看到,對於 Java 線程,guard 區域大小被設置為 0,其他類型的線程都被設置為默認的 4k。

不要高興的太早,沒有 Linux 原生線程標準的 guard 區域,不代表 Java 線程沒有自己實現。實際上 Hotspot 不光自己接管了 Guard 區域,它還實現了兩個,一個叫 Yellow Zone,一個叫 Red Zone,如下所示。

java_thread_stack

其中 Yellow Zone 的默認大小為 8k,可以通過 -XX:StackRedPages 來指定,Red Zone 的默認大小為 4k,可以通過 -XX:StackRedPages 來指定。這 12k 的權限都是 PROT_NONE,也就是不可讀不可寫不可執行,讀寫這一塊區域都會觸發 Segmentation Fault(SIGSEGV),JVM 為了能自己處理棧溢出異常,它處理了 SIGSEGV 這個信號。

接下來介紹一下這兩塊區域:

Yellow zone:這一塊區域是用來處理可恢復的棧溢出的,當棧溢出發生在這一塊區域時,會把這 8k 的內存區域的權限改為可讀可寫,隨後 JVM 會拋出 StackOverflowError 異常,StackOverflowError 這個異常應用層可以被捕獲進行處理。當異常拋出處理完以後,這 8k 內存區域的權限又會恢復為不可讀、不可寫、不可執行的狀態。Red zone:這一塊的區域是用來處理不可恢復的棧溢出的,算是線程棧最後的防線了。這個區域的棧溢出,JVM 會視為致命錯誤,進程會退出並生成 hs_err_pid.log 文件。當棧溢出在這個區域時,會首先把這 4k 的權限改為可讀可寫,以便留一些棧空間生成 hs_err_pid.log 文件。

完整的代碼見 os_linux_x86.cpp,如下所示。

extern "C" JNIEXPORT int
JVM_handle_linux_signal(int sig, siginfo_t* info, void* ucVoid, int abort_if_unrecognized) {
// Handle ALL stack overflow variations here
if (sig == SIGSEGV) {
address addr = (address) info->si_addr;

// 檢查發生段錯誤的地址是不是在棧內存的有效範圍內 [stack_base-stack_size, stack_base]
if (addr < thread->stack_base() &&
addr >= thread->stack_base() - thread->stack_size()) {
// stack overflow
// 發生段錯誤的地址處於 yellow 區域
if (thread->in_stack_yellow_zone(addr)) {
// 先把 yellow zone 的 8k 權限改為可讀可寫,以便調用拋出 STACK_OVERFLOW 異常
thread->disable_stack_yellow_zone();
if (thread->thread_state() == _thread_in_Java) {
// Throw a stack overflow exception. Guard pages will be reenabled
// while unwinding the stack.
stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::STACK_OVERFLOW);
} else {
// Thread was in the vm or native code. Return and try to finish.
return 1;
}
} else if (thread->in_stack_red_zone(addr)) { // 如果地址在 red zone
// Fatal red zone violation. Disable the guard pages and fall through
// to handle_unexpected_exception way down below.
// 先 disable 掉 red zone,把權限改為可讀可寫,方便留出 4k 的棧給生成 hs_err_pid.log 文件的代碼使用
thread->disable_stack_red_zone();
tty->print_raw_cr("An irrecoverable stack overflow has occurred.");

// This is a likely cause, but hard to verify. Let's just print
// it as a hint.
tty->print_raw_cr("Please check if any of your loaded .so files has "
"enabled executable stack (see man page execstack(8))");
} else {

}
}
}

Java 線程棧的大小最小是多少?

這是一個比較有意思的問題,之前也沒有怎麼多想過,只知道默認的棧大小為 1M,那我們隨便試一下:

可以看到在我的 64 位 Centos7 系統上,這個值為棧大小最小要指定 228k,這個值怎麼來的呢?我們來看看源碼。

os::Linux::min_stack_allowed = MAX2(os::Linux::min_stack_allowed,
(size_t)(StackYellowPages+StackRedPages+StackShadowPages) * Linux::page_size() +
(2*BytesPerWord COMPILER2_PRESENT(+1)) * Linux::vm_default_page_size());

其中 MAX2 函數表示取兩個入參的最大值,os::Linux::min_stack_allowed 的值為 64k,StackYellowPages=2,StackRedPages=1,StackShadowPages=20,Linux::page_size() 的值為 4k,BytesPerWord=8,Linux::vm_default_page_size() 的值為 8k。

min_stack_allowed = max(64k, (2 + 1 + 20) * 4k + (2 * 8 + 1) * 8k)
= max(64k, 228k) = 228k

在 Mac 上 Xss 的最小值為 160k,它的計算規則有一點不太一樣,源碼如下:

os::Bsd::min_stack_allowed = MAX2(os::Bsd::min_stack_allowed,
(size_t)(StackYellowPages+StackRedPages+StackShadowPages+
2*BytesPerWord COMPILER2_PRESENT(+1)) * Bsd::page_size());

計算的過程也就是:

min_stack_allowed = (2 + 1 + 20 + 16 + 1) * 4k = 160k

小結

這篇文章希望你能夠了解到下面這些知識:

進程與線程的生成,底層都是由 clone 系統調用生成進程與線程的一大區別在於進程擁有各自獨立的進程資源,線程則是共享進程的資源linux 線程棧的默認大小為 8M,除了線程棧的內存,每個線程還會額外多 4k 的 guard 區域防止棧溢出JHotspot 的普通線程的 guard 區域大小為 0,不過自己接管了 Guard 區域的實現Hotspot 通過 Yellow-Zone、Red-Zone 這兩個區域和自定義 SIGSEGV 信號的處理實現了棧溢出的處理JVM 的 XSS 最小值與平臺相關,具體的算法可以參考上面的內容

相關焦點

  • 程式設計師每日一題-jvm裡方法和方法區、棧區的二三事
    線程共享,主要是存放對象實例和數組。堆區是jvm裡面最需要深入研究的一塊區域,這裡面涉及內存分配,區域劃分,對象信息,垃圾回收。可以說如果java程式設計師對堆區不熟悉,那麼一定寫不出好的代碼。本文暫時不深入討論,後續會開專題深入講解。B:棧區,也叫虛擬機棧,顧名思義,它是一個棧,先進後出。它是線程創建時跟著創建,生命周期和線程一致,是線程私有的。
  • JVM筆記四-棧區
    棧數據結構特點:先進後出。生活中常見的case就是彈夾。最後一個壓進彈夾的子彈,最先出彈夾。Stack棧:棧也叫棧內存,主管Java程序的運行,是在線程創建時創建,它的生命周期跟隨線程的生命周期,線程結束,棧內存也就被釋放了。對於棧來說,不存在垃圾回收問題,只要是線程一結束,該棧就over了。
  • 順序棧與鏈式棧的圖解與實現
    # 順序棧與鏈式棧的圖解與實現棧是一種特殊的線性表,它與線性表的區別體現在增刪操作上棧的特點是先進後出,後進先出,也就是說棧的數據操作只能發生在末端,而不允許在中間節點進行操作如上圖所示,對棧的增刪操作都只能在末端也就是棧頂操作
  • 談談java的棧和堆
    先用一張圖展示一下Java堆棧的概況堆方法區:class文件信息,運行時常量池,以及編譯器編譯後的代碼堆:存儲對象,對象包含類的信息,指向方法區棧每個方法被調用和完成的過程,都對應一個棧幀從虛擬機棧上入棧和出棧的過程。
  • JVM內存區域之線程私有區域
    我們可以把windows的CPU+緩存+主內存和JVM的執行引擎+操作數棧+(棧、堆)對應起來,這樣更加利於我們去理解JVM。虛擬機棧:從上圖可見,java虛擬機棧是線程私有的,它的生命周期和線程相同。
  • Linux 多線程詳解 —— 什麼是線程
    線程是怎樣描述的?線程實際上也是一個task_struct,工作線程拷貝主線程的task_struct,然後共用主線程的mm_struct。線程ID是在用task_struct中pid描述的,而task_struct中tgid是線程組ID,表示線程屬於該線程組,對於主線程而言,其pid和tgid是相同的,我們一般看到的進程ID就是tgid。
  • java多線程之Thread構造函數(源碼分析)
    在上一篇文章中對線程狀態生命周期和常見的線程api進行了一個講解。這篇文章開始著重對其構造方法進行一個說明,也將揭曉為什麼我們調用了start方法就能啟動一個線程。一、守護線程和非守護線程我們獲取線程的id的時候會發現每次都不是0,這是因為在java虛擬機運行一個線程的時候會默認啟動一些其他的線程,來為我們的線程服務。
  • iOS線程生命周期的監控
    同時也提供了一套基於C語言的GCD線程池函數庫來支持多線程的處理應用。這些高級的線程類或者函數的內部實現大部分最終都會調用POSIX標準中的pthread線程庫中的pthread_xxx系列函數(#include <pthread.h>)來完成線程的創建、運行、暫停、恢復、銷毀、結束等操作。
  • 棧棧棧棧棧棧棧棧棧棧棧棧棧棧棧棧棧棧
    明白了棧的基本操作後,我們需要去深入地思考一下,棧是如何工作的。換句話說,為了使棧這個數據結構按照棧的方式去工作,它需要什麼?1)棧需要有一個指針,我們稱之為 TOP,用它來指向棧中最頂部的那個元素。2)當我們初始化一個棧的時候,我們把 TOP 的值設置為 -1,這樣我們就可以通過 TOP == -1 來判斷棧是否為空。3)當我們要在棧中壓入一個元素的時候,我們把 TOP 的值加 1,然後把新壓入的元素指向 TOP。
  • 阿里面試官問我Java線程和作業系統線程什麼關係
    當線程在用戶空間下實現時,作業系統對線程的存在一無所知,作業系統只能看到進程,而不能看到線程。所有的線程都是在用戶空間實現。在作業系統看來,每一個進程只有一個線程。綠色線程並不依賴底層的系統功能,模擬實現了多線程的運行,這種線程的管理調配發生在用戶空間而不是內核空間,所以它們可以在沒有原生線程支持的環境中工作。在Java 1.1中,綠色線程(至少在 Solaris 上)是JVM 中使用的唯一一種線程模型。
  • 看完這篇還不懂線程與線程池你來打我
    你可能會有疑問,講多線程為什麼要從 CPU 說起呢?原因很簡單,在這裡沒有那些時髦的概念,你可以更加清晰地看清問題的本質。CPU 並不知道線程、進程之類的概念。CPU 只知道兩件事:1. 從內存中取出指令;2. 執行指令,然後回到 1。
  • 一篇文章全面吃透Java虛擬機,線程安全的實現方法
    01前言jvm虛擬機包含堆,棧,本地方法棧,類裝載子系統,方法區,程序計數器也就是字節碼執行引擎。其中運行時數據區包含即內存模型包含堆,棧,本地方法棧。即類裝載子系統。我們的代碼調優也主要是在堆內存中進行。
  • 解析| 深入了解Apache Flink的網絡協議棧
    Flink 的網絡協議棧在這一點的處理上,不再處理單個記錄,而是將一組序列化的記錄填充到網絡緩衝區中進行處理。然而,與先前實現相比,總體內存使用可能仍然會降低,因為底層的網絡協議棧不再需要緩存大量數據,因為我們總是可以立即將其傳輸到 Flink(一定會有相應的 Buffer 接收數據)。在使用新的 Credit-based 流量控制時,可能還會注意到另一件事:由於我們在發送方和接收方之間緩衝較少的數據,反壓可能會更早的到來。然而,這是我們所期望的,因為緩存更多數據並沒有真正獲得任何好處。
  • Java 線程面試題 Top 50
    不同的進程使用不同的內存空間,而所有的線程共享一片相同的內存空間。別把它和棧內存搞混,每個線程都擁有單獨的棧內存用來存儲本地數據。3) 如何在Java中實現線程?在語言層面有兩種方式。每個線程都有自己的棧內存,用於存儲本地變量,方法參數和棧調用,一個線程中存儲的變量對其它線程是不可見的。而堆是所有線程共享的一片公用內存區域。對象都在堆裡創建,為了提升效率線程會從堆中弄一個緩存到自己的棧,如果多個線程使用該變量就可能引發問題,這時volatile 變量就可以發揮作用了,它要求線程從主存中讀取變量的值。25) 什麼是線程池? 為什麼要使用它?
  • 並發的本質:線程和進程?
    每個線程都可以去執行一個子任務,這樣一個進程就包括了多個線程,每個線程負責一個獨立的子任務。既然是在 一個進程內,那麼多個線程必然共享了進程的獨立地址空間或者說數據空間,每個線程控制自己的任務變量(程序計數器)和棧信息(存儲各任務的變量信息)進程和線程區別,圖片引自《深入理解計算機作業系統》課程線程讓進程中的內部並發成為了可能。
  • 深入分析Java虛擬機堆和棧及OutOfMemory異常產生原因
    而有一些數據區域為每個線程獨佔的,每個線程獨佔數據區域在線程創建時創建,在線程退出時銷毀,線程獨佔的數據區就不會有安全性問題。Run-Time Data Areas主要包括如下部分:pc寄存器,堆,方法區,虛擬機棧,本地方法棧。
  • 如何編寫線程安全的代碼?
    接下來我們了解一下什麼是線程安全,怎樣才能做到線程安全。這些問題解答後,多線程這頭大怪獸自然就會變成溫順的小貓咪。,只有這樣公共場所的秩序才不會被破壞,線程以某種不妨礙到其它線程的秩序使用共享資源就能實現線程安全。
  • 為什麼校招面試中總被問「線程與進程的區別」?我該如何回答?
    線程是進程的一個實體,是獨立運行和獨立調度的基本單位(CPU上真正運行的是線程)。線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。
  • fork() 函數與 Linux 中的多線程編程
    ,線程是最小的調度單位)。子進程的棧、數據以及棧段開始時是父進程內存相應各部分的完全拷貝,因此它們互不影響。從性能方面考慮,父 進程到子進程的數據拷貝並不是創建時就拷貝了的,而是採用了寫時拷貝(copy-on -write)技術來處理。調用fork()之後,父進程與子進程的執行順序是我們無法確定的(即調度進程使用CPU),意識到這一點極為重要,因為在一些設計不好的程序中會導致資源競爭,從而出現不可預知的問題。
  • java線程前傳——jvm內存結構、內存模型和cpu結構
    ,多個線程可以運行在不同的核上,這個時候,因為緩存的存在,如果沒有同步機制,那一個線程修改了緩存的數據,另一個線程也修改了緩存的數據,這個時候這兩個線程修改後的數據都需要寫入到內存當中,就會出現問題jvm為了方便,將這些緩存抽象出來,構造了自己的內存模型,即主內存和工作內存的數據交互,即java 內存模型(jmm)2、jvm 內存結構當使用java -jar