JVM、記憶體與垃圾回收
Java 原始檔 (. java):包含高階的 Java 程式語言程式碼,由程式設計師撰寫。這是人類可讀的原始碼
Java -- javac --> class 執行 --> JIT 同時編譯 -->機器碼 --> 硬體執行
位元組碼檔案 (.class):由 Java 編譯器將 Java 原始檔編譯產生的二進位檔案,其中包含了 Java 虛擬機器可以執行的位元組碼。位元組碼是一種由高階語言(例如 Java)原始碼編譯產生的、介於原始碼和機器碼之間的中間碼形式。
JVM 的整體結構
Java 程式碼執行流程
JVM 架構模型
java 編輯器輸入的指令流基於兩種架構
堆疊/暫存器的指令集架構
Java 編譯器輸入的指令流是指在編譯 Java 原始碼時,Java 編譯器產生的位元組碼 指令序列,用於描述原始碼的 執行邏輯
java 編譯器輸入的 指令流 基於 堆疊結構 轉化為位元組碼檔案
為何不更換暫存器架構
JVM 的生命週期
JVM 發展歷程 (了解)
JVM 發展歷程 (了解)
類別載入子系統
javap -v xxx.class
解析:反組譯(將位元組碼檔案內容轉換為人類可讀的形式)
idea中叫反編譯,class檔案和java檔案看起來一樣
類別載入器與類別的載入過程
ClassLoader 對 class 檔案進行載入 (不管能否執行, 看Execution Engine),需要載入類別的資訊存放於方法區
類別載入器與類別的載入過程
載入 Loading
透過類別的 全限定名 取得該類別的 binary byte stream (class loading is lazy: loaded on demand [active use]), which converts what the byte stream represents as static storage structure into runtime data of the method area, and then generates in memory the class object
static method belongs to the class itself, not to instances of the class. They can be used once the class has finished loading.
JVM memory and garbage collection > ^ne4cv6
linking linking (verification -->preparation -->resolution)
verification such as ca fe ba be
the virtual method table is created at this stage
initialization Initialization
- object creation
- executes the class
constructor method(constructor) andassignment of static variables
is responsible for completing the initialization of the object, including allocating space for instance variables and performing default assignment. It can also explicitly assign values to instance variables.
<clinit>method
<clinit>method
the method content is as follows
if there is no merging of class variable (static variable) assignment actions and static code block statements, there will be no<clinit>method, such as
in other words, only with static variablesassignmentwill there be<clinit>method, and if there is no assignment there is also none
the virtual machine guarantees that a class<clinit>method is synchronized and locked during multithreading
class loader classification
bootstrap class loader: this class loader is implemented in C and C++ language and is nested inside the JVM
extension class extloader: responsible for loading Java extension libraries, which are usually optional functional extensions that are not part of the Java core but are commonly used.
system class appclassLoader loader, the default loader for programs: generally speaking, the classes of a Java application are loaded by it
ext and app do not have an inheritance relationship, but ext is app the upper layer of, only having a reference
ext and app both and classLoader(abstract class) have an inheritance relationship
class loader classification
載入器
public static void main(String[] args) {
//取得系統類別載入器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//取得其上層:擴充類別載入器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//取得其上層:取得不到啟動類別載入器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//對於使用者自訂類別來說:預設使用系統類別載入器進行載入
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classLoader.getParent());//sun.misc.Launcher$ExtClassLoader@1b6d3586
//String類別使用啟動類別載入器進行載入。---> Java的核心類別庫都是使用啟動類別載入器進行載入的
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
驗證對應路徑的類別是對應載入器載入的
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//取得BootstrapClassLoader能夠載入的api的路徑
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//從上面的路徑中隨意選擇一個類別,來看看它的類別載入器是什麼:啟動類別載入器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);//null
System.out.println("***********扩展类加载器*************");
//System.getProperty():取得指定的Java系統屬性
//"java.ext.dirs":這個屬性鍵表示Java擴充目錄屬性
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
//從上面的路徑中隨意選擇一個類別,來看看它的類別載入器是什麼:擴充類別載入器
ClassLoader classLoader1 = SunEC.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1b6d3586
}
取得 ClassLoader 抽象類別
ClassLoader抽象類別
除了啟動類別載入器,所有類別載入器都繼承於 ClassLoader
java 中可以分為兩類載入器,一類啟動類別載入器,一類繼承 ClassLoader 類別的
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1); //sun.misc.Launcher$AppClassLoader@18b4aac2
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader();//sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader classLoader3 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
System.out.println(classLoader3);
雙親委派機制
類別是按需載入的,同時是雙親委派機制,類別會委託給父類別載入器,不斷向上委託,頂層父類別判斷自己能否載入,不能則委託給子類別
證明
反向委派
介面是核心 API,使用啟動類別載入器載入,但具體實作類別是第三方 Jar 套件,就會透過 執行緒情境載入器 ,反向委派 到 系統類別載入器
優勢
- 避免了類別的重複載入,每個載入器都有自己的載入範疇
- 保護程式安全,防止核心 API 被篡改,假如傳過來的自訂
String不會被載入,還是啟動類別載入器載入核心API
其他
一個類別物件必須來源於同一個 class 檔案和同一個 classLoader 才相等
使用者類別載入器載入的類別會將這個類別載入器的一個參考作為型別資訊的一部分保存在方法區
Java 程式對類的主動使用會導致初始化,被動使用不會導致類的初始化 ^ne4cv6
執行時資料區
一個 JVM 只有一個 Runtime 實例,也就是執行時環境
執行緒
在 Hotspot JVM 裡,每個執行緒都與作業系統的原生執行緒直接對應。當一個 Java 執行緒準備好執行以後,此時一個作業系統的原生執行緒也同時建立。Java 執行緒執行終止後,原生執行緒也會回收。
執行緒
程式計數器 (PC 暫存器)
為了能夠準確地記錄各個執行緒正在執行的當前位元組碼指令位址,
每一個執行緒都分配一個 PC 暫存器
儲存執行指向執行下一條指令的位址,位址由執行引擎讀取,可以理解成游標或者集合的迭代器,是程式控制流的指示器(分支,跳轉,迴圈,異常)
不存在 error,速度最快 既沒有 GC(不需要垃圾回收,不斷被替換為下一個位址) 唯一沒有 OOM(沒有溢出)
為了準確記錄各個執行緒當前執行的位元組碼指令位址,各個執行緒這樣不會互相干擾
虛擬機堆疊
一個執行緒對應一個 Java 虛擬機堆疊,有 Error,不存在 GC,存在 OOM,速度僅次於程式計數器
堆疊是執行時單位,堆積是儲存的單位
- 堆疊中可能出現的異常
堆疊的大小可設定動態或固定
固定時,超過 Java 虛擬機堆疊允許的最大容量會拋出
StackOverflowError若動態拓展申請不到足夠記憶體會拋出OutOfMemoryError
堆疊的記憶體決定堆疊框架總大小,單個堆疊框架大小決定堆疊框架數量
堆疊的儲存單位: 堆疊框架
每個執行緒都有自己的堆疊,堆疊中的資料都是以 堆疊框架(Stack Frame) 的格式存在,執行緒上的每個方法都各自對應一個 堆疊框架(Stack Frame) ,堆疊框架是一個記憶體區域,是一個資料集,維護方法執行過程中的各種資料資訊
以此類推
方法如果沒處理異常會拋給 main, 沒處理都會拋出異常的方式結束
- 棧幀內部結構
棧幀內部結構: 局部變數表
棧幀為一個 數字陣列(大小編譯期確定),也稱局部變數陣列或本地變數表,主要用於儲存 呼叫方法的參數 (方法執行時,虛擬機使用局部變數表完成方法的傳遞) 和定義在方法體內的 局部變數和值, 只在當前棧幀中有效
棧幀中與效能調校關係最密切的就是局部變數表
棧越大,方法巢狀呼叫次數越多
局部變數表中的變數是垃圾回收的根節點 (只要被局部變數表中直接或間接引用的物件都不會被回收)
info
局部變數表單位 slot
局部變數表
int i=1
杜宇基本資料型別儲存 i 和對應的值 1
對於參照型別局部變數表儲存位址值
局部變數表的最基本的單位是 Slot
byte 1,short 2,int 4,long 8,float 4 double 8,char 2 位元組,大小對應slot
參照型別32位 佔用一個 slot
關於 slot 的理解
堆疊訊框中的局部變數表中的槽位是可以重用的,如果一個局部變數過了其作用域,那麼在其作用域之後宣告的新的局部變數就很有可能會複用過期局部變數的槽位,從而達到節省資源的目的 (因為開闢的陣列不能變, 所以需要重複利用)(槽中的資料不再使用且需要被其他物件使用時,需要被回收,如果只是不再使用沒有被回收)
變數的分類:按照資料類型分:
① 基本資料類型
② 引用資料類型
按照在類中宣告的位置分:
① 成員變數:在使用前,都經歷過預設初始化賦值
類變數: linking的prepare階段:給類變數預設賦值 ---> initial階段:給類變數顯式賦值即靜態程式碼區塊賦值
實例變數:隨著物件的建立,會在堆空間中分配實例變數空間,並進行預設賦值
② 局部變數:在使用前,必須要進行顯式賦值的!因為局部變數不會被預設賦值,編譯不通過
堆疊訊框內部結構: 運算元堆疊
每一個獨立的堆疊訊框包含一個後進先出 (last-In-First-Out) 的運算元堆疊,也可以稱之為表達式堆疊 (Expression Stack)
運算元堆疊 (Operand Stack) 的作用就是輔助完成局部變數的資料的各種,主要用於保存計算的臨時結果,作為變數臨時儲存空間,所需最大深度在編譯期間就確定好了
呼叫方法的返回值會被壓入運算元堆疊,並更新程式計數器下一條指令的位址
JVM/上記憶體與垃圾回收 > 方法返回位址
運算元堆疊 並非採用訪問索引 的方式來進行資料訪問的,而是只能通過入棧 (push) 和出棧 (pop) 操作來完成一次資料訪問
程式碼追蹤
../../../Utility/Excalidraw/Drawing 2023-12-14 20.45.04.excalidraw
簡介
0 bipush 15 //bipush(push byte)將一個位元組的常量值推送到運算元堆疊上(bipushbyte-->int ;sipush是short-->int)
2 istore_1 //Integer STORE_ X表示將運算元堆疊上資料儲存到局部變數表的索引X的位置
3 bipush 8
5 istore_2
6 iload_1 // iload_X將區域變數表的資料 X 放到運算元堆疊
7 iload_2
8 iadd // 將運算元堆疊的資料相加
9 istore_3 //
10 return
- 區域變數表中,byte 會以 int 形式儲存
- 運算元堆疊中,bipush 指令壓入的是 byte 類型
為什麼位元組碼指令之間的編號似乎不連續
aload_0 的含義是將目前物件參考載入到運算元堆疊上
i++ 和 ++i 的位元組碼指令
對於左邊沒有賦值操作的 i++ 和 ++i 位元組碼指令一樣
對於左邊有賦值操作的
i++ 先load出來再incre by
++i 先incre by 再load出來
public void add(){
//第1類問題:
int i1 = 10;
i1++;
// 0 bipush 10
// 2 istore_1
// 3 iinc 1 by 1
int i2 = 10;
++i2;
// 6 bipush 10
// 8 istore_2
// 9 iinc 2 by 1
//第2類問題:
int i3 = 10;
int i4 = i3++;
// 12 bipush 10
// 14 istore_3
// 15 iload_3
// 16 iinc 3 by 1
// 19 istore 4
int i5 = 10;
int i6 = ++i5;
// 21 bipush 10
// 23 istore 5
// 25 iinc 5 by 1
// 28 iload 5
// 30 istore 6
//第3類問題:
int i7 = 10;
i7 = i7++;
// 32 bipush 10
// 34 istore 7
// 36 iload 7
// 38 iinc 7 by 1
// 41 istore 7
int i8 = 10;
i8 = ++i8;
// 43 bipush 10
// 45 istore 8
// 47 iinc 8 by 1
// 50 iload 8
// 52 istore 8
//第4類問題:
int i9 = 10;
int i10 = i9++ + ++i9;
// 54 bipush 10
// 56 istore 9
// 58 iload 9
// 60 iinc 9 by 1
// 63 iinc 9 by 1
// 66 iload 9
// 68 iadd
// 69 istore 10
// 71 return
}
堆疊頂端快取技術 (Top-of-Stack Cashing)
將堆疊頂端元素全部快取在實體 CPU 暫存器 中,以降低對記憶體讀寫次數,提升執行引擎的執行效率
動態連結 (或指向執行時常量池 的方法參考)
動態連結:將符號參考轉換為呼叫方法的直接參考
- 符號參考: 用符號來描述被參考目標,不直接指向記憶體位址。
- 直接參考: 直接指向記憶體位址或偏移量,可以被 JVM 直接使用。
每一個棧框內部都包含一個指向 執行時常量池中 該 棧框所屬方法的參考 包含這個參考的目的就是為了支援目前方法的程式碼能夠實現 動態連結 (Dynamic Linking), 動態連結的作用就是為了將這些符號參考轉換為呼叫方法的直接參考
例如 : invokedynamic 指令會攜帶一個符號參考, 到方法真正被呼叫時 JVM 會將這些符號參考轉換為呼叫方法的直接參考
也就是需要將符號參考轉換為呼叫方法的直接參考就是動態連結,就是 invokedynamic
有時動態連結和方法返回位址和一些附加資訊會被叫做幀資料區
方法參考:myClass.myMethod () 呼叫時,JVM 會使用執行時常量池中的方法符號參考來解析 myMethod 的實際記憶體位址
欄位參考:如果 MyClass 中有欄位存取,JVM 會使用執行時常量池中的欄位符號參考來解析欄位的實際記憶體位址
方法的呼叫
在 JVM 中,將符號參考轉換為呼叫方法的直接參考與方法的綁定機制有關
invokestatic : 呼叫靜態
invokespecial : 呼叫 <init> 方法,私有,父類,唯一方法
invokevirtual 呼叫虛擬方法
invokeinterface 呼叫介面
invokedynamic 動態解析呼叫方法
"虛擬方法" (Virtual Method) 指的是可以被子類別覆寫 (Override) 的實例方法。也就是說,當呼叫一個物件的方法時,具體執行哪個方法版本 (父類別還是子類別) 要在執行時才能確定。這就是 "虛擬" 的含義,因為它不是在編譯時就確定下來的。
- 特殊
呼叫自己類別獨有方法但為
public的也是invokevirtual
父類別中方法宣告為 final 的方法,不能被子类重写,可以被子类直接调用,直接调用会显示 invokevirtual,但此方法是非虚方法,也可以通过 super.,会显示为非虚方法
如果直接不通过 super. 调用父类的方法,编译期间不能确定子类是否重写父类方法,会显示为 invokevirtual
Java 是静态, lamda 的引用使 Java 在一定程度上具有动态
js, python 都是动态
调用一个对象的方法时,对象首先会压入操作数栈,然后根据字节码指令如 invokevirtual 找到這個物件的實際類型,然後找到常數池中和呼叫方法名稱一致的,進行存取權限校驗 (不通過 IllegalAccessError),通過後會按照繼承關係依次驗證,拋出 AbstactMethodError 說明找到最上層的介面發現是抽象方法,沒有被重寫
- 虛方法表
位元碼中包含了方法的符號引用,但並沒有包含方法的具體記憶體位址,設計出了虛方法表
虛方法表它包含了類別中所有的虛方法及其實際的 記憶體位址
虛方法表為了提高效能,避免程式碼中動態分配導致 JVM 執行時頻繁搜尋對應方法 ^j81ym3
記錄了對於某個類別中的方法是指向父類別還是自己類別的,不需要再進行尋找是屬於哪個類別的
如果沒有虛方法表,JVM 就必須在執行時進行複雜的判斷來確定具體呼叫哪個方法版本,這將大大降低程式的效能。
例如:
- 每次呼叫方法時,都需要遍歷整個繼承鏈,找到最合適的方法,這個過程效率很低。
- 當繼承層次很深時,效能損失會更嚴重。
方法返回位址
A 方法中呼叫完 B 方法
normal exit :
B no return value : execution engine accepts return address (that is A the address of), Execute the next stack frame based on this address
B There is a return value : The execution engine receives the return value of method B , and this return value is passed to method A,method A performs subsequent processing based on this return value
Exit with exception: Since exiting with an exception does not produce a normal return value, method A does not receive a value from method B valid return value, only the exception table stores where it should go when an exception occurs, target where to go (exception table return address, the return address is determined through the exception table), to resolve it
一些附加資訊
例如對程式除錯提供支援的資訊
本地方法堆疊
Java 虛擬機堆疊用於管理 Java 方法的呼叫 (執行緒私有) 本地方法堆疊用於管理本地方法的呼叫
堆積
一個行程由一個 JVM 實例只有一個堆積空間,在啟動時被建立,多個執行緒共用堆積空間 (共享意味著執行緒安全問題), 堆積也是 Java 記憶體管理的核心區域
JVM 啟動,空間大小已經確定 (實體記憶體空間雖然不連續,但邏輯視為連續空間)
所有的執行緒共享 Java 堆積,在這裡還可以劃分執行緒私有的緩衝區
Thread Local Allocation Buffer, TLAB
方法結束後,指向堆積中的位址消失了, 堆積空間中實例繼續存在 (為了避免方法多 GC 頻率過高,所以堆積空間滿後再執行,避免影響使用者執行緒),當堆積空間滿後,需要 GC 判斷,發現沒有參考就回收
設定堆積的大小與 OOM
- 設定堆積空間大小的參數
-Xms用來設定堆積空間(年輕代加老年代)的initial記憶體大小-Xisjvm的執行參數msismemory start
-Xmx 用來設定堆積空間(年輕代加老年代)的maximum記憶體大小
預設堆積空間的大小
初始記憶體大小:物理电脑内存大小 /64最大記憶體大小:物理电脑内存大小 /4手動設定:
-Xms600m-Xmx600m開發中建议將初始堆積記憶體和最大的堆積記憶體設定成相同的值 , 因為需要擴容和釋放,避免調整造成系統額外的壓力查看設定的參數 方式一:
jps/jstat-gc行程id方式二:-XX:+PrintGCDetails^ikhhqe
查看堆積大小程式碼
public static void main(String[] args) {
//回傳Java虛擬機中的堆積記憶體總量
// 執行時資料區是單例
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//回傳Java虛擬機試圖使用的最大堆積記憶體量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");
System.out.println("-Xmx : " + maxMemory + "M");
System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
年輕代與老年代
新生代 80%的物件都是朝生夕死
使用時發現不是 8 比 1, 需要手動指定
-Xms600m -Xmx600m
-XX:NewRatio : 設定新生代與老年代的比例。預設值是
-XX:SurvivorRatio :設定新生代中Eden區與Survivor區的比例。預設值是8 ,實際需要手動指定8
-XX:-UseAdaptiveSizePolicy :關閉自適應的記憶體分配策略(暫時用不到)自適應兩個sur區可能不一樣大
-Xmn:設定新生代的空間的大小(一般不設定)(優先高於比例)
圖解物件分配過程
由於記憶體分配演算法和垃圾回收密切相關
Minor GC,Major GC ,Full GC
GC
- 年輕代
GC(Minor GC)觸發機制
年輕代空間不足就會觸發 minor GC
交替使用 Survivor 區可以減少記憶體碎片。每次 GC 後,存活的物件被緊湊地複製到另一個 Survivor 區,避免了記憶體碎片的產生。
- 老年代
Major GC/Full GC觸發機制
老年代空間不足時,觸發 Major GC (根據收集器不同, 應該至少觸發一次 Minor Gc)
Major GC 一般比 Minor Gc 要慢 10 倍以上 , Major GC 後,記憶體還不足會呼叫 FULL GC ,不足就爆 OOM
Full GC觸發機制
OOM測試
老年代空間不足時會進行一次垃圾回收,回收後仍然不足才會爆OOM,爆OOM前必然會有Full GC
-Xms9m -Xmx9m -XX:+PrintGCDetails
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "atguigu.com";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Throwable t) {
t.printStackTrace();
System.out.println("遍历次数为:" + i);
}
}
[GC (Allocation Failure) [PSYoungGen: 2027K->504K(回收後新生代大小)(2560K)總空間大小] 2027K堆空間回收前(和前面一樣因為這時候堆空間老年代沒資料)->926K堆空間回收後,不一樣了,這時說明有資料放到老年代了(9728K)堆空間總大小, 0.0007621 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2210K->512K(2560K)] 2632K->1610K(9728K), 0.0006082 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2310K->496K(2560K)] 3408K->2346K(9728K), 0.0007793 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1238K->512K(2560K)] 7313K->6602K(9728K), 0.0005005 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 512K->496K(2560K)] 6602K->6602K(9728K)最後 新生代放到老年代,回收後沒變或者更大, 0.0003862 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 6106K->5014K(7168K)] 6602K->5014K(9728K), [Metaspace: 3439K->3439K(1056768K)], 0.0031555 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 5014K->5014K(8704K), 0.0003299 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 5014K->4912K(7168K)] 5014K->4912K(8704K), [Metaspace: 3439K->3439K(1056768K)], 0.0055557 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
遍歷次數為:16
Heap
PSYoungGen total 1536K, used 59K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 5% used [0x00000000ffd00000,0x00000000ffd0ef18,0x00000000ffe00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 7168K, used 4912K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 68% used [0x00000000ff600000,0x00000000ffacc2f8,0x00000000ffd00000)
Metaspace used 3473K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 371K, capacity 388K, committed 512K, reserved 1048576K
堆空間分代思想
分代就是為了 最佳化 GC 效能,對朝生夕死的物件及逆行回收,就能騰出很大的空間
記憶體分配策略
Minor GC 存活--> age=1 -->每一次 Minor GC -->age+1 --> >設定值(15) -->晉升老年代
設定值 -XX:MaxTenuringThreshold
特殊: 若 s 區中相同年齡的物件大小總和大於 S 區空間的一半,則大於等於該年齡的物件可直接進入老年代
大物件直接分配到老年代
為物件分配記憶體: TLAB
TLAB 是 JVM 中的一個記憶體分配機制。它為每個執行緒分配一個小的記憶體區域,用於物件的快速分配,每個執行緒都在自己的 TLAB 中分配物件。這片區域是私有的,其他執行緒不能存取,這樣可以減少執行緒之間的競爭,提高記憶體分配的效率。但一旦物件分配完成並且有其他執行緒持有該物件的參考,其他執行緒是可以存取和使用這個物件的。
TLAB 是 執行緒本地分配緩衝區, 每個執行緒都有一個自己的緩衝區,每個執行緒都有獨自的 TLAB 可以用來分配物件空間 (並不是所有物件都能分配到 TLAB[僅占 eden 1%], 但優先 TLAB),可以 減少執行緒之間的競爭,避免了部分執行緒安全問題
new 對應著在堆空間開闢新的空間,在 TLAB 開闢新的空間
堆空間的參數設定
測試堆空間常用的jvm參數:
-XX:+PrintFlagsInitial : 查看所有的參數的預設初始值
-XX:+PrintFlagsFinal :查看所有的參數的最終值(可能會存在修改,不再是初始值)
具體查看某個參數的指令:jps:查看當前執行中的行程
jinfo -flag SurvivorRatio 行程id
-Xms:初始堆空間記憶體 (預設為實體記憶體的1/64)
-Xmx:最大堆空間記憶體(預設為實體記憶體的1/4)
-Xmn:設定新生代的大小 (初始值及最大值)
-XX:NewRatio:配置新生代與老年代在堆結構的占比 (預設2)
-XX:SurvivorRatio:設定新生代中Eden和S0/S1空間的比例(S0/S1空間如果過小過大就失去了Minor GC就失去了意義)
-XX:MaxTenuringThreshold:設定新生代垃圾的最大年齡
-XX:+PrintGCDetails:輸出詳細的GC處理日誌
列印gc簡要資訊:① -XX:+PrintGC ② -verbose:gc
-XX:HandlePromotionFailure:是否設定空間分配擔保
堆是分配物件的唯一選擇嗎
逃逸分析(6 後預設開啟的): 如果發現一個物件並沒有逃逸出方法的話,那麼就可能被優化成棧上分配
物件在外部有調用的可能-->發生逃逸
如何快速的判斷是否發生了逃逸分析,大家就看new的物件實體是否有可能在方法外被調用
public EscapeAnalysis obj;
/*
方法返回EscapeAnalysis物件,發生逃逸
*/
public EscapeAnalysis getInstance(){
return obj == null? new EscapeAnalysis() : obj;
}
/*
為成員屬性賦值,發生逃逸
*/
public void setObj(){
this.obj = new EscapeAnalysis();
}
//思考:如果當前的obj引用聲明為static What about it? Escape still occurs.
/*
The scope of the object is valid only within the current method, so no escape occurs.
*/
public void useEscapeAnalysis(){
EscapeAnalysis e = new EscapeAnalysis();
}
/*
The value of the referenced member variable causes escape.
*/
public void useEscapeAnalysis1(){
EscapeAnalysis e = getInstance();
//getInstance().xxx()Escape also occurs.
}
- Conclusion
During development, if a local variable can be used, do not define it outside the method (do not declare it as a property).
Optimizations performed by the compiler using escape analysis
- Stack allocation
- Synchronization elision (lock elision)
- Scalar replacement of detached objects
Optimizations performed by the compiler using escape analysis
堆疊上分配測試
關閉逃逸分析:-DoEscapeAnalysis,未發生逃逸時,會有1千萬個User物件,且發生GC
開啟後,不會發生GC,只有10萬User物件
-Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
public class StackAllocation {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
// 查看執行時間
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
// 為了方便查看堆記憶體中物件個數, 執行緒sleep
try {
Thread.sleep(1000000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();
//關閉逃逸分析:-DoEscapeAnalysis,未發生逃逸時,會有1千萬個User物件,and occurs GC
//after enabling,does not occur GC,only 10 ten thousand User object
}
static class User {
}
}
純量替換
^ktyad4
方法區 ( 元空間 )
方法區(堆積外空間)
方法區包括型別資訊、常數、靜態變數、執行時常數池、字串常數、編譯後的程式碼快取
位元組碼檔案中的常數池會被載入到方法區中,稱為執行時常數池
指向物件型別資料的指標說明了是哪個類別 new 出來的,因此多型的資料型別要到執行時才會知道 ^9djjlm
方法區的理解
方法區可視為一塊獨立於 Java 堆積的記憶體空間,也可以固定大小或擴展,邏輯連續但實體不連續,是各個執行緒共用的空間
存在 OOM,可以不 GC,不去壓縮處理碎片問題
設定方法區大小與 OOM
-XX:MetaspaceSize
-XX:MaxMetaspaceSize (負一 沒有限制)
方法區的內部結構
位元碼檔案中的常數池會被載入到方法區中,稱為執行時常數池
常數池是位元碼檔案中的 執行時常數池是方法區中的
常數池
存放編譯期間產生的各種字面量和符號引用
數量值,字串值,類別引用,欄位引用,方法引用,(字串, final 常數,基本資料型別,其他)和符號引用,載入後放到執行時常數池中
執行時常數池 是當 class 檔案被載入完成後,JVM 會將 class 常數池 裡的內容轉移到 執行時常數池 裡,在 class 常數池的符號引用有一部分是會被轉變為直接引用,比如說類別的靜態方法、私有方法、實例建構方法、父類別方法。這是因為這些方法不能被改寫其他版本,所以能在載入的時候就可以將符號引用轉變為直接引用,而其他的一些方法是在這個方法被第一次呼叫的時候才會將符號引用轉變為直接引用的
方法區的演進細節
靜態變數放在哪?
第一個變數名: 堆空間,雖然規定了要放在方法區中,但 JDK 7 後虛擬機器自己選擇放到了堆空間中
第二個變數名: 是成員變數,放到了堆空間中
方法區的垃圾回收
方法區的垃圾回收通常為執行時常數池中廢棄的常數和不再使用的型別
總結
物件實例化,記憶體布局與存取定位
物件的實例化
物件建立執行步驟 ^2kosms
物件建立執行步驟
^2wz2dk
物件的記憶體布局
java
public class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客户";
}
public Customer(){
acct = new Account();
}
}
class Account{
}
public class CustomerTest {
public static void main(String[] args) {
Customer cust = new Customer();
}
}
物件的存取定位
大多數現代 JVM(例如 HotSpot JVM)預設使用直接指標(Direct Pointer)存取方式,因為它提供了更高的存取效率
- 使用
-XX:+UseCompressedOops(預設啟用)表示使用壓縮指標(這通常意味著直接指標)。 - 使用
-XX:-UseCompressedOops表示停用壓縮指標。
這些參數主要是針對指標壓縮的設定,但是不同的 JVM 實作可能會有不同的參數來選擇物件存取方式。如果需要變更具體的物件存取方式,可能需要查閱特定 JVM 實作的說明文件。
區域變數表 中儲存 控制代碼存取 和 直接存取 的值
區域變數表
儲存 : 控制代碼池中控制代碼物件的參考 儲存 : 物件的直接參考
控制代碼存取
控制代碼池: 儲存到物件實例和物件型別的指標
好處:物件移動時,到物件型別的資料的指標不需要修改(標記整理演算法,新生區 s 交換需要移動)
直接指標
效率比上面的高
直接記憶體
由於看不到本地記憶體,但也會出現 OutOfMemoryError,出現類似錯誤需要考慮是否可能出現這中錯誤
執行引擎
Java 核心組成部分之一,將位元組碼指令解釋/後端編譯成機器指令
虛擬機的執行引擎是由軟體自行實作的,因此能夠執行不被硬體直接支援的指令集格式
執行引擎的輸入輸出都是位元組串流,但輸入的是位元組碼二進位串流,處理過程是位元組碼解譯指令的等效過程,輸出的是執行結果
Java 程式碼編譯和執行過程
綠色代表解譯器執行過程
藍色代表編譯器執行過程
JIT 編譯器
本地方法庫與介面
為什麼需要 Native Method
StringTable
垃圾回收機制
Java 垃圾回收機制
垃圾回收相關演算法
垃圾標記階段
引用計數器演算法(不使用) / 可達性分析演算法 , 可區分已經死亡需要釋放的物件
標記階段:引用計數器演算法
- 引用計數器演算法 (不使用)
標記階段:可達性分析演算法
區域變數表中的變數是垃圾回收的根節點 (只要被區域變數表中直接或間接引用的物件都不會被回收)
透過可達性分析演算法 , 根據 根物件集合 (GC Roots) 為初始起點,搜尋被根物件集合所連接的目標物件是否可達,搜尋過的路徑被稱為 引用鏈,如果物件沒有被任何引用鏈相連接,則是不可達的, 就意味物件死亡 (也有可能之後復活)
JVM 與上記憶體與垃圾回收 > ^0oij7m
- 可以構成
根物件集合 (GC Roots)的- 虛擬機棧引用的物件
Object obj1 = new Object(); - 本地方法棧內本地方法引用的物件
public native void nativeMethod(); - 方法區中類別靜態屬性引用物件
public static Object obj2 = new Object(); - 方法區常數引用的物件
public static final String STR = "Hello"; - 執行時常數池中引用的物件 ``
- 虛擬機棧引用的物件
物件的 finalization 機制
GC 回收會自動呼叫 finalize 方法
object 類別中的 finalize 是空白,子類別重寫了這個方法,如 close(),永遠不要主動呼叫 finalize 方法
finalize 沒有被重寫則會直接被判定為不可觸及
如果被重寫了也只能呼叫一次,會將該類別插入到一個 F-Queue 佇列中,有虛擬機呼叫 finalize 方法, 呼叫後的物件沒有復活就會變成不可觸及狀態 (被標記為垃圾),永遠無法復活 (具體流程為圖四) 或物件沒有重寫 finalize 方法呼叫 finalize 也會被視為不可觸及
^0oij7m
GC 只會被呼叫一次,(使用 finalize 只能復活一次,第二次不會復活)
復活場景
手動重寫的 finalize 方法,與引用鏈上的任意一個物件建立了聯繫
public class CanReliveObj {
public static CanReliveObj obj;//類變數,屬於 GC Root
//此方法只能被呼叫一次
@Override
protected void finalize() throws Throwable {
System.out.println("调用当前类重写的finalize()方法");
obj = this;//當前待回收的物件在finalize()方法中與引用鏈上的一個物件obj建立了聯繫
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
// 物件第一次成功拯救自己
obj = null;
System.gc();//呼叫垃圾回收器,呼叫了重寫的finalize, obj = this,建立了聯繫
System.out.println("第1次 gc");
// 因為Finalizer執行緒優先級很低,暫停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面這段程式碼與上面的完全相同,但是這次自救卻失敗了
obj = null;
System.gc();
// 因為Finalizer執行緒優先級很低,暫停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- MAT 與
JProfiler的 GC Roots 溯源
清除階段:標記-清除演算法
空間滿後進入 STW ,進入標記和清除兩個階段
標記清除兩個環節
標記:標記可達物件(非垃圾物件) 清除:對堆記憶體從頭到尾進行走訪,沒有被標記的會被清除
效率不高,產生碎片
需要維護空閒列表
清除階段:複製演算法
複製演算法適合存活物件少,垃圾多時複製成本過高
將活著的記憶體空間分成了兩塊,每次只使用其中一塊(需要兩倍空間) 高效沒有碎片,但是浪費空間,GC 需要維護 region 之間物件引用關係
因為記憶體規則,建立物件使用指標碰撞就可以
刪除並不是真正的刪除
清除階段:標記-壓縮演算法(標記-清除-壓縮)
標記-壓縮 演算法:先標記清除,然後把物件 移動 到一端(移動過程會 STW),建立的新物件需要 指標碰撞
而 標記-清除 沒有移動,只是清除,需要維護空閒列表
指標碰撞:只需要修改指標的偏移量就可以將物件分配到一個空閒記憶體位置上
分代收集演算法
不同生命週期的物件採用不同的收集方式,以便提高收集效率
Young Generation: Copying algorithm. The efficiency of the copying algorithm only relates to object size, and the Young Generation has few surviving objects, so few objects need copying.
Old Generation: Mark Sweep, Mark Compact, or a combination of both.
Mark Phase consumption is number of surviving objects directly proportional directly proportional
Sweep Phase consumption is size of the managed region positively correlated positively correlated
Compact Phase consumption is surviving object data directly proportional directly proportional
Incremental collection algorithm, partition algorithm
Incremental collection algorithm
Because STW it causes long pauses, the garbage collection thread and the application thread can execute alternately, and each garbage collection reclaims a small region.
Partition algorithm
divides the entire space into several small blocks.
Concepts related to garbage collection
System.gc() understanding of
andRuntime.getRuntime().gc();has the same effect
trigger Full GC, only reminds the JVM to perform GC, and does not guarantee that GC executes.
System.runFinalization();//force call the referenced object finalize()method
GC case
//will not GC
public void localvarGC1() {
byte[] buffer = new byte[10 * 1024 * 1024];//10MB
System.gc();
}
//buffer be collected
public void localvarGC2() {
byte[] buffer = new byte[10 * 1024 * 1024];
buffer = null;
System.gc();
}
//buffer was not collected,because no object needs to occupy buffer the location of slot slot, does not need to be collected, but the bytecode will not display
![[assets/上内存与垃圾回收/image-20240106195523072.png]]
public void localvarGC3() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
System.gc();
}
//at this time buffer will be collected,value will occupy the previous buffer of the slot slot, which causes the original reference to no longer exist, and the original data is GC collected
public void localvarGC4() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
int value = 10;
System.gc();
}
public void localvarGC5() {
localvarGC1();
System.gc();
}
// after the method call is finished, then GC can be collected
public static void main(String[] args) {
LocalVarGC local = new LocalVarGC();
local.localvarGC5();
}
Memory overflow and memory leak
Memory overflow
OutOfMemoryError
There is not enough free space, andthe garbage collectorcannot provide more memory.
Memory leak (GC cannot collect)
A memory leak may lead to memory overflow.
Memory leak caused by design that cannot clean up or forgetting to close .close causing the memory leak
Stop The World
進行垃圾回收的過程中,會涉及物件的移動。為了保證物件參考更新的正確性,必須暫停所有的使用者執行緒
STW 過程中整個引用程式的執行緒都會被暫停 任何垃圾回收器都會有這個操作
垃圾回收的平行與並行
OopMap
OopMap 是 HotSpot 虛擬機中的一種資料結構,用於記錄物件的配置資訊。一旦類別載入完成,HotSpot 會根據物件的記憶體配置,計算並記錄在物件記憶體中的哪些偏移量上儲存著什麼類型的資料。同時,在即時編譯過程中,HotSpot 也會在特定位置產生 OopMap,用於記錄堆疊上和暫存器中的參考位置。
安全點與安全區域
安全點與安全區域是為了保證 GC 的效率效能
並不是隨時可以停下來進行 GC, 只有特定位置停下來進行 GC
根據長執行的特徵作為標準
而在安全區域 (程式碼片段)內則都可以進行 GC
如果堆積空間滿了,還沒到安全點,JVM 可能會觸發一些緊急的垃圾回收操作,以騰出一些空間。這通常被稱為「GC 主動觸發」或「GC 主動處理」(Garbage Collection induced by Application Threads)
再談參考
4 種參考強度依次減弱,都是可達的
我們為了對物件回收的優先順序進行區分,區分出哪些要優先回收,對應用類型進行區分
強參考 (主)
寧可拋出例外也不回收
軟參考 - 不足即回收
軟參考 - 不足即回收,回收後空間還不足就會爆 OOM 例外
當爆 OOM 時和軟參考無關
爆 OOM 時軟參考已經被回收
並不是 GC 後軟參考就不在,而是滿時前的 GC 才會清除,記憶體充足時不會回收
記憶體充足不會回收,不足才會回收
弱參考 - GC發現即回收
只要 GC 發現了就回收,無論記憶體充足與否
虛參考 - 物件回收追蹤
虛參考可以 追蹤 物件的回收時間,可以將一些資源釋放的操作放置在虛參考種執行和記錄, 只是為了在物件被垃圾回收時收到通知。
虛參考不能單獨使用,它必須與參考佇列(ReferenceQueue)聯合使用。
GC 具有不確定性,只是通知 JVM, 且 GC 只會被呼叫一次,(使用 finalize 只能復活一次)
設定虛參考關聯的唯一目的,就是在這個物件被收集器回收的時候收到一個系統通知或者後續新增進一步的處理。
java
public class PhantomReferenceExample {
public static void main(String[] args) throws InterruptedException {
// 建立一個參考佇列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 建立一個物件並將其包裝在虛參考中
Object obj = new Object();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);
// Clear strong references and allow the object to be garbage collected
obj = null;
// Force garbage collection to run
System.gc();
// Wait for a period of time to ensure the garbage collector runs
Thread.sleep(1000);
// Check whether the reference queue contains a phantom reference
if (referenceQueue.poll() != null) {
System.out.println("对象已被垃圾回收");
} else {
System.out.println("对象尚未被垃圾回收");
}
}
}
java
public class PhantomReferenceTest {
public static PhantomReferenceTest obj;//Declaration of the current class object
static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//Reference queue
public static class CheckRefQueue extends Thread {
@Override
public void run() {
while (true) {
if (phantomQueue != null) {
PhantomReference<PhantomReferenceTest> objt = null;
try {
objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (objt != null) {
System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了");
}
}
}
}
}
@Override
protected void finalize() throws Throwable { //finalize()The method can be called only once!
super.finalize();
System.out.println("调用当前类的finalize()方法");
obj = this;
}
public static void main(String[] args) {
Thread t = new CheckRefQueue();
t.setDaemon(true);//Set as a daemon thread: when the program has no non daemon threads, the daemon thread also finishes running.
t.start();
phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
obj = new PhantomReferenceTest();
//Constructed the PhantomReferenceTest phantom reference of the object and specified the reference queue
PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);
try {
//The object in the phantom reference cannot be obtained
System.out.println(phantomRef.get());
//Remove the strong reference
obj = null;
//Performed for the first time GC,Because the object can be resurrected,GC the object cannot be collected
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
System.out.println("第 2 次 gc");
obj = null;
System.gc(); //Once the obj object is collected, this phantom reference will be placed into the reference queue.
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Finalizer Reference
Garbage Collector
The garbage collector is not specified in great detail in the standard, so it can be implemented by different vendors and different versions of the JVM, and can be divided into many types
GC Classification and Performance Metrics
PerformanceKey pointsThroughput(runtime ratio to code runtime) andpause time
Low latency: the proportion of time spent running user code to total runtime (total runtime: program runtime plus memory reclamation time) Throughput: background computation
Current standard: prioritize maximum throughput while reducing pause time
Overview of Different Garbage Collectors
7 classic garbage collectors
- View the default garbage collector
-XX:+PrintCommadLineFlags View command line related parameters (including the garbage collector in use)
Command line instructions jinfo -flag Related garbage collection parameters process ID
Serial Collector: Serial Collection (overview)
ParNew Collector: Parallel Collection (out of favor)
-XX:+UseParNewGC Manually specify ParNew
-XX:ParalleGCThreads Limit the number of threads
Parallel Collector: Throughput Priority (JDK 8 default)
Low latency: requires user interaction Throughput: background computation
-XX:+UserParallelGC
-XX:+UserParal1elOldGC
-XX:ParallelGCThreads
-XX:MaxGCPauseMillis
-XX:GCTimeRatio
-XX:+UseAdaptiveSizePolicy
CMS Collector: Low Latency (concurrent)
CMS Collector: Low Latency
Summary
G1 Collector
G1 收集器則不需要區分新生代和老年代,
G1 採用分區演算法
將整個堆積記憶體劃分成多個大小相等的 Region 區域, 並根據 Region 中的物件存活時間和佔用空間來決定回收優先順序。
- 特點
region- 可預測停頓模型
soft real-time
G1 的特點優勢
Region
一個 region 只能是一個角色, 清空後可以轉換角色, 大於 1.5倍region 的物件會放到 humongous (避免短期大物件放到老年代堆垃圾收集器造成影響,確保老年代都是長期物件)
G1 回收過程
混合回收 :
由於低延遲: 只挑選價值比較高的進行回收
給每個 region 配置了一個 Rset,記錄了該 region 被哪個 region 指向 ^atdrli
Remembered Set
- 年輕代 GC
- 掃描根
- 更新
Rset: 會根據dirty card queue更新Rset(dirty card queue會在引用賦值語句前後保存物件的引用資訊) (由於Rset需要執行緒同步, 沒有直接記錄到Rset中, 而是記錄到dirty car queue中) - 處理
Rset - 複製物件
- 處理引用
- 併發標記過程
- 初始標記階段
- 根區域掃描
- 併發標記 : 只會回收一部分,判斷活性看值不值得回收
- 再次標記 : 修正上一次標記結果
snap-at-the-beginning - 獨佔清理: 計算存活物件和 GC 回收比例,沒有真正清理
- 併發清理
總結
GC 日誌分析
-Xloggc:./logs/gc.log 先要建立對應目錄
快捷
ctrl+shift+n 查類
其他
面試
java
int num = 10;
//s1 the declaration is thread safe
//資料不會在每個堆疊框架之間共享
public static void method1(){
//StringBuilder:執行緒不安全
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
//sBuilder的操作過程:是執行緒不安全的
//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;
}
//s1的操作:是執行緒安全的
public static String method4(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1.toString();
}
總結:當有內部資料的作用域不只在方法內時,就是方法不安全的










































































































































































































































































































































































































































































































