【JVM之內存與垃圾回收篇】虛擬機棧


虛擬機棧

虛擬機棧概述

由於跨平台性的設計,Java 的指令都是根據棧來設計的。不同平台 CPU 架構不同,所以不能設計為基於寄存器的。

優點是跨平台,指令集小,編譯器容易實現,缺點是性能下降,實現同樣的功能需要更多的指令。


有不少 Java 開發人員一提到 Java 內存結構,就會非常粗粒度地將 JVM 中的內存區理解為僅有 Java 堆(heap)和 Java 棧(stack)?為什么?

首先棧是運行時的單位,而堆是存儲的單位。

  • 棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。
  • 堆解決的是數據存儲的問題,即數據怎么放,放哪里

Java虛擬機棧是什么

Java 虛擬機棧(Java Virtual Machine Stack),早期也叫 Java 棧。每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應着一次次的 Java 方法調用。

Java 虛擬機棧是線程私有

生命周期

生命周期和線程一致,也就是線程結束了,該虛擬機棧也銷毀了

作用

主管Java程序的運行,它保存方法的局部變量、部分結果,並參與方法的調用和返回。

局部變量,它是相比於成員變量來說的(或屬性)

基本數據類型變量 VS 引用類型變量(類、數組、接口)

棧的特點

棧是一種快速有效的分配存儲方式,訪問速度僅次於罹序計數器。

JVM 直接對 Java 棧的操作只有兩個:

  • 每個方法執行,伴隨着進棧(入棧、壓棧)
  • 執行結束后的出棧工作

對於棧來說不存在垃圾回收(GC)問題(棧存在溢出(OOM)的情況)

面試題:開發中遇到哪些異常?

棧中可能出現的異常

Java 虛擬機規范允許 Java 棧的大小是動態的或者是固定不變的

  • 如果采用固定大小的 Java 虛擬機棧,那每一個線程的 Java 虛擬機棧容量可以在線程創建的時候獨立選定。如果線程請求分配的棧容量超過 Java 虛擬機棧允許的最大容量,Java 虛擬機將會拋出一個 StackOverflowError 異常
  • 如果 Java 虛擬機棧可以動態擴展,並且在嘗試擴展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那 Java 虛擬機將會拋出一個 OutOfMemoryError 異常
/**
 * 演示棧中的異常:StackOverflowError
 * @author: Nemo
 */
public class StackErrorTest {
    private static int count = 1;
    public static void main(String[] args) {
        System.out.println(count++);
        main(args);
    }
}

當棧深度達到 9803 的時候,就出現棧內存空間不足

設置棧內存大小

可查閱官方文檔:https://docs.oracle.com/en/java/javase/11/tools/tools-and-command-reference.html

我們可以使用參數 -Xss 選項來設置線程的最大棧空間,棧的大小直接決定了函數調用的最大可達深度

理解:Stack Size

-Xss1m
-Xss1k

棧的存儲單位

棧中存儲什么?

每個線程都有自己的棧,棧中的數據都是以棧幀(Stack Frame)的格式存在。

在這個線程上正在執行的每個方法都各自對應一個棧顏(Stack Frame)。

棧幀是一個內存區塊,是一個數據集,維系着方法執行過程中的各種數據信息。

OOP的基本概念:類和對象

類中基本結構:field(屬性、字段、域)、method

棧運行原理

JVM 直接對 Java 棧的操作只有兩個,就是對棧幀的壓棧出棧遵循“先進后出”/“后進先出”原則。

在一條活動線程中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱為當前棧幀(Current Frame),與當前棧幀相對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)。

執行引擎運行的所有字節碼指令只針對當前棧幀進行操作。

如果在該方法中調用了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,成為新的當前幀。

下面寫一個簡單的代碼

/**
 * 棧幀
 *
 * @author: Nemo
 */
public class StackFrameTest {
    public static void main(String[] args) {
        method01();
    }

    private static int method01() {
        System.out.println("方法1的開始");
        int i = method02();
        System.out.println("方法1的結束");
        return i;
    }

