當前位置:網站首頁>JVM初探

JVM初探

2022-07-23 05:39:51Hash..

JVM初探總結

-2022.3.25 -BDY

猛猪猪語錄:tmd大鼻子璐猪!!!

在這裏插入圖片描述


前言

本次總結是對看完黑馬程序員的JVM視頻的一次小總結,看完視頻也不清楚學了多少,也沒有一個清楚的認識,所以想到寫一個博客總結一下,主要會摘抄別人的筆記內容。PS:tmd連個大綱都不清楚,還寫個屁。
黑馬程序員JVM
https://www.bilibili.com/video/BV1yE411Z7AP?spm_id_from=333.999.0.0
大佬的JVM筆記
https://nyimac.gitee.io/2020/07/03/JVM%E5%AD%A6%E4%B9%A0/
第二個大佬的JVM筆記
https://blog.csdn.net/weixin_50280576/article/details/113742011


一、什麼是JVM?

定義

java virtual machine ,java程序的運行環境(java二進制季節碼的運行環境)
什麼是java二進制字節碼的運行環境?-> JVM

好處

1.一次編寫,到處運行
2.自動內存管理,垃圾回收機制
3.數組下標越界檢查

1.這也是java的好處,編寫成jar包後,只要有jvm就可以在電腦上運行
2.涉及JVM內存管理,JVM垃圾回收
3.數組下標越界檢查
->

如下定義一個數組: int[] ints = new int[100];
此時就會在堆中開辟一個對應的空間,ints也被分配了相應的內存空間。
這裏從JVM的角度說下自己的理解,不一定是對的哈,比如現在只在堆中給ints分配了相對應它長度100的內存空間,如果不檢查數組下標越界,那麼ints就可以無限分配了,直到堆內存的極限,那麼問題來了,其他的對象也有被分配在堆上,如果數組允許下標越界的內存分配方式,就可能把這個內存上的內容給覆蓋了,也就可能把其他對象給覆蓋了,這樣大家都是數組了,還怎麼做業務。。。


在這裏插入圖片描述

二、內存結構

整體架構和定義

在這裏插入圖片描述
JVM內存結構主要包括:方法區(method area), 堆(heap),虛擬機棧(JVM stacks),程序計數器(PC register),本地方法棧(Native Method Stacks)

方法區(method area)

請添加圖片描述
jdk1.8之後,方法區會移動到本地內存空間,稱為元空間(1.6之前為永久代),JVM中只保存方法區的引用,方法區中的串池也會移動到中存儲

定義: 方法區與java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。它有個別命叫Non-Heap(非堆)。當方法區無法滿足內存分配需求時,拋出OutOfMemoryError异常。

  1. 內存溢出OutOfMemoryError
    1.8以前會導致永久代內存溢出
    1.8以後會導致元空間內存溢出
  2. 常量池
    二進制字節碼的組成:類的基本信息、常量池、類的方法定義(包含了虛擬機指令)
    下面是方法區常量池的實例:請添加圖片描述
    運行時常量池
    常量池是.class文件中的,當該*類被加載以後,它的常量池信息就會放入運行時常量池,並把裏面的符號地址變為真實地址**
  3. 串池StringTable

特征

1.常量池中的字符串僅是符號,只有在被用到時才會轉化為對象
2.利用串池的機制,來避免重複創建字符串對象(元素不重複)
3…字符串變量拼接的原理是StringBuilder
4.字符串常量拼接的原理是編譯器優化
5. 可以使用intern方法,主動將串池中還沒有的字符串對象放入串池中
6. 注意:無論是串池還是堆裏面的字符串,都是對象

intern方法1.8
如果串池中沒有該字符串對象,則放入成功
如果有該字符串對象,則放入失敗
無論放入是否成功,都會返回串池中的字符串對象

StringTable調優
因為StringTable是由HashTable實現的,所以可以適當增加HashTable桶的個數,(减少hash碰撞),來减少字符串放入串池所需要的時間

