JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用於計算設備的規范,它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。Java語言的一個非常重要的特點就是與平台的無關性。而使用Java虛擬機是實現這一特點的關鍵。一般的高級語言如果要在不同的平台上運行,至少需要編譯成不同的目標代碼。而引入Java語言虛擬機后,Java語言在不同平台上運行時不需要重新編譯。Java語言使用Java虛擬機屏蔽了與具體平台相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平台上不加修改地運行。Java虛擬機在執行字節碼時,把字節碼解釋成具體平台上的機器指令執行。這就是Java的能夠“一次編譯,到處運行”的原因。
1.JVM的生命周期:
*啟動。啟動一個Java程序時,一個JVM實例就產生了,任何一個擁有public static void main(String[] args)函數的class都可以作為JVM實例運行的起點。
*運行。main()作為該程序初始線程的起點,任何其他線程均由該線程啟動。
*消亡。當程序中的所有非守護線程都終止時,JVM才退出;若安全管理器允許,程序也可以使用Runtime類或者System.exit()來退出。
當在電腦上運行一個程序時,就會運行一個java虛擬機,java虛擬機總是開始於main方法,main方法是程序的起點。
java的線程一般分為兩種:守護線程和普通線程。守護線程是java虛擬機自己使用的線程,比如GC線程就是一個守護線程,當然你可以把自己的線程設置為守護線程,注意:main方法啟動的初始線程不是守護線程。
只要java虛擬機中還有普通線程在執行,java虛擬機就不會停止,如果有足夠的權限,你可以調用exit()方法終止線程。
2.JVM的體系結構:
1) 類裝載器(ClassLoader)(用來裝載.class文件)
2) 執行引擎(執行字節碼,或者執行本地方法)
3) 運行時數據區(方法區、堆、虛擬機棧、程序計數器、本地方法棧)
首先我們對運行時數據區中的5個區域進行分析:
3. 運行時數據區:
3.1 堆:
所有線程共享的內存區域,在虛擬機啟動時創建。
用來存儲對象實例,如:String a = new String()中new String()創建了一個對象,該對象存放在堆內存中,而a 是存放在棧中的,堆中new String() 存放了棧中 a 的內存地址。
可以通過-Xmx和-Xms控制堆的大小
當在堆中沒有內存完成實例分配,且堆也無法再擴展時,會報OutOfMemoryError異常。
java堆是垃圾回收器的主要工作區域。java對還可以分為新生代、老年代。但是垃圾回收器的永久代是在方法區中的,不在堆中。
(新生代:新建的對象由新生代分配內存;老年代:存放經過多次垃圾回收器回收仍然存活的對象;永久代:存放靜態文件,如java類、方法等,永久代存放在方法區,對垃圾回收沒有顯著的影響)
3.1.1 新生代:
分為三部分:1個Eden區和2個Survivor區(分別叫from和to)。默認比例為8:1。一般情況下,新創建的對象都會被分配到Eden區(一些大對象特殊處理),這些對象經過第一次Minor GC后,如果仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中。
因為年輕代中的對象基本都是朝生夕死的(80%以上),所以在年輕代的垃圾回收算法使用的是復制算法,復制算法的基本思想就是將內存分為兩塊,每次只用其中一塊,當這一塊內存用完,就將還活着的對象復制到另外一塊上面。復制算法不會產生內存碎片。
在GC開始的時候,對象只會存在於Eden區和名為“From”的Survivor區,Survivor區“To”是空的。緊接着進行GC,Eden區中所有存活的對象都會被復制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被復制到“To”區域。(動態判斷對象的年齡。如果Survivor區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代。)經過這次GC后,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重復這樣的過程,直到“To”區被填滿,“To”區被填滿之后,會將所有對象移動到年老代中。
GC可分為三種:Minor GC Major GC 和 Full GC
Minor GC :是清理新生代。觸發條件:當Eden區滿時,觸發Minor GC。
Major GC:是清理老年代。是 Major GC 還是 Full GC,大家應該關注當前的 GC 是否停止了所有應用程序的線程,還是能夠並發的處理而不用停掉應用程序的線程。
Full GC :是清理整個堆空間—包括年輕代和老年代。觸發條件:調用System.gc時,系統建議執行Full GC,但是不必然執行;老年代空間不足;方法區空間不足;通過Minor GC后進入老年代的平均大小大於老年代的可用內存;由Eden區、From Space區向To Space區復制時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
3.1.2 老年代:
主要存放應用程序中生命周期長的內存對象。
老年代的對象比較穩定,所以MajorGC不會頻繁執行。在進行MajorGC前一般都先進行了一次MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次MajorGC進行垃圾回收騰出空間。
MajorGC采用標記—清除算法:首先掃描一次所有老年代,標記出存活的對象,然后回收沒有標記的對象。MajorGC的耗時比較長,因為要掃描再回收。MajorGC會產生內存碎片,為了減少內存損耗,我們一般需要進行合並或者標記出來方便下次直接分配。
當老年代也滿了裝不下的時候,就會拋出OOM(Out of Memory)異常。
3.1.3 永久代(永久代是在方法區中的,而不在堆中,這里只是為了總結GC的運行機制並和新生代、老年代進行比較才將永久代放在這里寫):
指內存的永久保存區域,主要存放Class和Meta(元數據)的信息,Class在被加載的時候被放入永久區域. 它和存放實例的區域不同,GC不會在主程序運行期對永久區域進行清理。所以這也導致了永久代的區域會隨着加載的Class的增多而脹滿,最終拋出OOM異常。
在Java8中,永久代已經被移除,被一個稱為“元數據區”(元空間)的MetaSpace區域所取代。元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。類的元數據放入 native memory, 字符串池和類的靜態變量放入java堆中. 這樣可以加載多少類的元數據就不再由MaxPermSize控制, 而由系統的實際可用空間來控制.采用元空間取代永久代的原因:(1)為了解決永久代的OOM問題,元數據和class對象存在永久代中,容易出現性能問題和內存溢出。(2)類及方法的信息等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出(因為堆空間有限,此消彼長)。(3)永久代會為 GC 帶來不必要的復雜度,並且回收效率偏低(4)Oracle 可能會將HotSpot 與 JRockit 合二為一。
3.2 方法區:
所有線程共享
用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
當方法區無法滿足內存的分配需求時,報OutOfMemoryError異常
方法區中有一個運行時常量池,用於存儲編譯期生成的各種字面量與符號引用,當常量池無法再申請到內存時報OutOfMemoryError異常。
3.3 虛擬機棧:
線程私有,聲明周期與線程同步。
存儲一些方法的局部變量表(基本類型、對象引用)、操作數棧、動態鏈接、方法出口等信息。
每個虛擬機線程都有一個私有的棧,一個線程的java棧在線程創建的時候被創建。
每個方法執行的同時都會創建一個棧幀,每個方法被調用直至完成的過程,就對應着一個棧幀在虛擬機中從入棧到出棧的過程。
當線程請求的棧深度大於虛擬機允許的深度時報StackOverFlowError異常。
當棧的擴展無法申請到足夠的內存時報OutOfMemoryError異常。
3.4 本地方法棧:
主要是為虛擬機使用到的Native方法服務,Native 方法就是一個java調用非java代碼的接口,該方法的實現由非java語言實現。Native方法用native修飾,沒有方法體,因為方法體中的實現是非java語言的。
有時java需要調用操作系統的一些方法,而操作系統基本都是C語言寫的,這時就需要使用到Native方法了。
Native方法關鍵字修飾的方法是一個原生態的方法,方法對應的實現不是在當前文件,而是在用其他語言(如C和C++)實現的文件中。java語言本身不能對操作系統底層進行訪問和操作,但是可以通過JNI(Java Native Interface)接口調用其他語言來實現對底層的訪問。
3.5 程序計數器:
當前線程所執行的字節碼的行號指示器,當前線程私有,由於他只是存儲行號,一般就是一個數字,所以不會出現OutOfMemoryError異常。
其特點是:如果正在執行java方法,則這個計數器記錄的是正在執行的虛擬機字節碼指令地址,如果正在執行Native方法,則這個計數器為空(undefined),此內存區域是唯一一個在java虛擬機中沒有規定任何OutOfMemoryError異常情況的區域。
使用場景:A線程先獲取CPU時間片執行,當執行到一半的時候,B線程過來了,且優先級比A線程的高,所以處理器又去執行B線程了,把A線程掛起,當B線程執行完了以后,再回過頭來執行A線程,這時就需要知道A線程已經執行的位置,也就是查看A中的程序計數器中的指令。
總結:java對象存放在堆中,常量存放在方法區的常量池中,虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據放在方法區,以上區域都是線程共享的。棧是線程私有的,存放該方法的局部變量(基本類型、對象引用)操作數棧、動態鏈接、方法出口等信息。一個java程序對應一個JVM,一個方法對應一個java棧。
4.1 垃圾收集器的種類:
4.1.1 Serial 收集器:
這個收集器是一個單線程的收集器,但它的單線程的意義不僅僅說明它會只使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。串行垃圾回收器是為單線程環境而設計的,如果你的程序不需要多線程,啟動串行垃圾回收。串行收集器是最古老,最穩定以及效率高的收集器,可能會產生較長的停頓,只使用一個線程去回收。新生代、老年代使用串行回收;新生代復制算法、老年代標記-壓縮;垃圾收集的過程中會Stop The World(服務暫停)
4.1.2 ParNew收集器:
ParNew收集器其實就是Serial收集器的多線程版本。新生代並行,老年代串行;新生代復制算法、老年代標記-壓縮
4.1.3 Parallel 收集器:
Parallel 收集器類似ParNew收集器,Parallel收集器更關注系統的吞吐量。可以通過參數來打開自適應調節策略,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大的吞吐量;也可以通過參數控制GC的時間不大於多少毫秒或者比例;新生代復制算法、老年代標記-壓縮
4.1.4 Parallel Old 收集器:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器是在JDK 1.6中才開始提供
4.1.5 CMS收集器:
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用都集中在互聯網站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。
運作過程相對於前面幾種收集器來說要更復雜一些,整個過程分為4個步驟,包括:
初始標記(CMS initial mark)
並發標記(CMS concurrent mark)
重新標記(CMS remark)
並發清除(CMS concurrent sweep)
其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,並發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是為了修正並發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。
由於整個過程中耗時最長的並發標記和並發清除過程中,收集器線程都可以與用戶線程一起工作,所以總體上來說,CMS收集器的內存回收過程是與用戶線程一起並發地執行。老年代收集器(新生代使用ParNew)
優點:並發收集、低停頓
缺點:產生大量空間碎片、並發階段會降低吞吐量
4.1.6 G1收集器:
空間整合,G1收集器采用標記整理算法,不會產生內存空間碎片。分配大對象時不會因為無法找到連續空間而提前觸發下一次GC。
可預測停頓,這是G1的另一大優勢,降低停頓時間是G1和CMS的共同關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為N毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特征了。
上面提到的垃圾收集器,收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存布局與其他收集器有很大差別,它將整個Java堆划分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,它們都是一部分(可以不連續)Region的集合。
G1的新生代收集跟ParNew類似,當新生代占用達到一定比例的時候,開始出發收集。和CMS類似,G1收集器收集老年代對象會有短暫停頓。
5.1 java源碼編譯機制:
java源碼是不能被機器識別的,需要經過編譯器編譯成JVM可以執行的.class字節碼文件,再由解釋器編譯運行,即:Java源文件(.java) -- Java編譯器 --> Java字節碼文件 (.class) -- Java解釋器 --> 執行。流程圖如下:
java中字符只以Unicode存在,字符轉換發生在JVM和OS交界處
5.2 類加載機制(ClassLoader):
java程序並不是一個可執行文件,是由多個獨立的類文件組成。這些類文件並不是一次性全部裝入內存,而是依據程序逐步載入。
JVM的類加載是通過ClassLoader及其子類來完成的,累的層次關系和加載順序可以由下圖來描述:
1)Bootstrap ClassLoader
是JVM的根ClassLoader,由C++實現;加載Java的核心API:$JAVA_HOME中jre/lib/rt.jar中所有class文件的加載,這個jar中包含了java規范定義的所有接口以及實現;JVM啟動的時候就開始初始化此ClassLoader。
2)Extension ClassLoader
加載java擴展API(lib/ext中的類)
3)App ClassLoader
加載Classpath目錄下定義的class
4)Custom ClassLoader
屬於應用程序根據自身需要自定義的ClassLoader,如tomcat、Jboss都是會根據J2EE規范自行實現ClassLoader
注意:加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視為已加載此類,保證此類只所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。
雙親委派機制
JVM在加載類時默認采用的是雙親委派機制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸。如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
作用:1)避免重復加載;2)更安全。如果不是雙親委派,那么用戶在自己的classpath編寫了一個java.lang.Object的類,那就無法保證Object的唯一性。所以使用雙親委派,即使自己編寫了,但是永遠都不會被加載運行。
破壞雙親委派機制
雙親委派機制並不是一種強制性的約束模型,而是Java設計者推薦給開發者的類加載器實現方式。
線程上下文類加載器,這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過的話,那么這個類加載器就是應用程序類加載器。像JDBC就是采用了這種方式。這種行為就是逆向使用了加載器,違背了雙親委派模型的一般性原則。
參考文檔:
https://www.cnblogs.com/IUbanana/p/7067362.html
https://blog.csdn.net/leaf_0303/article/details/78953669