    private static int method02() {
        System.out.println("方法2的開始");
        int i = method03();;
        System.out.println("方法2的結束");
        return i;
    }
    private static int method03() {
        System.out.println("方法3的開始");
        int i = 30;
        System.out.println("方法3的結束");
        return i;
    }
}

輸出結果為

方法1的開始
方法2的開始
方法3的開始
方法3的結束
方法2的結束
方法1的結束

滿足棧先進后出的概念,通過 IDEA 的 DEBUG,能夠看到棧信息


不同線程中所包含的棧幀是不允許存在相互引用的,即不可能在一個棧幀之中引用另外一個線程的棧幀。

如果當前方法調用了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛擬機會丟棄當前棧幀,使得前一個棧幀重新成為當前棧幀。

Java 方法有兩種返回函數的方式,一種是正常的函數返回,使用 return 指令;另外一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出。

public class CurrentFrameTest {
    public void methodA() {
        System.out.println("當前棧幀對應的方法->methodA");
        methodB();
        System.out.println("當前棧幀對應的方法->methodA");
    }
    public void methodB() {
        System.out.println("當前棧幀對應的方法->methodB");
    }
}

棧幀的內部結構

每個棧幀中存儲着:

  • 局部變量表(Local Variables)
  • 操作數棧(operand Stack)(或 表達式棧)
  • 動態鏈接(DynamicLinking)(或 指向運行時常量池的方法引用)
  • 方法返回地址(Return Address)(或 方法正常退出或者異常退出的定義)
  • 一些附加信息

並行每個線程下的棧都是私有的,因此每個線程都有自己各自的棧,並且每個棧里面都有很多棧幀,棧幀的大小主要由局部變量表和操作數棧決定的

局部變量表

局部變量表:Local Variables,被稱之為局部變量數組或本地變量表

定義為一個數字數組,主要用於存儲方法參數和定義在方法體內的局部變量,這些數據類型包括各類基本數據類型、對象引用(reference),以及 returnAddress 類型。

由於局部變量表是建立在線程的棧上,是線程的私有數據,因此不存在數據安全問題

局部變量表所需的容量大小是在編譯期確定下來的,並保存在方法的 Code 屬性的 maximum local variables 數據項中。在方法運行期間是不會改變局部變量表的大小的。

方法嵌套調用的次數由棧的大小決定。一般來說,棧越大,方法嵌套調用次數越多。對一個函數而言,它的參數和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以滿足方法調用所需傳遞的信息增大的需求。進而函數調用就會占用更多的棧空間,導致其嵌套調用次數就會減少。

局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機通過使用局部變量表完成參數值到參數變量列表的傳遞過程。當方法調用結束后,隨着方法棧幀的銷毀,局部變量表也會隨之銷毀。


可以看到,在 Class 文件的局部變量表中,顯示了每個局部變量的作用域范圍、所在槽位的索引(index 列)、變量名(name 列)和數據類型(J 表示 long 類型)。

關於Slot的理解

參數值的存放總是在局部變量數組的 index0 開始,到數組長度 -1 的索引結束。

局部變量表,最基本的存儲單元是 Slot(變量槽)

局部變量表中存放編譯期可知的各種基本數據類型(8 種),引用類型(reference),returnAddress 類型的變量。

在局部變量表里,32 位以內的類型只占用一個 slot(包括 returnAddress 類型),64 位的類型(long 和 double)占用兩個 slot。

  • byte、short、char 在存儲前被轉換為 int,boolean 也被轉換為 int,0 表示 false,非 0 表示 true。
  • long 和 double 則占據兩個 slot。

JVM 會為局部變量表中的每一個 Slot 都分配一個訪問索引,通過這個索引即可成功訪問到局部變量表中指定的局部變量值

當一個實例方法被調用的時候,它的方法參數和方法體內部定義的局部變量將會按照順序被復制到局部變量表中的每一個 slot 上

如果需要訪問局部變量表中一個 64bit 的局部變量值時,只需要使用前一個索引即可。(比如:訪問 long 或 double 類型變量)