-XX:StringTableSize=xxxx

堆(heap)

  • 定義
    通過new關鍵字創建的對象都會被放在堆內存
    PS:在堆中放入的是創建的對象,在方法區中是對象的引用。
  • 特點
    1.線程共享
    2.有垃圾回收機制
    3.堆和方法區同樣都是共享區
  • 堆內存溢出
    java.lang.OutofMemoryError :java heap space. 堆內存溢出

原因: 堆是Java程序中最為重要的內存空間,由於大量的對象都直接分配在堆上,因此它也成為最有可能發生溢出的區間。一般來說,絕大部分Java的內存溢出都屬於這種情况。其原因是因為大量對象占據了堆空間,而這些對象都持有强引用,導致無法回收,當對象大小之和大於由Xmx參數指定的堆空間大小時,溢出錯誤就自然而然地發生了。

虛擬機棧(JVM stacks)

  • 定義

1.每個線程運行需要的內存空間,稱為虛擬機棧

2.每個棧由多個棧幀組成,對應著每次調用方法時所占用的內存,每個方法對應一個棧幀。

3.每個線程只能有一個活動棧幀,對應著當前正在執行的方法。

  • 問題

1.垃圾回收是否涉及棧內存
不需要。因為虛擬機棧中是由一個個棧幀組成的,在方法執行完畢後,對應的棧幀就會被彈出棧。所以無需通過垃圾回收機制去回收內存。

2.棧內存的分配越大越好嗎
不是。因為物理內存是一定的,棧內存越大,可以支持更多的遞歸調用,但是可執行的線程數就會越少。

3.方法內的局部變量是否是線程安全的
如果方法內局部變量沒有逃離方法的作用範圍,則是線程安全的
如果如果局部變量引用了對象,並逃離了方法的作用範圍,則需要考慮線程安全問題

  • 內存溢出

Java.lang.stackOverflowError 棧內存溢出

原因:棧幀過多(無限遞歸),棧幀過大。

程序計數器(PC register)

  • 作用
    用於保存JVM中下一條所要執行的指令的地址
  • 特點
    1.線程私有
    當線程時間片用完再從新獲取時間片後,通過程序計數器可以知道要執行哪條代碼。
    2.不會存在內存溢出

本地方法棧(Native Method Stacks)

一些帶有native關鍵字的方法就是需要JAVA去調用本地的C或者C++方法,因為JAVA有時候沒法直接和操作系統底層交互,所以需要用到本地方法

直接內存

請添加圖片描述

直接內存的回收機制總結 使用了Unsafe類來完成直接內存的分配回收,回收需要主動調用freeMemory方法
ByteBuffer的實現內部使用了Cleaner(虛引用)來檢測ByteBuffer。一旦ByteBuffer被垃圾回收,那麼會由ReferenceHandler來調用Cleaner的clean方法調用freeMemory來釋放內存


三、垃圾回收

1.判斷垃圾是否可回收(垃圾回收原理)

  • 引用計數法
    顧名思義,當對象被引用後,計數加一,消除引用,計數减一
    弊端:循環引用時,兩個對象的計數都為1,導致兩個對象都無法被釋放請添加圖片描述
  • 可達性分析算法
    這是JVM使用的算法。
    方法:掃描堆中的對象,看能否沿著GC Root對象為起點的引用鏈找到該對象,如果找不到,則錶示可以回收

補充:

可以作為GC Root的對象
虛擬機棧(棧幀中的本地變量錶)中引用的對象。 
方法區中類靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中JNI(即一般說的Native方法)引用的對象

  • 五種引用

請添加圖片描述
强引用
只有GC Root都不引用該對象時,才會回收强引用對象
如上圖B、C對象都不引用A1對象時,A1對象才會被回收

軟引用
當GC Root指向軟引用對象時,在內存不足時,會回收軟引用所引用的對象,只有當內存不足時才會回收
如上圖如果B對象不再引用A2對象且內存不足時,軟引用所引用的A2對象就會被回收

