一、程式計數器 程式計數器記憶體很小,可以看作是當前線程所執行位元組碼的行號指示器。 有了它,程式就能被正確的執行。 因為有線程切換的存在,則每個線程必須有各自獨立的程式計數器,即線程私有的記憶體。 這裡再解釋一下什麼是線程切換,線程切換指的是: 單處理器在執行多線程時所進行的線程切換,多線程的交替運行會 ...
一、程式計數器
程式計數器記憶體很小,可以看作是當前線程所執行位元組碼的行號指示器。
有了它,程式就能被正確的執行。
因為有線程切換的存在,則每個線程必須有各自獨立的程式計數器,即線程私有的記憶體。
這裡再解釋一下什麼是線程切換,線程切換指的是:
單處理器在執行多線程時所進行的線程切換,多線程的交替運行會產生同時運行的錯覺。
程式計數器不會發生OOM原因:
占用記憶體非常小,當線程結束時程式計數器也會隨之回收。
二、本地方法棧與虛擬機棧
棧是stack的翻譯,那stack又是什麼?
在英文語境中,stack指的是一摞盤子堆疊起來、一摞書堆疊起來的這種狀態,也就是 a stack of books. 借這種現實物理情境來描述電腦中的數據結構。
這種結構的特征就是LIFO, Last In First Out, 即後進先出。
也就是,一摞盤子,你只能一個一個往上堆,也只能一個一個從頂上往外取,對應**入棧和出棧(彈棧)**的操作。
以上是對棧這種結構的解釋。
接下來說這兩種棧結構:Native Method Stack 和 JVM stack.
棧是線程私有的,它的生命周期和線程是相同的。
棧裡面保存棧幀。
什麼是棧幀?
每個方法執行時都會創建一個棧幀。棧幀存儲了局部變數表、操作數棧、動態連接和方法出口等信息。每個方法從調用到運行結束的過程,就對應著一個棧幀在棧中入棧到出棧的過程。
棧有可能出現什麼異常?
StackOverflowError和OutOfMemoryError。
前者主要是深度遞歸和複雜嵌套方法調用造成。
後者的話發生在棧在進行動態擴展的時候,也就是說 jvm 實現中棧的大小此時是不固定的,因為線程操作需要更多的棧空間而在申請記憶體的時候失敗就會拋出OutOfMemoryError錯誤。
JVM 中的棧包括 Java 虛擬機棧和本地方法棧。
兩者的區別就是:
Java 虛擬機棧為 JVM 執行 Java 方法服務,本地方法棧則為 JVM 使用到的 Native 方法(比如C或C++)服務。
四、堆
Heap有什麼特征?
-
• JVM管理的最大記憶體區域
-
• 線程共用
-
• 存放對象實例
-
• 記憶體空間可以物理上不連續
-
• 垃圾回收的主要區域
關於垃圾回收的內容這裡不展開講了。
五、方法區
JVM規範把方法區描述為堆的一個邏輯部分,但有一個別名叫Non-Heap,即Heap中的Non-heap, 也是說明它和堆其實還是不一樣的。
方法區有什麼特征?
• 線程共用
• 存放已被虛擬機載入的類信息、常量、靜態變數、即時編譯器編譯後的代碼等數據
• 垃圾回收較少出現,甚至可選擇不進行垃圾回收
方法區的垃圾回收主要針對常量池的回收和對類的卸載。
這裡主要說一下運行時常量池:
Class文件中除了有類的版本、欄位、方法、介面等描述信息外還有一項常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的運行時常量池中存放。
這裡出現了兩個常量池,它們是不一樣的,一個叫常量池,一個叫運行時常量池(Runtime Constant Pool), 運行時常量池具備動態性,那怎麼理解動態性呢?
就是說除了編譯期產生的常量進入了常量池在類載入後又緊接著進入了運行時常量池以外,運行期間新的常量也會進入運行時常量池。
關於類載入的介紹我們再單獨寫一篇。
下麵舉一些例子把這裡具體搞搞清楚。
常量池有什麼用 ?
**優點:**常量池避免了頻繁的創建和銷毀對象而影響系統性能,其實現了對象的共用。
下麵具體講一下字元串常量池(String常量池):
String 是由 final 修飾的類,是不可以被繼承的。通常有兩種方式來創建對象。
//1、這種存在Heap中,每次new都會創建一個全新對象
String str = new String("abcd");
//2、這種是在棧上創建對象引用變數str,指向字元串常量池中的“abcd”(沒有的話新建一個)
String str = "abcd";
關於字元串 + 號連接問題:
對於字元串常量的 + 號連接,在程式編譯期,JVM就會將其優化為 + 號連接後的值。所以在編譯期其字元串常量的值就確定了。
String a = "a1";
String b = "a" + 1;
System.out.println((a == b)); //result = true
String a = "atrue";
String b = "a" + "true";
System.out.println((a == b)); //result = true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println((a == b)); //result = true
關於字元串引用 + 號連接問題:
對於字元串引用的 + 號連接問題,由於字元串引用在編譯期是無法確定下來的,在程式的運行期動態分配並創建新的地址存儲對象。
public static void main(String[] args){
String str1 = "a";
String str2 = "ab";
String str3 = str1 + "b";
System.out.print(str2 == str3);//false
}
通過 jad 反編譯工具,分析上述代碼到底做了什麼。
public class TestDemo{
public TestDemo(){
}
public static void main(String args[]){
String s = "a";
String s1 = "ab";
String s2 = (new StringBuilder()).append(s).append("b").toString();
System.out.print(s1 = s2);
}
}
發現 new 了一個 StringBuilder 對象,然後使用 append 方法優化了 + 操作符。new 在堆上創建對象,而 String s1=“ab”則是在常量池中創建對象,兩個應用所指向的記憶體地址是不同的,所以 s1 == s2 結果為 false。
這裡引出一個實際開發中關於字元串拼接的問題。就是儘量不要在 for 迴圈中使用 + 號來操作字元串。
因為如果用“+”號的話,每次迴圈都會創建和銷毀一個StringBuilder對象,這樣還不如在迴圈外創建一個StringBuilder對象,然後使用append方法。
public static void main(String[] args){
StringBuilder s = new StringBuilder();
for(int i = 0; i < 100; i++){
s.append("a");
}
}
使用final修飾的字元串
public static void main(String[] args){
final String str1 = "a";
String str2 = "ab";
String str3 = str1 + "b";
System.out.print(str2 == str3);//true
}
final 修飾的變數是一個常量,編譯期就能確定其值。所以 str1 + "b"就等同於 "a" + "b",所以結果是 true。
String對象的intern方法。
public static void main(String[] args){
String s = "ab";
String s1 = "a";
String s2 = "b";
String s3 = s1 + s2;
System.out.println(s3 == s);//false
System.out.println(s3.intern() == s);//true
}
通過前面學習我們知道,s1+s2 實際上在堆上 new 了一個 StringBuilder 對象,而 s 在常量池中創建對象 “ab”,所以 s3 == s 為 false。
但是 s3 調用 intern 方法,返回的是s3的內容(ab)在常量池中的地址值。所以 s3.intern() == s 結果為 true。
往期推薦:
● 學會@ConfigurationProperties月薪過三千
● 0.o?讓我看看怎麼個事兒之SpringBoot自動配置