如果當前幀是由構造方法或者實例方法創建的,那么該對象引用 this 將會存放在 index 為 0 的 slot 處,其余的參數按照參數表順序繼續排列。

Slot的重復利用

棧幀中的局部變量表中的槽位是可以重用的,如果一個局部變量過了其作用域,那么在其作用域之后申明的新的局部變就很有可能會復用過期局部變量的槽位,從而達到節省資源的目的。

靜態變量與局部變量的對比

變量的分類:

按數據類型分:

  • 基本數據類型
  • 引用數據類型

按類中聲明的位置分:

  • 成員變量(類變量,實例變量):在使用前,都經歷過默認初始化賦值
    • 類變量:linking 的 prepare 階段,給類變量默認賦值。
      inital 階段:給類變量顯示賦值即靜態代碼塊賦值
    • 實例變量:隨着對象創建,會在堆空間中分配實例變量空間,並進行默認賦值
  • 局部變量:在使用前必須進行顯式賦值!否則,編譯不通過。

參數表分配完畢之后,再根據方法體內定義的變量的順序和作用域分配。

我們知道類變量表有兩次初始化的機會,第一次是在“准備階段”,執行系統初始化,對類變量設置零值,另一次則是在“初始化”階段,賦予程序員在代碼中定義的初始值。

和類變量初始化不同的是,局部變量表不存在系統初始化的過程,這意味着一旦定義了局部變量則必須人為的初始化,否則無法使用。

在棧幀中,與性能調優關系最為密切的部分就是前面提到的局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞。

局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收。

操作數棧

概念

操作數棧:Operand Stack

每一個獨立的棧幀除了包含局部變量表以外,還包含一個后進先出(Last-In-First-Out)的 操作數棧,也可以稱之為 表達式棧(Expression Stack)

操作數棧,在方法執行過程中,根據字節碼指令,往棧中寫入數據或提取數據,即入棧(push)/出棧(pop)

  • 某些字節碼指令將值壓入操作數棧,其余的字節碼指令將操作數取出棧。使用它們后再把結果壓入棧
  • 比如:執行復制、交換、求和等操作

代碼舉例

操作數棧,主要用於保存計算過程的中間結果,同時作為計算過程中變量臨時的存儲空間。

操作數棧就是 JVM 執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被創建出來,這個方法的操作數棧是空的。

這個時候數組是有長度的,因為數組一旦創建,那么就是不可變的

每一個操作數棧都會擁有一個明確的棧深度用於存儲數值,其所需的最大深度在編譯期就定義好了,保存在方法的 Code 屬性中,為 maxstack 的值。

棧中的任何一個元素都是可以任意的 Java 數據類型

  • 32bit 的類型占用一個棧單位深度
  • 64bit 的類型占用兩個棧單位深度

操作數棧並非采用訪問索引的方式來進行數據訪問的,而是只能通過標准的入棧和出棧操作來完成一次數據訪問

如果被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操作數棧中,並更新 PC 寄存器中下一條需要執行的字節碼指令。

操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類加載過程中的類檢驗階段的數據流分析階段要再次驗證。

另外,我們說 Java 虛擬機的解釋引擎是基於棧的執行引擎,其中的棧指的就是操作數棧。

代碼追蹤

我們給定代碼

public void testAddOperation() {
    byte i = 15;
    int j = 8;
    int k = i + j;
}

使用 javap 命令反編譯 class 文件: javap -v 類名.class

byte、short、char、boolean 內部都是使用int型來進行保存的

從上面的代碼我們可以知道,我們都是通過bipush對操作數 15 和 8進行入棧操作

同時使用的是 iadd方法進行相加操作,i -> 代表的就是 int,也就是int類型的加法操作

執行流程如下所示:

首先執行第一條語句,PC 寄存器指向的是 0,也就是指令地址為 0,然后使用 bipush 讓操作數 15 入棧。

執行完后,讓 PC + 1,指向下一行代碼,下一行代碼就是將操作數棧的元素存儲到局部變量表 1 的位置,我們可以看到局部變量表的已經增加了一個元素

為什么局部變量表不是從 0 開始的呢?

