溫馨提示×

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

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

DEBUG方式線程的底層運(yùn)行原理是什么

發(fā)布時(shí)間:2021-06-21 18:12:17 來(lái)源:億速云 閱讀:144 作者:Leah 欄目:開(kāi)發(fā)技術(shù)

這篇文章將為大家詳細(xì)講解有關(guān)DEBUG方式線程的底層運(yùn)行原理是什么,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。


一、Java 運(yùn)行時(shí)數(shù)據(jù)區(qū)域

友情提示:這部分內(nèi)容可能大部分同學(xué)都有一定的了解了,可以跳過(guò)直接進(jìn)入下一小節(jié)哈。

Java 虛擬機(jī)在執(zhí)行 Java 程序的過(guò)程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,這些區(qū)域都有各自的用途,以及創(chuàng)建和銷毀的時(shí)間。

全文我們都將以 JDK 7 的運(yùn)行時(shí)數(shù)據(jù)區(qū)域?yàn)槔?/p>

DEBUG方式線程的底層運(yùn)行原理是什么

先簡(jiǎn)單解釋下線程共享和線程私有是啥意思。

所謂線程私有,通俗來(lái)說(shuō)就是每個(gè)線程都會(huì)創(chuàng)建一個(gè)屬于自己的東西,每個(gè)線程之間的這塊私有區(qū)域互不影響,獨(dú)立存儲(chǔ)。比如程序計(jì)數(shù)器就是線程私有的,每個(gè)線程都會(huì)擁有一個(gè)屬于自己的程序計(jì)數(shù)器,互不干涉。

線程共享就沒(méi)啥好說(shuō)的,簡(jiǎn)單理解為公共場(chǎng)所,誰(shuí)都能去,存儲(chǔ)的數(shù)據(jù)所有線程都能訪問(wèn)。

OK,然后我們來(lái)逐個(gè)分析下每個(gè)區(qū)域都是用來(lái)存儲(chǔ)什么的。當(dāng)然了,這里不會(huì)做太多詳細(xì)的說(shuō)明,不然會(huì)使文章顯得非常臃腫,在理解本文的基礎(chǔ)上能夠讓大家對(duì)各個(gè)區(qū)域有基本的認(rèn)知就好了。

首先來(lái)看一下線程共享的兩個(gè)區(qū)域:

1)Java 堆(Java Heap)是 Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。這一點(diǎn)在 Java 虛擬機(jī)規(guī)范中的描述是:所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配。

2)方法區(qū)(Method Area)與 Java 堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。

很多人習(xí)慣的把方法區(qū)稱為永久代(Permanent Generation),但實(shí)際上這兩者并不等價(jià)。通俗來(lái)說(shuō),方法區(qū)是一種規(guī)范,而永久代是 HotSpot 虛擬機(jī)實(shí)現(xiàn)這個(gè)規(guī)范的一種手段,對(duì)于其他虛擬機(jī)(比如 BEA JRockit、IBM J9 等)來(lái)說(shuō)是不存在永久代的概念的。

另外,對(duì)于 HotSpot 虛擬機(jī)來(lái)說(shuō),它在 JDK 8 中完全廢棄了永久代的概念,改用與 JRockit、J9 一樣在本地內(nèi)存中實(shí)現(xiàn)的元空間(Meta-space)來(lái)代替,把 JDK 7 中永久代還剩余的內(nèi)容(主要是類型信息)全部移到元空間中。

再來(lái)看看線程私有的三個(gè)區(qū)域:

1)虛擬機(jī)棧(Java Virtual Machine Stacks)其實(shí)是由一個(gè)一個(gè)的棧幀(Stack Frame)組成的,一個(gè)棧幀描述的就是一個(gè) Java 方法執(zhí)行的內(nèi)存模型。也就是說(shuō)每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀,用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法的返回地址等信息。

DEBUG方式線程的底層運(yùn)行原理是什么

每一個(gè)方法從調(diào)用直至執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過(guò)程,當(dāng)然,出棧的順序自然是遵守棧的后進(jìn)先出原則的。

棧幀的概念在接下來(lái)的原理解析部分非常重要,各位務(wù)必搞懂哈。

2)本地方法棧(Native Method Stack)和上面我們所說(shuō)的虛擬機(jī)棧作用基本一樣,區(qū)別只不過(guò)是本地方法棧為虛擬機(jī)使用到的 Native 方法服務(wù),而虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(也就是字節(jié)碼)服務(wù)。