軟引用的使用

public class Demo1 {
    
	public static void main(String[] args) {
    
		final int _4M = 4*1024*1024;
		//使用軟引用對象 list和SoftReference是强引用,而SoftReference和byte數組則是軟引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
	}
}

如果想要清理軟引用,需要使用引用隊列
大概思路為:查看引用隊列中有無軟引用,如果有,則將該軟引用從存放它的集合中移除(這裏為一個list集合)

弱引用
只有弱引用引用該對象時,在垃圾回收時,無論內存是否充足,都會回收弱引用所引用的對象
如上圖如果B對象不再引用A3對象,則A3對象會被回收
弱引用的使用和軟引用類似,只是將 SoftReference 換為了 WeakReference,在是否回收時有區別

虛引用
當虛引用對象所引用的對象被回收以後,虛引用對象就會被放入引用隊列中,調用虛引用的方法
虛引用的一個體現是釋放直接內存所分配的內存,當引用的對象ByteBuffer被垃圾回收以後,虛引用對象Cleaner就會被放入引用隊列中,然後調用Cleaner的clean方法來釋放直接內存
如上圖,B對象不再引用ByteBuffer對象,ByteBuffer就會被回收。但是直接內存中的內存還未被回收。這時需要將虛引用對象Cleaner放入引用隊列中,然後調用它的clean方法來釋放直接內存

終結器引用
所有的類都繼承自Object類,Object類有一個finalize方法。當某個對象不再被其他的對象所引用時,會先將終結器引用對象放入引用隊列中,然後根據終結器引用對象找到它所引用的對象,然後調用該對象的finalize方法。調用以後,該對象就可以被垃圾回收了
如上圖,B對象不再引用A4對象。這是終結器對象就會被放入引用隊列中,引用隊列會根據它,找到它所引用的對象。然後調用被引用對象的finalize方法。調用以後,該對象就可以被垃圾回收了
引用隊列
1.軟引用和弱引用可以配合引用隊列
在弱引用和虛引用所引用的對象被回收以後,會將這些引用放入引用隊列中,方便一起回收這些軟/弱引用對象

2.虛引用和終結器引用必須配合引用隊列
虛引用和終結器引用在使用時會關聯一個引用隊列

2.垃圾回收算法(垃圾回收過程)

  1. 標記—清除
    請添加圖片描述

定義:標記清除算法顧名思義,是指在虛擬機執行垃圾回收的過程中,先采用標記算法確定可回收對象,然後垃圾收集器根據標識清除相應的內容,給堆內存騰出相應的空間,這裏的騰出內存空間並不是將內存空間的字節清0,而是記錄下這段內存的起始結束地址,下次分配內存的時候,會直接覆蓋這段內存。

缺點:容易產生大量的內存碎片,可能無法滿足大對象的內存分配,一旦導致無法分配對象,那就會導致jvm啟動gc,一旦啟動gc,我們的應用程序就會暫停,這就導致應用的響應速度變慢

  1. 標記—整理
    請添加圖片描述

**定義:**標記-整理 會將不被GC Root引用的對象回收,清楚其占用的內存空間。然後整理剩餘的對象,可以有效避免因內存碎片而導致的問題

**缺點:**但是因為整體需要消耗一定的時間,所以效率較低

  1. 複制

請添加圖片描述
請添加圖片描述
請添加圖片描述
請添加圖片描述

將內存分為等大小的兩個區域,FROM和TO(TO中為空)。先將被GC Root引用的對象從FROM放入TO中,再回收不被GC Root引用的對象。然後交換FROM和TO。這樣也可以避免內存碎片的問題,但是會占用雙倍的內存空間

3.分代回收(垃圾回收過程)

請添加圖片描述
流程:
1.新創建的對象都被放在了新生代的伊甸園中
請添加圖片描述
2.當伊甸園中的內存不足時,就會進行一次垃圾回收,這時的回收叫做 Minor GC