其實局部變量表也是從 0 開始的,但是因為 0 號位置存儲的是 this 指針,所以說就直接省略了~

然后 PC+1,指向的是下一行。讓操作數 8 也入棧,同時執行 store 操作,存入局部變量表中

然后從局部變量表中,依次將數據放在操作數棧中

然后將操作數棧中的兩個元素執行相加操作,並存儲在局部變量表 3 的位置

最后PC寄存器的位置指向 10,也就是 return 方法,則直接退出方法

程序員面試過程中,常見的 i++ 和 ++i 的區別,放到字節碼篇章時再介紹。

棧頂緩存技術

棧頂緩存技術:Top Of Stack Cashing

前面提過,基於棧式架構的虛擬機所使用的零地址指令更加緊湊,但完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味着將需要更多的指令分派(instruction dispatch)次數和內存讀/寫次數。

由於操作數是存儲在內存中的,因此頻繁地執行內存讀/寫操作必然會影響執行速度。為了解決這個問題,HotSpot JVM 的設計者們提出了棧頂緩存(ToS,Top-of-Stack Cashing)技術,將棧頂元素全部緩存在物理CPU的寄存器中,以此降低對內存的讀/寫次數,提升執行引擎的執行效率。

寄存器:指令更少,執行速度快

動態鏈接(或指向運行時常量池的方法引用)

動態鏈接:Dynamic Linking

動態鏈接、方法返回地址、附加信息 : 有些地方被稱為幀數據區

每一個棧幀內部都包含一個指向運行時常量池該棧幀所屬方法的引用包含這個引用的目的就是為了支持當前方法的代碼能夠實現動態鏈接(Dynamic Linking)。比如:invokedynamic 指令

在 Java 源文件被編譯到字節碼文件中時,所有的變量和方法引用都作為符號引用(symbolic Reference)保存在 class 文件的常量池里。

比如:描述一個方法調用了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那么動態鏈接的作用就是為了將這些符號引用轉換為調用方法的直接引用。

為什么需要運行時常量池?

因為在不同的方法,都可能調用常量或者方法,所以只需要存儲一份即可,節省了空間

常量池的作用:就是為了提供一些符號和常量,便於指令的識別

方法的調用:解析與分配

在 JVM 中,將符號引用轉換為調用方法的直接引用與方法的綁定機制相關

鏈接

靜態鏈接

當一個字節碼文件被裝載進 JVM 內部時,如果被調用的目標方法在編譯期可知,且運行期保持不變時,這種情況下降調用方法的符號引用轉換為直接引用的過程稱之為靜態鏈接

動態鏈接

如果被調用的方法在編譯期無法被確定下來,也就是說,只能夠在程序運行期將調用的方法的符號轉換為直接引用,由於這種引用轉換過程具備動態性,因此也被稱之為動態鏈接。

綁定機制

對應的方法的綁定機制為:早期綁定(Early Binding)和晚期綁定(Late Binding)。綁定是一個字段、方法或者類在符號引用被替換為直接引用的過程,這僅僅發生一次。

早期綁定

早期綁定就是指被調用的目標方法如果在編譯期可知,且運行期保持不變時,即可將這個方法與所屬的類型進行綁定,這樣一來,由於明確了被調用的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號引用轉換為直接引用。

晚期綁定

如果被調用的方法在編譯期無法被確定下來,只能夠在程序運行期根據實際的類型綁定相關的方法,這種綁定方式也就被稱之為晚期綁定。

早晚期綁定的發展歷史

隨着高級語言的橫空出世,類似於 Java 一樣的基於面向對象的編程語言如今越來越多,盡管這類編程語言在語法風格上存在一定的差別,但是它們彼此之間始終保持着一個共性,那就是都支持封裝、繼承和多態等面向對象特性,既然這一類的編程語言具備多態特悄,那么自然也就具備早期綁定和晚期綁定兩種綁定方式。

Java 中任何一個普通的方法其實都具備虛函數的特征,它們相當於 C++ 語言中的虛函數(C++ 中則需要使用關鍵字 virtual 來顯式定義)。如果在 Java 程序中不希望某個方法擁有虛函數的特征時,則可以使用關鍵字 final 來標記這個方法。