這里解釋一下 Native 方法的概念,其實(shí)不僅 Java,很多語(yǔ)言中都有這個(gè)概念。

"A native method is a Java method whose implementation is provided by non-java code."

就是說(shuō)一個(gè) Native 方法其實(shí)就是一個(gè)接口,但是它的具體實(shí)現(xiàn)是在外部由非 Java 語(yǔ)言寫的。所以同一個(gè) Native 方法,如果用不同的虛擬機(jī)去調(diào)用它,那么得到的結(jié)果和運(yùn)行效率可能是不一樣的,因?yàn)椴煌奶摂M機(jī)對(duì)于某個(gè) Native 方法都有自己的實(shí)現(xiàn),比如 Object 類的 hashCode 方法。

這使得 Java 程序能夠超越 Java 運(yùn)行時(shí)的界限,有效地?cái)U(kuò)充了 JVM。

3)程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來(lái)完成。

由于 Java 虛擬機(jī)的多線程是通過(guò)輪流分配 CPU 時(shí)間片的方式來(lái)實(shí)現(xiàn)的,因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器。

那么程序計(jì)數(shù)器里存的到底是什么東西呢?

《深入理解 Java 虛擬機(jī):JVM 高級(jí)實(shí)踐與最佳實(shí)戰(zhàn) - 第 2 版》給出了答案:如果線程正在執(zhí)行的是一個(gè) Java 方法,程序計(jì)數(shù)器中記錄的就是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是 Native 方法,這個(gè)計(jì)數(shù)器值則為空(Undefined)。

二、用 DEBUG 的方式看線程運(yùn)行原理

接下來(lái),我們就通過(guò) DEBUG 這段代碼來(lái)看下線程的運(yùn)行原理:

DEBUG方式線程的底層運(yùn)行原理是什么

上述代碼的邏輯非常簡(jiǎn)單,main 方法調(diào)用了 method1 方法,而 method1 方法又調(diào)用了 method2 方法。

看下圖,我們打了一個(gè)斷點(diǎn):

DEBUG方式線程的底層運(yùn)行原理是什么

OK,以 DEBUG 的方式運(yùn)行 Test.main(),雖然這里我們沒(méi)有顯示的創(chuàng)建線程,但是 main 函數(shù)的調(diào)用本身就是一個(gè)線程,也被稱為主線程(main 線程),所以我們一啟動(dòng)這個(gè)程序,就會(huì)給這個(gè)主線程分配一個(gè)虛擬機(jī)棧內(nèi)存。

DEBUG方式線程的底層運(yùn)行原理是什么

上文我們也說(shuō)了,虛擬機(jī)棧內(nèi)存其實(shí)就是個(gè)殼兒,里面真正存儲(chǔ)數(shù)據(jù)的,其實(shí)是一個(gè)一個(gè)的棧幀,每個(gè)方法都對(duì)應(yīng)著一個(gè)棧幀。

所以當(dāng)主線程調(diào)用 main 方法的時(shí)候,就會(huì)為 main 方法生成一個(gè)棧幀,其中存儲(chǔ)了局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法的返回地址等信息。

各位現(xiàn)在可以看看 DEBUG 窗口顯示的界面:

DEBUG方式線程的底層運(yùn)行原理是什么

左邊的 Frames 就是棧幀的意思,可以看見(jiàn)現(xiàn)在主線程中只有一個(gè) main 棧幀;

右邊的 Variables 就是該棧幀存儲(chǔ)的局部變量表,可以看到現(xiàn)在 main 棧幀中只有一個(gè)局部變量,也就是方法參數(shù) args。

1)Step Over:F8

DEBUG方式線程的底層運(yùn)行原理是什么

程序向下執(zhí)行一行,如果當(dāng)前行有方法調(diào)用,這個(gè)方法將被執(zhí)行完畢并返回,然后到下一行

2)Step Into:F7

DEBUG方式線程的底層運(yùn)行原理是什么

程序向下執(zhí)行一行,如果該行有自定義方法,則運(yùn)行進(jìn)入自定義方法(不會(huì)進(jìn)入官方類庫(kù)的方法)

3)Force Step Into:Alt + Shift + F7

DEBUG方式線程的底層運(yùn)行原理是什么

程序向下執(zhí)行一行,如果該行有自定義方法或者官方類庫(kù)方法,則運(yùn)行進(jìn)入該方法(也就是可以進(jìn)入任何方法)

4)Step Out:Shift + F8

DEBUG方式線程的底層運(yùn)行原理是什么

如果在調(diào)試的時(shí)候你進(jìn)入了一個(gè)方法,并覺(jué)得該方法沒(méi)有問(wèn)題,你就可以使用 Step Out 直接執(zhí)行完該方法并跳出,返回到該方法被調(diào)用處的下一行語(yǔ)句。

5)Drop frame

DEBUG方式線程的底層運(yùn)行原理是什么

點(diǎn)擊該按鈕后,你將返回到當(dāng)前方法的調(diào)用處重新執(zhí)行,并且所有上下文變量的值也回到那個(gè)時(shí)候。只要調(diào)用鏈中還有上級(jí)方法,可以跳到其中的任何一個(gè)方法。

OK,我們點(diǎn)擊 Step Into 進(jìn)入 method1 方法,可以看到,虛擬機(jī)棧內(nèi)存中又多出了一個(gè) method1 棧幀:

DEBUG方式線程的底層運(yùn)行原理是什么

再點(diǎn)擊 Step Into 直到進(jìn)入 method2 方法,于是虛擬機(jī)棧內(nèi)存中又多出了一個(gè) method2 棧幀:

DEBUG方式線程的底層運(yùn)行原理是什么

當(dāng)我們 Step Into 走到 method2 方法中的 return n 語(yǔ)句后,n 指向的堆中的地址就會(huì)被返回給 method1 中的 m,并且,滿足棧后進(jìn)先出的原則,method2 棧幀會(huì)從虛擬機(jī)棧內(nèi)存中被銷毀。

DEBUG方式線程的底層運(yùn)行原理是什么

然后點(diǎn)擊 Step Over 執(zhí)行完輸出語(yǔ)句(Step Into 會(huì)進(jìn)入 println 方法,Force Step Into 會(huì)進(jìn)入 Object.toString 方法)

至此,method1 的使命全部完成,method1 棧幀會(huì)從虛擬機(jī)棧內(nèi)存中被銷毀。

DEBUG方式線程的底層運(yùn)行原理是什么

最后再往下走一步,main 棧幀也會(huì)被銷毀,這里就不再貼圖了。

三、線程運(yùn)行原理詳細(xì)圖解

上面寫了這么多,其實(shí)也就是教會(huì)了大家棧幀這個(gè)東西,接下來(lái)我們通過(guò)圖解的方式,來(lái)帶大家詳細(xì)看看線程運(yùn)行時(shí),Java 運(yùn)行時(shí)數(shù)據(jù)區(qū)域的各種變化。

首先第一步,類加載。

《深入理解 Java 虛擬機(jī):JVM 高級(jí)實(shí)踐與最佳實(shí)戰(zhàn) - 第 2 版》中是這樣解釋類加載的:虛擬機(jī)把描述類的數(shù)據(jù)從 Class 文件(字節(jié)碼文件)加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的 Java 類型,這就是虛擬機(jī)的類加載機(jī)制。

而加載進(jìn)來(lái)的這些字節(jié)碼信息,就存儲(chǔ)在方法區(qū)中。看下圖,這里為了各位理解方便,我就不寫字節(jié)碼了,直接按照代碼來(lái),大家知道這里存的其實(shí)是字節(jié)碼就行

DEBUG方式線程的底層運(yùn)行原理是什么

主線程調(diào)用 main 方法,于是為該方法生成一個(gè) main 棧幀:

DEBUG方式線程的底層運(yùn)行原理是什么

那么這個(gè)參數(shù) args 的值從哪里來(lái)呢?沒(méi)錯(cuò),就是從堆中 new 出來(lái)的:

DEBUG方式線程的底層運(yùn)行原理是什么

而 main 方法的返回地址就是程序的退出地址。

再來(lái)看程序計(jì)數(shù)器,如果線程正在執(zhí)行的是一個(gè) Java 方法,程序計(jì)數(shù)器中記錄的就是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址,也就是說(shuō)此時(shí) method1(10) 對(duì)應(yīng)的字節(jié)碼指令的地址會(huì)被放入程序計(jì)數(shù)器,圖片中我們?nèi)匀灰跃唧w的代碼代替哈,大家知道就好

DEBUG方式線程的底層運(yùn)行原理是什么

OK,CPU 根據(jù)程序計(jì)數(shù)器的指示,進(jìn)入 method1 方法,自然,method1 棧幀就被創(chuàng)建出來(lái)了:

DEBUG方式線程的底層運(yùn)行原理是什么

局部變量表和方法返回地址安頓好后,就可以開(kāi)始具體的方法調(diào)用了,首先 10 會(huì)被傳給 x,然后走到 y 被賦值成 x + 1 這步,也就是程序計(jì)數(shù)器會(huì)被修改成這步代碼對(duì)應(yīng)的字節(jié)碼指令的地址:

DEBUG方式線程的底層運(yùn)行原理是什么

走到 Object m = method2(); 這一步的時(shí)候,又會(huì)創(chuàng)建一個(gè) method2 棧幀:

DEBUG方式線程的底層運(yùn)行原理是什么

可以看到,method2 方法的第一行代碼會(huì)在堆中創(chuàng)建一個(gè) Object 對(duì)象:

DEBUG方式線程的底層運(yùn)行原理是什么

隨后,走到 method2 方法中的 return n; 語(yǔ)句,n 指向的堆中的地址就會(huì)被返回給 method1 中的 m,并且,滿足棧后進(jìn)先出的原則,method2 棧幀會(huì)從虛擬機(jī)棧內(nèi)存中被銷毀:

DEBUG方式線程的底層運(yùn)行原理是什么

根據(jù) method2 棧幀指向的方法返回地址,我們接著執(zhí)行 System.out.println(m.toString()) 這條輸出語(yǔ)句,執(zhí)行完后,method1 棧幀也被銷毀了:

DEBUG方式線程的底層運(yùn)行原理是什么

再根據(jù) method1 棧幀指向的方法返回地址,發(fā)現(xiàn)我們的程序已走到了生命的盡頭,main 棧幀于是也被銷毀了,就不再貼圖了。

四、用 DEBUG 的方式看多線程運(yùn)行原理

上面說(shuō)的是只有一個(gè)線程的情況,其實(shí)多線程的原理也差不多,因?yàn)樘摂M機(jī)棧是每個(gè)線程私有的,大家互不干涉,這里我就簡(jiǎn)單的提一嘴。

分別在如下兩個(gè)位置打上 Thread 類型的斷點(diǎn):

DEBUG方式線程的底層運(yùn)行原理是什么

然后以 DEBUG 方式運(yùn)行,你就會(huì)發(fā)現(xiàn)存在兩個(gè)互不干涉的虛擬機(jī)??臻g:

DEBUG方式線程的底層運(yùn)行原理是什么

當(dāng)然,使用多線程就不可避免的會(huì)遇到一個(gè)問(wèn)題,那就是線程的上下文切換(Thread Context Switch),就是說(shuō)因?yàn)槟承┰驅(qū)е?CPU 不再執(zhí)行當(dāng)前的線程,轉(zhuǎn)而執(zhí)行另一個(gè)線程。

導(dǎo)致線程上下文切換的原因大概有以下幾種:

1)線程的 CPU 時(shí)間片用完

2)發(fā)生了垃圾回收

3)有更高優(yōu)先級(jí)的線程需要運(yùn)行

4)線程自己調(diào)用了 sleep、yield、wait、join、park、synchronized、lock 等方法

當(dāng)線程的上下文切換發(fā)生時(shí),也就是從一個(gè)線程 A 轉(zhuǎn)而執(zhí)行另一個(gè)線程 B 時(shí),需要由操作系統(tǒng)保存當(dāng)前線程 A 的狀態(tài)(為了以后還能順利回來(lái)接著執(zhí)行),并恢復(fù)另一個(gè)線程 B 的狀態(tài)。

這個(gè)狀態(tài)就包括每個(gè)線程私有的程序計(jì)數(shù)器和虛擬機(jī)棧中每個(gè)棧幀的信息等,顯然,每次操作系統(tǒng)都需要存儲(chǔ)這么多的信息,頻繁的線程上下文切換勢(shì)必會(huì)影響程序的性能。

關(guān)于DEBUG方式線程的底層運(yùn)行原理是什么就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問(wèn)一下細(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)容。

AI