Minor GC 會將伊甸園和幸存區FROM存活的對象先複制到 幸存區 TO中, 並讓其壽命加1,再交換兩個幸存區
請添加圖片描述

請添加圖片描述
請添加圖片描述
3.再次創建對象,若新生代的伊甸園又滿了,則會再次觸發 Minor GC(會觸發 stop the world, 暫停其他用戶線程,只讓垃圾回收線程工作),這時不僅會回收伊甸園中的垃圾,還會回收幸存區中的垃圾,再將活躍對象複制到幸存區TO中。回收以後會交換兩個幸存區,並讓幸存區中的對象壽命加1
請添加圖片描述
4.如果幸存區中的對象的壽命超過某個閾值(最大為15,4bit),就會被放入老年代中請添加圖片描述
如果新生代老年代中的內存都滿了,就會先觸發Minor GC,再觸發Full GC,掃描新生代和老年代中所有不再使用的對象並回收

補:GC分析

大對象處理策略 當遇到一個較大的對象時,就算新生代的伊甸園為空,也無法容納該對象時,會將該對象直接晋昇為老年代

///線程內存溢出 某個線程的內存溢出了而拋异常(out of memory),不會讓其他的線程結束運行

這是因為當一個線程拋出OOM异常後,它所占據的內存資源會全部被釋放掉,從而不會影響其他線程的運行,進程依然正常

4.垃圾回收器(垃圾回收的幾種垃圾回收器使用)

1.相關概念

並行收集:指多條垃圾收集線程並行工作,但此時用戶線程仍處於等待狀態。

並發收集:指用戶線程與垃圾收集線程同時工作(不一定是並行的可能會交替執行)。用戶程序在繼續運行,而垃圾收集程序運行在另一個CPU上

吞吐量:即CPU用於運行用戶代碼的時間與CPU總消耗時間的比值(吞吐量 = 運行用戶代碼時間 / ( 運行用戶代碼時間 + 垃圾收集時間 )),也就是。例如:虛擬機共運行100分鐘,垃圾收集器花掉1分鐘,那麼吞吐量就是99%
串行
單線程
內存較小,個人電腦(CPU核數較少)

請添加圖片描述

安全點:讓其他線程都在這個點停下來,以免垃圾回收時移動對象地址,使得其他線程找不到被移動的對象
因為是串行的,所以只有一個垃圾回收線程。且在該線程執行回收工作時,其他線程進入阻塞狀態

2.回收器

1.Serial 收集器

2.ParNew 收集器

3.Serial Old 收集器

4.Parallel Scavenge 收集器

5.Parallel Old 收集器

6.CMS 收集器

7.G1

PS:關於各種回收器的知識暫不理解,等有機會需要用到再來補充!!!

5.GC調優(垃圾回收器調優)

1.調優領域

內存 鎖競爭 CPU占用 IO GC

2.目標

低延遲/高吞吐量? 選擇合適的GC

3.最好的GC是不發生GC

首先排除减少因為自身編寫的代碼而引發的內存問題

查看Full GC前後的內存占用,考慮以下幾個問題 數據是不是太多? 數據錶示是否太臃腫 對象圖 對象大小 是否存在內存泄漏

4.新生代調優

特點:
所有的new操作分配內存都是非常廉價的
TLAB
死亡對象回收零代價
大部分對象用過即死(朝生夕死)
MInor GC 所用時間遠小於Full GC

關於新生代內存是否越大越好?
不是
新生代內存太小:頻繁觸發Minor GC,會STW,會使得吞吐量下降
新生代內存太大:老年代內存占比有所降低,會更頻繁地觸發Full GC。而且觸發Minor GC時,清理新生代所花費的時間會更長

5.幸存區調優

改變晋昇閾值,改變幸存區大小
幸存區需要能够保存 當前活躍對象+需要晋昇的對象
晋昇閾值配置得當,讓長時間存活的對象盡快晋昇

6.老年代調優
更改老年代內存區大小


四、類加載和字節碼技術

請添加圖片描述
類加載流程圖:
請添加圖片描述

首先是編譯期,將Java源文件也就是敲好的代碼通過編譯,轉換成.class文件,也就是字節碼文件(byte),然後經過傳輸傳給類加載器,傳輸的是剛轉換好的字節碼文件,也可以是通過網絡傳輸過來的字節碼文件,這個是分布式架構下的情况。

然後就是運行期,運行期一開始,類加載器初始化字節碼文件,通過本地類庫來驗證字節碼文件的正確性,然後交給JVM的解釋器和即時編譯器,最後匯合給JVM內部的Java運行系統,都ok了後傳給PC的操作系統,最後就是物理硬件層面。

1.類文件結構

類的字節碼文件:

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14

類文件結構

u4 magic
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];

PS:面試不考,再見!

2.字節碼指令(各種類型分析)

了解類加載流程
代碼:

public class Demo3_1 {
        
	public static void main(String[] args) {
            
		int a = 10;        
		int b = Short.MAX_VALUE + 1;        
		int c = a + b;        
		System.out.println(c);   
    } 
}

1.常量池載入運行時常量池
請添加圖片描述
2.方法字節碼載入方法區
請添加圖片描述
3.開始執行字節碼
具體流程請看轉載:
大佬的JVM筆記
https://nyimac.gitee.io/2020/07/03/JVM%E5%AD%A6%E4%B9%A0/

4.構造方法

cinit()V

在類加載的准備階段,虛擬機會為static的類變量賦上類型的初始值、常量附上定義的值(值必須為字面量或常量)。

public class Demo3 {
    
	static int i = 10;

	static {
    
		i = 20;
	}

	static {
    
		i = 30;
	}

	public static void main(String[] args) {
    
		System.out.println(i); //結果為30
	}
}

編譯器會按從上至下的順序,收集所有 static 靜態代碼塊和靜態成員賦值的代碼,合並為一個特殊的方法 cinit()V

init()V

在new對象之後,init方法之前,虛擬機會為實例變量賦上類型初始值,常量附上定義的值(值必須為字面量或常量)。

public class Demo4 {
    
	private String a = "s1";

	{
    
		b = 20;
	}

	private int b = 10;

	{
    
		a = "s2";
	}

	public Demo4(String a, int b) {
    
		this.a = a;
		this.b = b;
	}

	public static void main(String[] args) {
    
		Demo4 d = new Demo4("s3", 30);
		System.out.println(d.a);
		System.out.println(d.b);
	}
}

編譯器會按從上至下的順序,收集所有 {} 代碼塊和成員變量賦值的代碼,形成新的構造方法,但原始構造方法內的代碼總是在後

cinit和init方法區別:
一個在類加載准備階段,一個在new對象之後
加載方法類型不同,一個是靜態代碼塊,一個是{}代碼塊和成員變量

5.方法調用

public class Demo5 {
    
	public Demo5() {
    

	}

	private void test1() {
    

	}

	private final void test2() {
    

	}

	public void test3() {
    

	}

	public static void test4() {
    

	}

	public static void main(String[] args) {
    
		Demo5 demo5 = new Demo5();
		demo5.test1();
		demo5.test2();
		demo5.test3();
		Demo5.test4();
	}
}

不同方法在調用時,對應的虛擬機指令有所區別
1.私有、構造、被final修飾的方法,在調用時都使用invokespecial指令
2.普通成員方法在調用時,使用invokespecial指令。因為編譯期間無法確定該方法的內容,只有在運行期間才能確定
3. 靜態方法在調用時使用invokestatic指令

new 是創建【對象】,給對象分配堆內存,執行成功會將【對象引用】壓入操作數棧
dup 是賦值操作數棧棧頂的內容,本例即為【對象引用】,為什麼需要兩份引用呢,一個是要配合 invokespecial 調用該對象的構造方法 “init”()V (會消耗掉棧頂一個引用),另一個要 配合 astore_1 賦值給局部變量
終方法(final),私有方法(private),構造方法都是由 invokespecial 指令來調用,屬於靜態綁定
普通成員方法是由 invokevirtual 調用,屬於動態綁定,即支持多態 成員方法與靜態方法調用的另一個區別是,執行方法前是否需要【對象引用】