虛方法和非虛方法

非虛方法:

  • 如果方法在編譯期就確定了具體的調用版本,這個版本在運行時是不可變的。這樣的方法稱為非虛方法
  • 靜態方法、私有方法、final 方法、實例構造器、父類方法都是非虛方法。

虛方法

  • 其他方法稱為虛方法。

子類對象的多態的使用前提

  • 類的繼承關系
  • 方法的重寫

調用指令

虛擬機中提供了以下幾條方法調用指令:

普通調用指令:

  • invokestatic:調用靜態方法,解析階段確定唯一方法版本
  • invokespecial:調用 <init> 方法、私有及父類方法,解析階段確定唯一方法版本
  • invokevirtual:調用所有虛方法
  • invokeinterface:調用接口方法

動態調用指令:

  • invokedynamic:動態解析出需要調用的方法,然后執行

前四條指令固化在虛擬機內部,方法的調用執行不可人為干預,而 invokedynamic 指令則支持由用戶確定方法版本。其中 invokestatic 指令和 invokespecial 指令調用的方法稱為非虛方法,其余的(final 修飾的除外)稱為虛方法。

invokedynamic指令

JVM 字節碼指令集一直比較穩定,一直到 Java7 中才增加了一個 invokedynamic 指令,這是 Java 為了實現“動態類型語言”支持而做的一種改進。

但是在 Java7 中並沒有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 這種底層字節碼工具來產生 invokedynamic 指令。直到 Java8 的 Lambda 表達式的出現,invokedynamic 指令的生成,在 Java 中才有了直接的生成方式。

Java7 中增加的動態語言類型支持的本質是對 Java 虛擬機規范的修改,而不是對 Java 語言規則的修改,這一塊相對來講比較復雜,增加了虛擬機中的方法調用,最直接的受益者就是運行在 Java 平台的動態語言的編譯器。

動態類型語言和靜態類型語言

動態類型語言和靜態類型語言兩者的區別就在於對類型的檢查是在編譯期還是在運行期,滿足前者就是靜態類型語言,反之是動態類型語言。

說的再直白一點就是,靜態類型語言是判斷變量自身的類型信息;動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值才有類型信息,這是動態語言的一個重要特征。

Java:String info = "Nemo"; (Java 是靜態類型語言的,會先編譯就進行類型檢查)

Python:info = 130.5
JS:var name = "Nemo";
var name = 10; (運行時才進行檢查)

方法重寫的本質

Java 語言中方法重寫的本質:

  1. 找到操作數棧頂的第一個元素所執行的對象的實際類型,記作 C。
  2. 如果在類型 C 中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法的直接引用,查找過程結束;如果不通過,則返回 java.lang.IllegalAccessError 異常。
  3. 否則,按照繼承關系從下往上依次對 C 的各個父類進行第 2 步的搜索和驗證過程。
  4. 如果始終沒有找到合適的方法,則拋出 java.lang.AbstractMethodsrror 異常。

IllegalAccessError介紹

程序試圖訪問或修改一個屬性或調用一個方法,這個屬性或方法,你沒有權限訪問。一般的,這個會引起編譯器異常。這個錯誤如果發生在運行時,就說明一個類發生了不兼容的改變。

方法的調用:虛方法表

在面向對象的編程中,會很頻繁的使用到動態分派,如果在每次動態分派的過程中都要重新在類的方法元數據中搜索合適的目標的話就可能影響到執行效率。因此,為了提高性能,JVM 采用在類的方法區建立一個虛方法表
(virtual method table)(非虛方法不會出現在表中)來實現。使用索引表來代替查找。

每個類中都有一個虛方法表,表中存放着各個方法的實際入口。

虛方法表是什么時候被創建的呢?
虛方法表會在類加載的鏈接階段被創建並開始初始化,類的變量初始值准備完成之后,JVM 會把該類的方法表也初始化完畢。

如上圖所示:如果類中重寫了方法,那么調用的時候,就會直接在虛方法表中查找,否則將會直接連接到 Object 的方法中。

