JVM詳解(五)虛擬機棧
在這裏感謝尚硅谷JVM(宋紅康),在此記錄一下自己詳細對學習筆記,希望對你有所幫助。
05虛擬機棧
由於跨平臺性的設計,Java的指令都是根據棧來設計的。不同平臺CPU架
構不同,所以不能設計爲基於寄存器的。
優點是跨平臺,指令集小,編譯器容易實現,缺點是性能下降,實現同樣
的功能需要更多的指令。
有不少Java開發人員一*提到Java內存結構,就會非常粗粒度地將JVM中的內存區理解爲僅有Java堆(heap)和Java棧(stack)?爲什麼?
堆,來解決數據的存儲問題,主體的數據都在堆中放,當然也不是全部,對象呢主要是在堆中放的。那如果你要是方法內的一些局部變量的話,是放在棧中,當然這變量是基本數據類型。引用數據類型的話呢,在棧空間只是放這個對象的一個引用只要是對象,都是在堆空間。所以主體上數據都放在堆空間,棧空間它也是可以放一些數據的,局部變量的一些基本類型的數據或者引用的對象的一個地址,也是放在這個棧中。堆的大小可以設置,方法區因爲是本地物理內存,最大,堆第二。
Java虛批機桟是什麼?
Java虛似機棧(Java Virtual Machine Stack) ,早期也叫Java棧。
毎個線程在創建吋都會創建一個虛擬機棧,其內部保存一個個的棧幀(stack Frame) ,對應着一次次的Java方法凋用。
是線程私有的
生命週期
生命週期和線程一致。
作用
主管Java程序的運行,它保存方法的局部變量(8種基本數據類型、對象的引用地址)、部分結果,並參與方法的調用和返回。
- 局部變量 VS 成員變量(或屬性)
- 基本數據變量 VS 引用類型變量(類、數組、接口)
棧頂頂方法叫做當前方法
棧的特點(優點)
●棧是一種快速有效的分配存儲方式,訪問速度僅次於程序計數器。
●JVM直接對Java棧的操作只有兩個:
➢每個方法執行,伴隨着進棧(入棧、壓棧)
➢執行結束後的出棧工作
●對於棧來說不存在垃圾回收問題
棧存在OOM,不存在GC (因爲只有進棧出棧的操作)
程序計數器不存在GC,也不存在OOM,因爲它只存一個地址
面試中:開發中遇到的異常有哪些?
棧中可能出現的異常
●Java虛擬機規範允許Java棧的大小是動態的或者是固定不變的。
➢如果採用固定大小的Java虛擬機棧,那每一個線程的Java虛擬機棧容量可以在線程創建的時候獨立選定。如果線程請求分配的棧容量超過Java虛擬機棧允許的最大容量,Java 虛擬機將會拋出一個StackOverflowError異常。
➢如果Java虛擬機棧可以動態擴展,並且在嘗試擴展的時候無法申請到足夠的存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那Java虛擬機將會拋出一個OutofMemoryError異常。
現改變棧大小
不同操作系統會有所不同
棧的存儲單位
棧中存儲什麼?
●每個線程都有自己的棧棧中的數據都是以棧幀(Stack Frame)的格式存在。
● 在這個線程上正在執行的每個方法都各自對應一個棧幀(Stack Frame) 。
●棧幀是一個內存區塊,是一個數據集,維繫着方法執行過程中的各種數據信息。
複習:
- OOP的基本概念:類、對象
- 類中基本結構:field(屬性、字段、域)、method
●JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循“先進後出”/“後進先出”原則。
●在一條活動線程中,一個時間點上,只會有一個活動的棧幀。即只有當前正
在執行的方法的棧幀(棧項棧幀)是有效的,這個棧幀被稱爲當前棧幀(Current Frame) ,與當前棧幀相對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)
●執行引擎運行的所有字節碼指令只針對當前棧幀進行操作。
●如果在該方法中調用了其他方法,對應的新的棧幀會被創建出來,放在棧的
頂端,成爲新的當前幀。
debug來看一下
●不同線程 中所包含的棧幀是不允許存在相互引用的,即不可能在一個棧幀之,中引用另外一個線程的棧幀。
●如果當前方法調用了其他方法,方法返回之際,當前棧幀會傳回此方法的執行果
給前一個棧幀,接着,虛擬機會丟棄當前棧幀,使得前一個棧幀重新成爲當前幀。
●Java方法有兩種返回函數的方式,一種是正常的函數返回,使用return指令;另外一種是拋出異常(沒有處理的異常)。不管使用哪種方式,都會導致棧幀被彈出。
例3拋2->1
由上可知method1跟main都是異常出錯。沒有進行捕獲
- method1處捕獲
2.main處捕獲
此時main是以正常方式結束的,method1不是
再來看一下返回值
public double method3(){
System.out.println("method3開始執行");
double j = 20.0;
System.out.println("method3即將結束");
return j;
}
public int method2(){
System.out.println("method2開始執行");
int i = 10;
int m = (int)method3();
System.out.println("method2即將結束");
return i + m;
}
public void method1(){
System.out.println("method1()開始執行");
method2();
// System.out.println(10/0);
System.out.println("method1()執行結束");
}
棧幀的內部結構
毎個棧幀中存儲着:
- 局部變量表(Local variables)
- 操作數棧(operand stack) (或表達式棧)
- 動態鏈接(Dynamic Linking) ( 或指向運行吋常量池的方法引用)
- 方法返回地址(Return Address) (或方法正常退出或者異常退出的定義)一些附加信息
棧幀的大小取決於內部結構的大小
多個線程時
局部變量表
- 局部變量表也被稱之爲局部變量數組或本地變量表
- 定義爲一個數字數組,主要用於存儲方法參數和定義在方法體內的局部變量,這些數據類型包括各類基本數據類型、對象引用(reference) ,以returnAddress類型。
- 由於局部變量表是建立在線程的棧上,是線程的私有數據,因此不存在數據安全問題
- 局部變量表所需的容量大小是在編譯期確定下來的,並保存在方法的Code屬性的maximum local variables數據項中。在方法運行期間是不會改變局部變量表的大小的。
對大小在編譯期確定下來的做一個驗證
Javap 相當於對字節碼文件的一個解析
字節碼從上往下的一個格式
現在解釋一下結構
Bytecode是字節碼,與上圖相對應
Exception table異常表,沒有就是空的
Maximum local variables:最大局部變量表長度
所以是編譯期確定的,在運行時不會改變
Code length: 方法執行的一個長度(指的是字節碼)
-
**方法嵌套調用的次數由棧的大小決定。**一般來說,棧越大,方法嵌套調用次
**數越多。**對一個函數而言,它的參數和局部變量越多,使得局部變量表膨脹,
它的棧幀就越大,以滿足方法調用所需傳遞的信息增大的需求。進而函數調
用就會佔用更多的棧空間,導致其嵌套調用次數就會減少。 -
局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機通過
使用局部變量表完成參數值到參數變量列表的傳遞過程**。當方法調用結束後,**
隨着方法棧幀的銷燬,局部變量表也會隨之銷燬。 -
參數值的存放總是在局部變量數組的index0開始,到數組長度-1的索引結束。
局部變量表,最基本的存儲單元是Slot (變量槽) -
局部變量表中存放編譯期可知的各種基本數據類型(8種),引用類(reference),returnAddress類型。
-
在局部變量表裏,32位以內的類型只佔用一個slot (包括returnAddress型),64位的類型(long和dquble)佔用兩個slot。
➢byte 、short 、char在存儲前被轉換爲int,boolean 也被轉換爲int,0表示false,非0表示true。
➢long和double則佔據兩個Slot(變量槽)。
現對jclasslib做一個說明
Name:名稱
Descriptor:方法的參數。String類型的一維數組,V是void類型
Access flags:訪問標識
行號表
Line Number java 代碼的行號
Start PC 字節碼指令的行號
以Line Number=15爲例
局部變量表
按照聲明的先後順序,依次生成局部變量表的索引位置
Start PC,表明了該變量作用域的一個起始位置
聲明之後,作用域從下一行開始
Length是長度(偏移)。會發現0+16=8+8=11+5=16都是爲
代碼長度
即作用域爲括號內,括號裏面長度爲16,出了作用域就失效。
如hello爲(2,5)就是ello,後面就沒了
關於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。
沒有調用this局部變量表中也有
靜態代碼塊中不允許用this
因爲this不存在與當前局部變量表中
舉例:靜態變量與局部變量的對比
變量的分類:
按照數據類型分:1、基本數據類型 2、引用數據類型
按照在類中聲明的位置分:
1、成員變量:在使用前,都經歷過默認初始化賦值
1.1類變量:linking的prepare階段:給類變量默認賦值 —> initial階段:給類變量顯式賦值即靜態代碼塊賦值
1.2實例變量:隨着對象的創建、會在空間中分配實例變量空間,並進行默認賦值
2、局部變量:在使用前,必須要進行顯式賦值,否則編譯不通過
補充說明
●在棧幀中,與性能調優關係最爲密切的部分就是前面提到的局部變量表。
在方法執行時,虛擬機使用局部變量表完成方法的傳遞。
●局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直
接或間接引用的對象都不會被回收。
棧是管運行的
局部變量表存儲的數據需要load加載,sotre等都會影響到堆中的gc。對gc影響較大的就是棧中的局部變量表,
操作數棧
●操作數棧,主要用於保存計算過程的中間結果,同時作爲計算過程中變量
臨時的存儲空間。
●操作數棧就是JVM執行引擎的一一個工作區,當一個方法剛開始執行的時候,
一個新的棧幀也會隨之被創建出來,這個方法的操作數棧是空的。
●每一個操作數棧都會擁有一個明確的棧深度用於存儲數值,其所需的最大
深度在編譯期就定義好了,保存在方法的Code屬性中,爲max_ stack的
值。
●棧中的任何一個元素都是可以任意的Java數據類型。
➢32bit的類型佔用一個棧單位深度
➢64bit的類型佔用兩個棧單位深度
●操作數棧並非採用訪問索引的方式來進行數據訪問的,而是隻能通過標準
的入棧(push) 和出棧(pop)操作來完成一次數據訪 問。
locals:局部變量表深度
stack: 操作數棧的深度
●如果被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操作數棧中,並更新PC寄存器中下一條需要執行的字節碼指令。
●操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類加載過程中的類檢驗階段的數據流分析階段要再次驗證。
●另外,我們說Java虛擬機的解釋引擎是基於棧的執行引擎,其中的棧指的
就是操作數棧。
代碼追蹤
一、
二、
三、
四、
由上可知操作數棧深度爲2
8在byte範圍內,一個字節可以表示
800即爲short
超過int,編譯就會報錯
load_0 就是把一個對象引用加載到局部變量表,而剛好這個對象是this因爲getSum()方法就是this對象的
動態鏈接
棧幀內部結構
幀數據區:一些附加信息,動態鏈接,方法返回地址
動態鏈接(或指向運行時常量池的方法引用)
●每一個棧幀內部都包含一個指向運行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是爲了支持當前方法的代碼能夠實現動態鏈接(Dynamic Linking) 。比如: invokedynamic指 令
●在Java源文件被編譯到字節碼文件中時,所有的變量和方法引用都作爲符號引用(symbolic Reference) 保存在class文件的常量池裏。比如:描述一個方法調用了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那麼動態鏈接的作用就是爲了將這些符號引用轉換爲調用方法的直接引用。
大部分字節碼指令在執行時都會進行常量池的訪問
Constant pool常量池在運行時期存到方法區(運行時常量池)
通過引用去調用,幾份一起調用對應地址都一樣,不然浪費
比如多態,編寫的父類,運行的子類
爲什麼需要常量池呢?
常量池的作用,就是爲了提供一些符號和常量,便於指令的識別。
方法的調用
在JVM中,將符號引用轉換爲調用方法的直接引用與方法的綁定機制相關(在編譯期間確定還是運行期間確定)。
●靜態鏈接:
當一個字節碼文件被裝載進JVM內部時,如果被調用的目標方法在編譯期可知,
且運行期保持不變時。這種情況下將調用方法的符號引用轉換爲直接引用的
過程稱之爲靜態鏈接。
●動態鏈接:
如果被調用的方法在編譯期無法被確定下來,也就是說,只能夠在程序運行
期將調用方法的符號引用轉換爲直接引用,由於這種引用轉換過程具備動態
性,因此也就被稱之爲動態鏈接。
對應的方法的綁定機制爲:早期綁定(Early Binding) 和晚期綁定.
(Late Binding) 。綁定是一個字段、方法或者類在符號引用被替換爲直接引用的過程,這僅僅發生一次。
●早期綁定:
早期綁定就是指被調用的目標方法如果在編譯期可知,且運行期保持不變時,即可將這個方法與所屬的類型進行綁定,這樣一來,由於明確了被調用的目。標方法究竟是哪–個,因此也就可以使用靜態鏈接的方式將符號引用轉換爲直接引用。
●晚期綁定:
如果被調用的方法在編譯期無法被確定下來,只能夠在程序運行期根據實際
的類型綁定相關的方法,這種綁定方式也就被稱之爲晚期綁定。
Invoke virtual虛調用指令
Invoke special早期綁定
如
隨着高級語言的橫空出世,I類似於Java-樣的基於面向對象的編程語言如今
越來越多,儘管這類編程語言在語法風格.上存在一定的差別,但是它們彼此
之間始終保持着一個共性,那就是都支持封裝、繼承和多態等面向對象特性, .
既然這一類的編程語言具備多態特性,那麼自然也就具備早期綁定和晚期綁
定兩種綁定方式。
Java中任何一個普通的方法其實都具備虛函數的特徵,它們相當於C++語言
中的虛函數(C+ +中則需要使用關鍵字virtual來顯式定義)。如果在Java
程序中不希望某個方法擁有虛函數的特徵時,則可以使用關鍵字final來標
記這個方法。
final就是不能被重寫了,在編譯期就確定了。
非虛方法:
●如果方法在編譯期就確定了具體的調用版本,這個版本在運行時是不可變的。
這樣的方法稱爲非虛方法。
●(靜態方法、私有方法、final方法)【不能重寫】、(實例構造器 、父類方法都是非虛方法)【例如通過this.調用,都是一個確定的方法】。
●其他方法稱爲虛方法。
子類對象的多態性的使用前提:1.類的繼承關係。2.方法的重寫
虛擬機中提供了以下幾條方法調用指令:
普通調用指令:(1、2非虛方法)
-
invokestatic: 調用靜態方法,解析階段確定唯一方法版本
-
invokespecial: 調用方法、私有及父類方法,解析階段確定唯一方法版本
-
invokevirtual: 調用所有虛方法
-
invokeinterface: 調用接口方法
動態調用指令:
- invokedynamic: 動態解析出需要調用的方法,然後執行(JDK7新增)
前四條指令固化在虛擬機內部,方法的調用執行不可人爲干預,而invokedynamic指令則支持由用戶確定方法版本。其中invokestatic指令和invokespecial指令調用的方法稱爲非虛方法,其餘的(final修飾的除外)稱爲虛方法。
class Father{
public Father(){
System.out.println("father的構造器");
}
public static void showStatic(String str){
System.out.println("father "+ str);
}
public final void showFinal(){
System.out.println("father show final");
}
public void showCommon(){
System.out.println("father 普通方法");
}
}
public class Son extends Father{
public Son(){
//invokespecial
super();
}
public Son(int age){
//invokespecial
this();
}
//不是重寫的父類的靜態方法,因爲靜態方法不能被重寫
public static void showStatic(String str){
System.out.println("son "+ str);
}
public void showPrivate(String str){
System.out.println("son private "+str);
}
public void show(){
//invokestatic
showStatic("p3wj.top");
//invokestatic
Father.showStatic("good!");
//invokespecial
showPrivate("hello!");
//invokespecial
super.showCommon();
//虛方法:編譯期間無法確定下來的
//invokevirtual,雖然是這個但是被final修飾他不是一個虛方法
showFinal();
//invokespecial,加上super,顯示地表示是一個父類地方法
super.showFinal();
//invokevirtural,因爲有可能該子類會重寫這方法,如果加上super就是invokespecial
showCommon();
info();
MethodInterface in = null;
//invokeinterface
in.methodA();
}
public void info(){
}
public void display(Father f){
f.showCommon();
}
public static void main(String[] args) {
Son so = new Son();
so.show();
}
}
interface MethodInterface{
void methodA();
}
方法的調用:關於invokedynamic指令
●JVM字節碼指令集一直比較穩定,一直到Java7中才增加了一個invokedynamic指令,這是Java爲了實現「動態類型語言」支持而做的一種改進。
●但是在Java7中並沒有提供直接生成invokedynamic指令的方法,需要藉助ASM這種底層字節碼工具來產生invokedynamic指令。直到Java8的Lambda表達式的出現,invokedynamic指 令的生成,在Java中才有 了直接的生成方式。
●Java7中增加的動態語言類型支持的本質是對Java虛擬機規範的修改,而不是對Java語言規則的修改,這一塊相對來講比較複雜,增加了虛擬機中的方法調用,最直接的受益者就是運行在Java平臺的動態語言的編譯器。
動態類型語言和靜態類型語言。
動態類型語言和靜態類型語言兩者的區別就在於對類型的檢查是在編譯期還
是在運行期,滿足前者就是靜態類型語言,反之是動態類型語言。
說的再直白一-點就是,靜態類型語言是判斷變量自身的類型信息;動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值纔有類型信息,這是動態語言的一個重要特徵。
Java: String info = "atguigu"; / /info = atguigu;
JS: varname = "shkstart"; var name = 10 ;
Python:info = 130. 5;
方法的調用:方法重寫的本質
Java語言中方法重寫的本質:
1.找到操作數棧頂的第一個元素所執行的對象的實際類型,記作C。
2.如果在類型C中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法的直接引用,查找過程結束;如果不通過,則返回java. lang. IllegalAccessError異常。
3.否則,按照繼承關係從下往上依次對C的各個父類進行第2步的搜索和驗證過程。
4.如果始終沒有找到合適的方法,則拋出java. lang . AbstractMethodError異常。
IllegalAccessError介紹:
程序試圖訪問或修改一個屬性或調用一個方法,這個屬性或方法,你沒有權限訪問。一般的,這個會引起編譯器異常。這個錯誤如果發生在運行時,就說明一個類發生了不兼容的改變。
方法的調用:虛方法表
●在面向對象的編程中,會很頻繁的使用到動態分派,如果在每次動態分派的過程中都要重新在類的方法元數據中搜索合適的目標的話就可能影響到執行效率。因此,爲了提高性能,JVM採用在類的方法區建立一個虛方法表
(virtual method table) (非虛方法不會出現在表中)來實現。使用索引表來代替查找。
●每個類中都有一一個虛方法表,表中存放着各個方法的實際入口。
●那麼虛方法表什麼時候被創建?
虛方法表會在類加載的鏈接階段被創建並開始初始化,類的變量初始值準備完成之後,JVM會把該類的方法表也初始化完畢。
方法調用:虛方法表
package top.p3wj.java3;
/**
* @author Aaron
* @description
* @date 2020/5/8 5:26 PM
*/
interface Friendly {
void sayHello();
void sayGoodbye();
}
public class VirtualMethodTable {
}
class Dog {
public void sayHello(){
}
@Override
public String toString(){
return "Dog";
}
}
class Cat implements Friendly {
public void eat(){
}
@Override
public void sayHello() {
}
@Override
public void sayGoodbye() {
}
@Override
protected void finalize(){
}
@Override
public String toString(){
return "Cat";
}
}
class CockerSpaniel extends Dog implements Friendly {
@Override
public void sayHello(){
super.sayHello();
}
@Override
public void sayGoodbye(){
}
}
方法返回地址(主要針對於正常退出的情況)
●存放調用該方法的pc寄存器的值。
●一個方法的結束,有兩種方式:
➢正常執行完成
➢出現未處理的異常,非正常退出
●無論通過哪種方式退出,在方法退出後都返回到該方法被調用的位置。方法正常退出時,**調用者的pc計數器的值作爲返回地址,即調用該方法的指令的下一條指令的地址。**而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。
交給執行引擎,去執行後續的操作
區別:
本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的局部變量表、操作數棧、將返回值壓入調用者棧幀的操作數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。
正常完成出口和異常完成出口的區別在於:通過異常完成出口退出的不會給他的上層調用者產生任何的返回值。
當一個方法開始執行後,只有兩種方式可以退出這個方法:
1、執行引擎遇到任意-一個方法返回的字節碼指令(return),會有返回值傳遞給上層的方法調用者,簡稱正常完成出口;
➢一個方法在正常調用完成之後究竟需要使用哪一個返回指令還需要根據方法返回值的實際數據類型而定。
➢在字節碼指令中,返回指令包含ireturn (當返回值是boolean、byte、 char.
short和int類型時使用)、lreturn、 freturn、 dreturn以及areturn,另外還有一個return指令供聲明爲void的方法、實例初始化方法、類和接口的初始化方法使用。
其他的就不截圖了
2、在方法執行的過程中遇到了異常(Exception) ,並且這個異常沒有在
方法內進行處理,也就是隻要在本方法的異常表中沒有搜索到匹配的異常處
理器,就會導致方法退出。簡稱異常完成出口。
方法執行過程中拋出異常時的異常處理,存儲在一個異常處理表,方便在發
生異常的時候找到處理異常的代碼。
Exception | Table: | ||
---|---|---|---|
from | to | target | type |
4 | 16 | 19 | any |
19 | 21 | 19 | any |
以上數字爲字節碼指令地址
如果在4-16行出現異常,則用19行處理,針對任何類型
goto,直接到16.即要是沒處理就直接return了,處理就按照11行
一些附加信息
有的文章、論壇會有這個部分
棧幀中還允許|攜帶與Java虛擬機實現相關的一些附加信息。例如,對程序調試提供支持的信息。
棧的相關面試題
- 舉例棧溢出的情況?(StackOverflowError)
- 通過-Xss設置棧的大小;OOM(整個內存空間不足了)
- 調整棧大小,就能保證不出現溢出嗎?不能
- 分配的棧內存越大越好嗎?
- 垃圾回收是否會涉及到虛擬機棧?不會
- 方法中定義的局部變量是否線程安全?具體問題具體分析
package top.p3wj.java3;
/**
* @author Aaron
* @description 方法中定義局部變量石佛線程安全?具體情況具體分析
* 如果只有一個線程纔可以操作此數據,則必是線程安全的
* 如果有多個線程操作此數據,則此數據是共享數據,如果不考慮同步機制的話,會存在線程安全問題
* @date 2020/5/10 3:01 PM
*/
public class StringBuilderTest {
int num = 10;
//s1聲明方式是安全的,保證了被單線程所使用
public static void method1() {
//StringBuilder線程不安全
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
//sBuilder的操作過程,是線程不安全的。可能被多個線程所調用
public static void method2(StringBuilder sBuilder) {
sBuilder.append("a");
sBuilder.append("b");
}
//s1的操作:線程不安全的。返回出去可能能被其他的調用。只看該方法的話是安全的
public static StringBuilder method3() {
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1;
}
//s2的操作,是線程安全的。返回後s1小消亡了
public static String method4() {
StringBuilder s2 = new StringBuilder();
s2.append("a");
s2.append("b");
return s2.toString();
}
public static void main(String[] args) {
StringBuilder s = new StringBuilder();
new Thread(() -> {
s.append("a");
s.append("b");
}).start();
method2(s);
}
}