6.多態原理
多態原理
有億點點難

因為普通成員方法需要在運行時才能確定具體的內容,所以虛擬機需要調用invokevirtual指令

在執行invokevirtual指令時,經曆了以下幾個步驟:

先通過棧幀中對象的引用找到對象
分析對象頭,找到對象實際的Class
Class結構中有vtable
查詢vtable找到方法的具體地址
執行方法的字節碼 异常處理

7.异常處理(try—catch)

public class Demo1 {
    
	public static void main(String[] args) {
    
		int i = 0;
		try {
    
			i = 10;
		}catch (Exception e) {
    
			i = 20;
		}
	}
}

Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
//多出來一個异常錶
Exception table:
from to target type
2 5 8 Class java/lang/Exception

可以看到多出來一個 Exception table 的結構,[from, to) 是前閉後開(也就是檢測2~4行)的檢測範圍,一旦這個範圍內的字節碼執行出現异常,則通過 type 匹配异常類型,如果一致,進入 target 所指示行號
8行的字節碼指令 astore_2 是將异常對象引用存入局部變量錶的2號比特置(為e)

3.編譯器處理

經典:語法糖
所謂的 語法糖 ,其實就是指 java 編譯器把 .java 源碼編譯為 .class 字節碼的過程中,自動生成和轉換**的一些代碼,主要是為了减輕程序員的負擔,算是 java 編譯器給我們的一個額外福利

1.構造函數

public class Candy1 {
    

}
public class Candy1 {
    
   //這個無參構造器是java編譯器幫我們加上的
   public Candy1() {
    
      //即調用父類 Object 的無參構造方法,即調用 java/lang/Object." <init>":()V
      super();
   }
}

2.自動拆裝箱

基本類型和其包裝類型的相互轉換過程,稱為拆裝箱

public class Demo2 {
    
   public static void main(String[] args) {
    
      Integer x = 1;
      int y = x;
   }
}
public class Demo2 {
    
   public static void main(String[] args) {
    
      //基本類型賦值給包裝類型,稱為裝箱
      Integer x = Integer.valueOf(1);
      //包裝類型賦值給基本類型,稱謂拆箱
      int y = x.intValue();
   }
}

3.泛型集合取值

泛型也是在 JDK 5 開始加入的特性,但 java 在編譯泛型代碼後會執行 泛型擦除 的動作,即泛型信息在編譯為字節碼之後就丟失了,實際的類型都當做了 Object 類型來處理

4.可變參數

public class Demo4 {
    
   public static void foo(String... args) {
    
      //將args賦值給arr,可以看出String...實際就是String[] 
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
    
      foo("hello", "world");
   }
}

可變參數 String… args 其實是一個 String[] args

public class Demo4 {
    
   public Demo4 {
    }

    
   public static void foo(String[] args) {
    
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
    
      foo(new String[]{
    "hello", "world"});
   }
}

5.foreach
foreach轉為for

public class Demo5 {
    
	public static void main(String[] args) {
    
        //數組賦初值的簡化寫法也是一種語法糖。
		int[] arr = {
    1, 2, 3, 4, 5};
		for(int x : arr) {
    
			System.out.println(x);
		}
	}
}
public class Demo5 {
    
    public Demo5 {
    }

	public static void main(String[] args) {
    
		int[] arr = new int[]{
    1, 2, 3, 4, 5};
		for(int i=0; i<arr.length; ++i) {
    
			int x = arr[i];
			System.out.println(x);
		}
	}
}

如果是集合使用foreach,需要該集合類實現了Iterable接口,因為集合的遍曆需要用到迭代器Iterator.