方法返回地址

存放調用該方法的 PC 寄存器的值。

一個方法的結束,有兩種方式:

  • 正常執行完成
  • 出現未處理的異常,非正常退出

無論通過哪種方式退出,在方法退出后都返回到該方法被調用的位置。方法正常退出時,調用者的 PC 計數器的值作為返回地址,即調用該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。

當一個方法開始執行后,只有兩種方式可以退出這個方法:

  • 執行引擎遇到任意一個方法返回的字節碼指令(return),會有返回值傳遞給上層的方法調用者,簡稱正常完成出口
    • 一個方法在正常調用完成之后,究竟需要使用哪一個返回指令,還需要根據方法返回值的實際數據類型而定。
    • 在字節碼指令中,返回指令包含 ireturn(當返回值是boolean,byte,char,short 和 int 類型時使用),lreturn(Long類型),freturn(Float類型),dreturn(Double類型),areturn。另外還有一個 return 指令聲明為 void 的方法,實例初始化方法,類和接口的初始化方法使用。
  • 在方法執行過程中遇到異常(Exception),並且這個異常沒有在方法內進行處理,也就是只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出,簡稱異常完成出口
    • 方法執行過程中,拋出異常時的異常處理,存儲在一個異常處理表,方便在發生異常的時候找到處理異常的代碼

本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的局部變量表、操作數棧、將返回值壓入調用者棧幀的操作數棧、設置 PC 寄存器值等,讓調用者方法繼續執行下去。

正常完成出口和異常完成出口的區別在於:通過異常完成出口退出的不會給他的上層調用者產生任何的返回值。

一些附加信息

棧幀中還允許攜帶與 Java 虛擬機實現相關的一些附加信息。例如:對程序調試提供支持的信息。

棧的相關面試題

  • 舉例棧溢出的情況?(StackOverflowError)
    • 通過 -Xss 設置棧的大小
  • 調整棧大小,就能保證不出現溢出么?
    • 不能保證不溢出
  • 分配的棧內存越大越好么?
    • 不是,一定時間內降低了 OOM 概率,但是會擠占其它的線程空間,因為整個空間是有限的。
  • 垃圾回收是否涉及到虛擬機棧?
    • 不會
  • 方法中定義的局部變量是否線程安全?
    • 具體問題具體分析
/**
 * 面試題
 * 方法中定義局部變量是否線程安全?具體情況具體分析
 * 何為線程安全?
 *    如果只有一個線程才可以操作此數據,則必是線程安全的
 *    如果有多個線程操作,則此數據是共享數據,如果不考慮共享機制,則為線程不安全
 *    
 * @author: Nemo
 */
public class StringBuilderTest {

    // s1的聲明方式是線程安全的
    public static void method01() {
        // 線程內部創建的,屬於局部變量
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
    }

    // 這個也是線程不安全的,因為有返回值,有可能被其它的程序所調用
    public static StringBuilder method04() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("a");
        stringBuilder.append("b");
        return stringBuilder;
    }

    // stringBuilder 是線程不安全的,操作的是共享數據
    public static void method02(StringBuilder stringBuilder) {
        stringBuilder.append("a");
        stringBuilder.append("b");
    }


    /**
     * 同時並發的執行,會出現線程不安全的問題
     */
    public static void method03() {
        StringBuilder stringBuilder = new StringBuilder();
        new Thread(() -> {
            stringBuilder.append("a");
            stringBuilder.append("b");
        }, "t1").start();

        method02(stringBuilder);
    }

    // StringBuilder是線程安全的,但是String也可能線程不安全的
    public static String method05() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("a");
        stringBuilder.append("b");
        return stringBuilder.toString();
    }
}

總結一句話就是:如果對象是在內部產生,並在內部消亡,沒有返回到外部,那么它就是線程安全的,反之則是線程不安全的。

運行時數據區,是否存在 OOM Error 和 GC?

運行時數據區 是否存在 OOM Error 是否存在 GC
程序計數器
虛擬機棧
本地方法棧
方法區 是(OOM)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM