JVM入门
文章目錄
- 什么是JVM
- 內存結構
- 垃圾回收
- 類加載與字節碼技術
什么是JVM
定義
Java Virtual Machine,JAVA程序的運行環境(JAVA二進制字節碼的運行環境)
好處
一次編寫,到處運行
自動內存管理,垃圾回收機制
數組下標越界檢查
內存結構
1、程序計數器
用于保存JVM中下一條所要執行的指令的地址
特點
線程私有
CPU會為每個線程分配時間片,當當前線程的時間片使用完以后,CPU就會去執行另一個線程中的代碼
程序計數器是每個線程所私有的,當另一個線程的時間片用完,又返回來執行當前線程的代碼時,通過程序計數器可以知道應該執行哪一句指令,不會存在內存溢出
2、虛擬機棧
每個線程運行需要的內存空間,稱為虛擬機棧
每個棧由多個棧幀組成,對應著每次調用方法時所占用的內存
每個線程只能有一個活動棧幀,對應著當前正在執行的方法
問題辨析
垃圾回收是否涉及棧內存?
不需要。因為虛擬機棧中是由一個個棧幀組成的,在方法執行完畢后,對應的棧幀就會被彈出棧。所以無需通過垃圾回收機制去回收內存。
棧內存的分配越大越好嗎?
不是。因為物理內存是一定的,棧內存越大,可以支持更多的遞歸調用,但是可執行的線程數就會越少。
方法內的局部變量是否是線程安全的?
如果方法內局部變量沒有逃離方法的作用范圍,則是線程安全的
如果如果局部變量引用了對象,并逃離了方法的作用范圍,則需要考慮線程安全問題
內存溢出
Java.lang.stackOverflowError 棧內存溢出
發生原因
虛擬機棧中,棧幀過多(無限遞歸)
每個棧幀所占用過大
線程運行診斷
CPU占用過高
3、本地方法棧
一些帶有native關鍵字的方法就是需要JAVA去調用本地的C或者C++方法,因為JAVA有時候沒法直接和操作系統底層交互,所以需要用到本地方法
4、堆
通過new關鍵字創建的對象都會被放在堆內存
特點
所有線程共享,堆內存中的對象都需要考慮線程安全問題
有垃圾回收機制
堆內存溢出
java.lang.OutofMemoryError :java heap space. 堆內存溢出
堆內存診斷
jps
jmap
jconsole
jvirsalvm
5、方法區
內存溢出
1.8以前會導致永久代內存溢出
1.8以后會導致元空間內存溢出
常量池
二進制字節碼的組成:類的基本信息、常量池、類的方法定義(包含了虛擬機指令)
常量池
就是一張表(如上圖中的constant pool),虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量信息
運行時常量池
常量池是.class文件中的,當該類被加載以后,它的常量池信息就會放入運行時常量池,并把里面的符號地址變為真實地址
串池StringTable
常量池中的字符串僅是符號,只有在被用到時才會轉化為對象
利用串池的機制,來避免重復創建字符串對象
字符串變量拼接的原理是StringBuilder
字符串常量拼接的原理是編譯器優化
可以使用intern方法,主動將串池中還沒有的字符串對象放入串池中
注意:無論是串池還是堆里面的字符串,都是對象
Jdk1.8
Jdk1.6
Jdk1.8 串池(StringTable)從方法區(永久代)移到堆中
Jdk1.6 運行時常量池(永久代)字符串常量池(永久代)
Jdk1.8 運行時常量池(元空間)串池(堆)
6、直接內存
屬于操作系統,常見于NIO操作時,用于數據緩沖區
分配回收成本較高,但讀寫性能高
不受JVM內存回收管理
文件讀寫流程
使用了DirectBuffer
直接內存是操作系統和Java代碼都可以訪問的一塊區域,無需將代碼從系統內存復制到Java堆內存,從而提高了效率
垃圾回收
1、如何判斷對象可以回收
引用計數法
弊端:循環引用時,兩個對象的計數都為1,導致兩個對象都無法被釋放
可達性分析算法
JVM中的垃圾回收器通過可達性分析來探索所有存活的對象
掃描堆中的對象,看能否沿著GC Root對象為起點的引用鏈找到該對象,如果找不到,則表示可以回收
可以作為GC Root的對象
虛擬機棧(棧幀中的本地變量表)中引用的對象。
方法區中類靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中JNI(即一般說的Native方法)引用的對象
五種引用
強引用
只有GC Root都不引用該對象時,才會回收強引用對象
軟引用
當GC Root指向軟引用對象時,在內存不足時,會回收軟引用所引用的對象
如果在垃圾回收時發現內存不足,在回收軟引用所指向的對象時,軟引用本身不會被清理
如果想要清理軟引用,需要使用引用隊列
大概思路為:查看引用隊列中有無軟引用,如果有,則將該軟引用從存放它的集合中移除(這里為一個list集合)
弱引用
只有弱引用引用該對象時,在垃圾回收時,無論內存是否充足,都會回收弱引用所引用的對象
虛引用
軟引用和弱引用可以配合引用隊列
在弱引用和虛引用所引用的對象被回收以后,會將這些引用放入引用隊列中,方便一起回收這些軟/弱引用對象
虛引用和終結器引用必須配合引用隊列
虛引用和終結器引用在使用時會關聯一個引用隊列
2、垃圾回收算法
標記-清除
定義:標記清除算法顧名思義,是指在虛擬機執行垃圾回收的過程中,先采用標記算法確定可回收對象,然后垃圾收集器根據標識清除相應的內容,給堆內存騰出相應的空間
這里的騰出內存空間并不是將內存空間的字節清0,而是記錄下這段內存的起始結束地址,下次分配內存的時候,會直接覆蓋這段內存
缺點:容易產生大量的內存碎片,可能無法滿足大對象的內存分配,一旦導致無法分配對象,那就會導致jvm啟動gc,一旦啟動gc,我們的應用程序就會暫停,這就導致應用的響應速度變慢
標記-整理
標記-整理 會將不被GC Root引用的對象回收,清楚其占用的內存空間。然后整理剩余的對象,可以有效避免因內存碎片而導致的問題,但是因為整體需要消耗一定的時間,所以效率較低
復制
將內存分為等大小的兩個區域,FROM和TO(TO中為空)。先將被GC Root引用的對象從FROM放入TO中,再回收不被GC Root引用的對象。然后交換FROM和TO。這樣也可以避免內存碎片的問題,但是會占用雙倍的內存空間
3、分代回收
回收流程
新創建的對象都被放在了新生代的伊甸園中
當伊甸園中的內存不足時,就會進行一次垃圾回收,這時的回收叫做 Minor GC
Minor GC 會將伊甸園和幸存區FROM存活的對象先復制到 幸存區 TO中, 并讓其壽命加1,再交換兩個幸存區
再次創建對象,若新生代的伊甸園又滿了,則會再次觸發 Minor GC(會觸發 stop the world, 暫停其他用戶線程,只讓垃圾回收線程工作),這時不僅會回收伊甸園中的垃圾,還會回收幸存區中的垃圾,再將活躍對象復制到幸存區TO中?;厥找院髸粨Q兩個幸存區,并讓幸存區中的對象壽命加1
如果幸存區中的對象的壽命超過某個閾值(最大為15,4bit),就會被放入老年代中
如果新生代老年代中的內存都滿了,就會先觸發Minor GC,再觸發Full GC,掃描新生代和老年代中所有不再使用的對象并回收
4、垃圾回收器
并行收集:指多條垃圾收集線程并行工作,但此時用戶線程仍處于等待狀態。
并發收集:指用戶線程與垃圾收集線程同時工作(不一定是并行的可能會交替執行)。用戶程序在繼續運行,而垃圾收集程序運行在另一個CPU上
吞吐量:即CPU用于運行用戶代碼的時間與CPU總消耗時間的比值(吞吐量 = 運行用戶代碼時間 / ( 運行用戶代碼時間 + 垃圾收集時間 )),也就是。例如:虛擬機共運行100分鐘,垃圾收集器花掉1分鐘,那么吞吐量就是99%
4.1、串行
單線程
內存較小,個人電腦(CPU核數較少)
安全點:讓其他線程都在這個點停下來,以免垃圾回收時移動對象地址,使得其他線程找不到被移動的對象
因為是串行的,所以只有一個垃圾回收線程。且在該線程執行回收工作時,其他線程進入阻塞狀態
Serial 收集器
Serial收集器是最基本的、發展歷史最悠久的收集器
特點:單線程、簡單高效(與其他收集器的單線程相比),采用復制算法。對于限定單個CPU的環境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程手機效率。收集器進行垃圾回收時,必須暫停其他所有的工作線程,直到它結束(Stop The World)
ParNew 收集器
ParNew收集器其實就是Serial收集器的多線程版本
特點:多線程、ParNew收集器默認開啟的收集線程數與CPU的數量相同,在CPU非常多的環境中,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。和Serial收集器一樣存在Stop The World問題
Serial Old 收集器
Serial Old是Serial收集器的老年代版本
特點:同樣是單線程收集器,采用標記-整理算法
4.2、吞吐量優先
多線程
堆內存較大,多核CPU
單位時間內,STW(stop the world,停掉其他所有工作線程)時間最短
JDK1.8默認使用的垃圾回收器
Parallel Scavenge 收集器
與吞吐量關系密切,故也稱為吞吐量優先收集器
特點:屬于新生代收集器也是采用復制算法的收集器(用到了新生代的幸存區),又是并行的多線程收集器(與ParNew收集器類似)
Parallel Old 收集器
是Parallel Scavenge收集器的老年代版本
特點:多線程,采用標記-整理算法(老年代沒有幸存區)
4.3、響應時間優先
多線程
堆內存較大,多核CPU
盡可能讓單次STW時間變短(盡量不影響其他線程運行)
CMS 收集器
Concurrent Mark Sweep,一種以獲取最短回收停頓時間為目標的老年代收集器
特點:
基于標記-清除算法實現。并發收集、低停頓,但是會產生內存碎片
應用場景:
適用于注重服務的響應速度,希望系統停頓時間最短,給用戶帶來更好的體驗等場景下。如web程序、b/s服務
CMS收集器的運行過程分為下列4步:
初始標記:標記GC Roots能直接到的對象。速度很快但是仍存在Stop The World問題
并發標記:進行GC Roots Tracing 的過程,找出存活對象且用戶線程可并發執行
重新標記:為了修正并發標記期間因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄。仍然存在Stop The World問題
并發清除:對標記的對象進行清除回收
CMS收集器的內存回收過程是與用戶線程一起并發執行的
G1收集器
(JDK 9以后默認使用,而且替代了CMS 收集器)
新生代伊甸園垃圾回收—–>內存不足,新生代回收+并發標記—–>回收新生代伊甸園、幸存區、老年代內存——>新生代伊甸園垃圾回收(重新開始)
適用場景
同時注重吞吐量和低延遲(響應時間)
超大堆內存(內存大的),會將堆內存劃分為多個大小相等的區域
整體上是標記-整理算法,兩個區域之間是復制算法
Young Collection (會STW)
(分區算法region)
分代是按對象的生命周期劃分,分區則是將堆空間劃分連續幾個不同小區間,每一個小區間獨立回收,可以控制一次回收多少個小區間,方便控制 GC 產生的停頓時間
Young Collection + CM(并發標記)
在 Young GC 時會對 GC Root 進行初始標記
在老年代占用堆內存的比例達到閾值時,對進行并發標記(不會STW),閾值可以根據用戶來進行設定
Mixed Collection
會對E S O 進行全面的回收:
最終標記
拷貝存活
因為指定了最大停頓時間,如果對所有老年代都進行回收,耗時可能過高。為了保證時間不超過設定的停頓時間,會回收最有價值的老年代(回收后,能夠得到更多內存)
Full GC
G1在老年代內存不足時(老年代所占內存超過閾值)
如果垃圾產生速度慢于垃圾回收速度,不會觸發Full GC,還是并發地進行清理
如果垃圾產生速度快于垃圾回收速度,便會觸發Full GC
Young Collection 跨代引用(老年代引用新生代)
卡表與Remembered Set
Remembered Set 存在于E中,用于保存新生代對象對應的臟卡
臟卡:O被劃分為多個區域(一個區域512K),如果該區域引用了新生代對象,則該區域被稱為臟卡
在引用變更時通過post-write barried + dirty card queue
concurrent refinement threads 更新 Remembered Set
Remark
重新標記階段
在垃圾回收時,收集器處理對象的過程中
黑色:已被處理,需要保留的 灰色:正在處理中的 白色:還未處理的
但是在并發標記過程中,有可能A被處理了以后未引用C,但該處理過程還未結束,在處理過程結束之前A引用了C,這時就會用到remark
過程如下
之前C未被引用,這時A引用了C,就會給C加一個寫屏障,寫屏障的指令會被執行,將C放入一個隊列當中,并將C變為 處理中 狀態
在并發標記階段結束以后,重新標記階段會STW,然后將放在該隊列中的對象重新處理,發現有強引用引用它,就會處理它
類加載與字節碼技術
類加載階段
加載,連接,初始化
加載
將類的字節碼載入方法區(1.8后為元空間,在本地內存中)中,內部采用 C++ 的 instanceKlass 描述 java 類,它的重要 ?eld 有:
_java_mirror 即 java 的類鏡像,例如對 String 來說,它的鏡像類就是 String.class,作用是把 klass 暴露給 java 使用
_super 即父類
_?elds 即成員變量
_methods 即方法
_constants 即常量池
_class_loader 即類加載器
_vtable 虛方法表
_itable 接口方法
如果這個類還有父類沒有加載,先加載父類
加載和鏈接可能是交替運行的
InstanceKlass和*.class(JAVA鏡像類)互相保存了對方的地址
類的對象在對象頭中保存了*.class的地址。讓對象可以通過其找到方法區中的instanceKlass,從而獲取類的各種信息
連接
驗證
驗證類是否符合 JVM規范,安全性檢查
準備
為 static 變量分配空間,設置默認值
static變量在JDK 7以前是存儲與instanceKlass末尾。但在JDK 7以后就存儲在_java_mirror末尾了
static變量在分配空間和賦值是在兩個階段完成的。分配空間在準備階段完成,賦值在初始化階段完成
如果 static 變量是 ?nal 的基本類型,以及字符串常量,那么編譯階段值就確定了,賦值在準備階段完成
如果 static 變量是 ?nal 的,但屬于引用類型,那么賦值也會在初始化階段完成
解析
將常量池中的符號引用解析為直接引用
初始化
初始化階段就是執行類構造器clinit()方法的過程,虛擬機會保證這個類的『構造方法』的線程安全
clinit()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合并產生的
注意
編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之后的變量,在前面的靜態語句塊可以賦值,但是不能訪問,如
發生時機
—類的初始化的懶惰的,以下情況會初始化
main 方法所在的類,總會被首先初始化
首次訪問這個類的靜態變量或靜態方法時
子類初始化,如果父類還沒初始化,會引發
子類訪問父類的靜態變量,只會觸發父類的初始化
Class.forName
new 會導致初始化
—以下情況不會初始化
訪問類的 static ?nal 靜態常量(基本類型和字符串)
類對象.class 不會觸發初始化
創建該類對象的數組
類加載器的.loadClass方法
Class.forNamed的參數2為false時
驗證類是否被初始化,可以看改類的靜態代碼塊是否被執行
類加載器
Java虛擬機設計團隊有意把類加載階段中的“通過一個類的全限定名來獲取描述該類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需的類。實現這個動作的代碼被稱為“類加載器”(ClassLoader)
類與類加載器
類加載器雖然只用于實現類的加載動作,但它在Java程序中起到的作用卻遠超類加載階段
對于任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個Java虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等
啟動類加載器
可通過在控制臺輸入指令,使得類被啟動類加器加載
拓展類加載器
如果classpath和JAVA_HOME/jre/lib/ext 下有同名類,加載時會使用拓展類加載器加載。當應用程序類加載器發現拓展類加載器已將該同名類加載過了,則不會再次加載
雙親委派模式
雙親委派模式,即調用類加載器ClassLoader 的 loadClass 方法時,查找類的規則
首先查找該類是否已經被該類加載器加載過了
如果沒有被加載過
看是否被它的上級加載器加載過了,如果沒有
看是否被啟動類加載器加載過
如果還是沒有找到,先讓拓展類加載器調用findClass方法去找到該類,如果還是沒找到,就拋出異常
然后讓應用類加載器去找classpath下找該類
總結
- 上一篇: 国庆档票房超27亿元 《坚如磐石》居首
- 下一篇: 领跑友商?基于Android 14更新的