while(iterator.hasNext()) {
Integer x = iterator.next();
System.out.println(x);
}

6.switch字符串

public class Demo6 {
    
   public static void main(String[] args) {
    
      String str = "hello";
      switch (str) {
    
         case "hello" :
            System.out.println("h");
            break;
         case "world" :
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}
public class Demo6 {
    
   public Demo6() {
    
      
   }
   public static void main(String[] args) {
    
      String str = "hello";
      int x = -1;
      //通過字符串的hashCode+value來判斷是否匹配
      switch (str.hashCode()) {
    
         //hello的hashCode
         case 99162322 :
            //再次比較,因為字符串的hashCode有可能相等
            if(str.equals("hello")) {
    
               x = 0;
            }
            break;
         //world的hashCode
         case 11331880 :
            if(str.equals("world")) {
    
               x = 1;
            }
            break;
         default:
            break;
      }

      //用第二個switch在進行輸出判斷
      switch (x) {
    
         case 0:
            System.out.println("h");
            break;
         case 1:
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}

7.其餘類型

switch枚舉
枚舉類
匿名內部類

4.類加載階段

加載-鏈接-驗證-准備-解析-初始化
請添加圖片描述

一、加載
1.將類的字節碼載入方法區(1.8後為元空間,在本地內存中)中,內部采用 C++ 的 instanceKlass 描述 java 類,它的重要 field 有:
_java_mirror 即 java 的類鏡像,例如對 String 來說,它的鏡像類就是 String.class,作用是把 klass 暴露給 java 使用
_super 即父類
_fields 即成員變量
_methods 即方法
_constants 即常量池
_class_loader 即類加載器
_vtable 虛方法錶
_itable 接口方法

2.如果這個類還有父類沒有加載,先加載父類

3.加載和鏈接可能是交替運行的

4.nstanceKlass保存在方法區。JDK 8以後,方法區比特於元空間中,而元空間又比特於本地內存中

5.InstanceKlass和*.class(JAVA鏡像類)互相保存了對方的地址

6.類的對象在對象頭中保存了*.class的地址。讓對象可以通過其找到方法區中的instanceKlass,從而獲取類的各種信息

二、鏈接

三、驗證
驗證類是否符合 JVM規範,安全性檢查

四、准備
為 static 變量分配空間,設置默認值

static變量在JDK 7以前是存儲與instanceKlass末尾。但在JDK 7以後就存儲在_java_mirror末尾了
static變量在分配空間和賦值是在兩個階段完成的。分配空間在准備階段完成,賦值在初始化階段完成
如果 static 變量是 final 的基本類型,以及字符串常量,那麼編譯階段值就確定了,賦值在准備階段完成
如果 static 變量是 final 的,但屬於引用類型,那麼賦值也會在初始化階段完成

五、解析

含義:將常量池中的符號引用解析為直接引用

未解析時,常量池中的看到的對象僅是符號,未真正的存在於內存中
解析以後,會將常量池中的符號引用解析為直接引用

六、初始化

初始化階段就是執行類構造器clinit()方法的過程,虛擬機會保證這個類的『構造方法』的線程安全

發生時機

類的初始化的懶惰的,以下情况會初始化:

main 方法所在的類,總會被首先初始化

首次訪問這個類的靜態變量或靜態方法時

子類初始化,如果父類還沒初始化,會引發

子類訪問父類的靜態變量,只會觸發父類的初始化

Class.forName

new 會導致初始化

驗證類是否被初始化,可以看改類的靜態代碼塊是否被執行

5.類加載器

1.定義:

Java虛擬機設計團隊有意把類加載階段中的“通過一個類的全限定名來獲取描述該類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己决定如何去獲取所需的類。實現這個動作的代碼被稱為“類加載器”(ClassLoader)

類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠超類加載階段

對於任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。這句話可以錶達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class文件,被同一個Java虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等

在這裏插入圖片描述
2.啟動類加載器Bootstrap ClassLoader

3.拓展類加載器Extension ClassLoader

4.應用程序類加載器Application ClassLoader

5.自定義加載器

6.雙親委派機制和沙箱安全機制

雙親委派機制:

程序加載某個類,查找過程(從父級開始找):
先在bootstrap ClassLoaer下,找不到就會去 Extension ClassLoaer 下找
如果在Extension ClassLoaer 找不到就會去App ClassLoaer找
如果在App ClassLoaer找不到,一般就會拋出class not found 异常

解釋
當一個類收到了類的加載請求,他首先不會自己去加載這個類,而是把這個請求委派給父親去完成,每一層的類加載器都是如此,只有當父類加載器反饋自己無法完成這個請求的時候(在他的加載的路徑下沒有找到所需要加載的class),子類加載器才會嘗試自己去加載。

優點
比如加載比特於rt.jar包中的類java.lang.Object,不管是哪個加載器加載這個類,最終都會委托給頂層的啟動類(BootStrap Class Loader)進行加載這樣就保證了不同的加載的類加載器最終得到的都是同樣的一個Object 對象

沙箱安全機制:

沙箱安全:程序員寫的代碼不會污染Java出廠自帶的源代碼,這樣就可以保證大家用的都是同一個代碼

解釋:

因為有雙親委派機制,也就是會在Java的BootStrap Class Loader 加載的jar包中尋找,在該jar包下就會找到一個java.lang.String 的類,此時就會停止在子級中尋找(先到先得原則),但是在該類中並未找到main方法,所以運行時會報錯。

這樣就保證了Java的出廠源碼不會受到開發人員編寫的污染(沙箱安全機制)

6.運行期優化

1.分層編譯
JVM 將執行狀態分成了 5 個層次:

0層:解釋執行,用解釋器將字節碼翻譯為機器碼
1層:使用 C1 即時編譯器編譯執行(不帶 profiling)
2層:使用 C1即時編譯器編譯執行(帶基本的profiling)
3層:使用 C1 即時編譯器編譯執行(帶完全的profiling)
4層:使用 C2 即時編譯器編譯執行

即時編譯器(JIT)與解釋器的區別

解釋器
將字節碼解釋為機器碼,下次即使遇到相同的字節碼,仍會執行重複的解釋
是將字節碼解釋為針對所有平臺都通用的機器碼

即時編譯器
將一些字節碼編譯為機器碼,並存入 Code Cache,下次遇到相同的代碼,直接執行,無需再編譯
根據平臺類型,生成平臺特定的機器碼

逃逸分析

全局逃逸(GlobalEscape)
即一個對象的作用範圍逃出了當前方法或者當前線程,有以下幾種場景:
對象是一個靜態變量
對象是一個已經發生逃逸的對象
對象作為當前方法的返回值

參數逃逸(ArgEscape)
即一個對象被作為方法參數傳遞或者被參數引用,但在調用過程中不會發生全局逃逸,這個狀態是通過被調方法的字節碼確定的

沒有逃逸
即方法中的對象沒有發生逃逸

逃逸分析優化

1.鎖消除

我們知道線程同步鎖是非常犧牲性能的,當編譯器確定當前對象只有當前線程使用,那麼就會移除該對象的同步鎖

例如,StringBuffer 和 Vector 都是用 synchronized
修飾線程安全的,但大部分情况下,它們都只是在當前線程中用到,這樣編譯器就會優化移除掉這些鎖操作

2.標量替換

3.棧上分配

當對象沒有發生逃逸時,該對象就可以通過標量替換分解成成員標量分配在棧內存中,和方法的生命周期一致,隨著棧幀出棧時銷毀,减少了 GC
壓力,提高了應用程序性能

2.方法內聯

不建議現在學習!!!
後面再說

3.反射優化

不建議現在學習!!!
後面再說


五、內存模型

主要是關於JUC中的知識,詳情等學完JUC再來補充

版權聲明
本文為[Hash..]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/204/202207221755086075.html

隨機推薦