JVM是可運行java代碼的假想計算機,包括一套字節碼指令集,一組寄存器,一個棧,一個垃圾回收、堆和一個存儲方法域。JVM是運行在操做系統之上的,它與硬件沒有直接的交互。java
咱們都知道Java代碼源文件,經過編譯器可以產生相應的.Class字節碼文件,而字節碼文件又經過Java虛擬機中的解釋器,編譯成特定機器上的機器碼。c++
① Java源文件 ——> 編譯器 ——> 字節碼文件 ② 字節碼文件 ——> JVM ——> 機器碼程序員
每種平臺的解釋器是不一樣的,可是虛擬機是相同的,這也就是java爲何可以跨平臺的緣由了。當一個程序從開始運行,這時虛擬機就開始實例化了,多個程序 啓動就會存在多個虛擬機實例。 程序退出或者關閉,則虛擬機實例消亡,多個虛擬機實例之間數據不能共享。算法
什麼是類的加載? 類的加載是指將類的字節碼文件數據讀入到內存中,將其放在運行時數據區的方法區內,而後在堆區建立一個java.lang.Class對象,用來封裝類在方法區內的數據結構。 類的加載的最終產品是位於堆區內中的Class對象,Class對象封裝了類在方法區內的數據結構,而且向java程序員提供了訪問方法區內的數據結構的接口。json
類加載器包括:瀏覽器
String str = new String("HelloWorld"); System.out.println(str.getClass().getClassLoader()); //控制檯打印null
擴展類加載器(ExtClassLoader)服務器
——是java代碼實現的,用來加載java安裝目錄下 jre/lib/ext 目錄中的可執行jar包。數據結構
應用程序類加載器(AppClassLoader)多線程
——是java代碼實現的,用來加載用戶編寫的代碼。咱們新建一個類,獲取其類加載器就是AppClassLoaderjvm
public class MyClassLoaderTest { public static void main(String[] args) { String str = new String("HelloWorld"); // 打印null System.out.println(str.getClass().getClassLoader()); // 打印sun.misc.Launcher$AppClassLoader@18b4aac2 System.out.println(MyClassLoaderTest.class.getClassLoader()); // 打印sun.misc.Launcher$ExtClassLoader@4554617c System.out.println(MyClassLoaderTest.class.getClassLoader().getParent()); // 打印null System.out.println(MyClassLoaderTest.class.getClassLoader().getParent().getParent()); } }
由上述代碼可見: AppClassLoader extend ExtClassLoader extend BootstrapClassLoader
爲了防止用戶自定義類與jdk自帶的類衝突,jdk內有雙親委派機制和沙箱機制。
上述過程當中,咱們認識到了類加載器之間的繼承關係。當java在加載類的時候,由AppClassLoader
委派其父類ExtClassLoader
進行加載,ExtClassLoader
會再次委派其父類BootStrapClassLoader
進行加載, 若是BootStrapClassLoader
找到該類那麼加載該類返回該類的Class對象,可是,若是此時BootStrapClassLoader
沒有找到該類, 那麼就須要ExtClassLoader
自身進行加載,若是ExtClassLoader
找到該類那麼加載該類返回該類的Class對象, 可是,若是ExtClassLoader
也沒有找到該類,那麼就要由AppClassLoader
進行加載。 若是最後AppClassLoader
也沒有找到該類,那麼就會拋出 ClassNotFoundException
。
(類加載器沒有向下尋找,沒有getChild只有getParent)
若是你本身定義了一個與jdk自帶類名包名一致的類,那麼java也不會去加載該類。
JVM內存區域主要分爲
生命週期
方法區和堆是全部線程共享的內存區域;而java棧、本地方法棧和程序計數器是運行時線程私有的內存區域。
方法區 主要存放靜態變量,常量,Class類模板(接口定義,構造函數),運行時常量池。
java堆(Heap),是Java虛擬機所管理的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。又被稱做爲運行時數據區。
程序計數器(Program Counter Register),是一塊比較小的內存空間,它的做用能夠看作是當前線程所執行的字節碼的行號指示器。每一個線程都有一個私有的,能夠理解爲它是一個指針,指向方法字節碼地址,用來標記下一個要執行的方法字節碼地址。
JVM棧(JVM Stacks),與程序計數器同樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同,線程結束棧內存也就釋放了,對於棧來講不存在來及回收的問題。主要保存八大基本數據類型的變量、對象的引用變量以及實例方法。虛擬機棧描述的是Java方法執行的內存模型:每一個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於存儲局部變量表、操做棧、動態連接、方法出口等信息。每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
本地方法棧(Native Method Stacks),與c/c++交互的一塊區域,本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務。
Sun JDK監控和故障處理命令有 jps
、 jstat
、 jmap
、 jhat
、 jstack
、 jinfo
經常使用調優工具分爲兩類
設定堆內存大小 -Xmx
:堆內存最大限制。
設定新生代大小。 新生代不宜過小,不然會有大量對象涌入老年代 -XX:NewSize
:新生代大小 -XX:NewRatio
新生代和老生代佔比 -XX:SurvivorRatio
:伊甸園空間和倖存者空間的佔比
設定垃圾回收器 年輕代用 -XX:+UseParNewGC
年老代用-XX:+UseConcMarkSweepGC
遞歸操做,程序沒有出口會一直進行壓棧操做
棧的深度不夠了
邏輯上分爲
物理上分爲
新生區 、 養老區、 永久區
又將新生區分爲了三個區
新new的對象都放在伊甸園區,存活率2%,其餘對象都被垃圾回收器回收 沒有被垃圾回收倖存下來的對象將會保存到倖存者區 當伊甸園區內存不足時,會進行輕量級(minor GC)垃圾回收,將倖存者from區和伊甸園區的還在用的對象移動到倖存者to區, 而後清空倖存者from區和伊甸園區,倖存者from區清空以後會交換from區和to區,保證to區始終是空的。注意from區向to區移動以前會判斷對象的年齡, 若是大於15,直接移動到養老區。年齡計數的原理:垃圾回收器回收一次,倖存活一次加一歲。 若是養老區的內存也不夠用了,就會觸動重量級GC(full GC)將養老區和新生區全量級回收垃圾對象。若是FullGC以後養老區的內存仍是不夠用,那麼會引起OOM。
若是程序一開始就new了一個比伊甸園區大的對象,伊甸園區沒有足夠的空間存放應該如何存放呢?此時會將對象存放到養老區,若是養老區也不夠存儲,那麼會引起OOM。
對象優先分配在Eden區,若是Eden區沒有足夠的空間時,虛擬機執行一次Minor GC。
大對象直接進入老年代(大對象是指須要大量連續內存空間的對象)。這樣作的目的是避免在Eden區和兩個Survivor區之間發生大量的內存拷貝(新生代採用複製算法收集內存)。
長期存活的對象進入老年代。虛擬機爲每一個對象定義了一個年齡計數器,若是對象通過了1次Minor GC那麼對象會進入Survivor區,以後每通過一次Minor GC那麼對象的年齡加1,知道達到閥值對象進入老年區。
動態判斷對象的年齡。若是Survivor區中相同年齡的全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象能夠直接進入老年代。 空間分配擔保。每次進行Minor GC時,JVM會計算Survivor區移至老年區的對象的平均大小,若是這個值大於老年區的剩餘值大小則進行一次Full GC,若是小於檢查HandlePromotionFailure設置,若是true則只進行Monitor GC,若是false則進行Full GC。
在java8中,永久代已經移除了,被「元數據」(元空間)的區域所取代。元空間的本質和永久代相似,元空間與永久代的最大區別在於: **元空間並不在虛擬機中,而是使用本地內存。**所以,默認狀況下,元空間的大小僅受本地內存限制。 類的源數據放入本定內存中,字符串和類的靜態變量放到java堆中,這樣能夠加載多少類的元數據就再也不由MaxPermSize控制,而由系統的實際可用空間來控制。
引用計數法 在java中,引用和對象是有關聯的。若是要操做對象則必須用引用進行。所以,一個簡單的方法就是經過引用計數來判斷一個對象是否能夠回收。簡單來講,即一個對象若是沒有任何與之關聯的引用,即他們的引用計數都不爲0,則說明對象不太可能再被用到,那麼這個對象就是可回收對象。
可達性分析 爲了解決引用計數法的循環引用問題,java使用了可達性分析的方法。經過一系列的「GC roots」對象做爲起點搜索,若是在「GC roots」和一個對象之間沒有可達路徑,則稱該對象是不可達的。
注意 不可達並不等價於可回收對象,不可達對象變爲可回收對象至少要通過兩次標記過程。兩次標記後還是可回收對象,則將面臨回收。
最基礎的垃圾回收算法,分爲兩個階段:標記和清楚。標記階段是標記出來全部要回收的對象,清楚階段回收被標記的對象所佔的空間。
該算法的缺點: 內存碎片化嚴重,垃圾清理完成後,形成不少內存空間不連續。後續可能發生大對象不能找到可利用的問題。
MajorGC使用該算法
MinorGC使用該算法
缺點: 這種算法雖然實現簡單,內存效率高,不易產生碎片,可是最大的問題是能夠用內存被壓縮到了本來的一半。且存活對象增多的話,copying算法的效率也大大下降。
6.1. 新生代與複製算法
目前大部分的JVM的GC對於新生代都採起了copying方法,由於新生代中每次垃圾回收都要回收大部分對象, 即要複製的操做比較少,但一般並非按照1:1來劃分新生代。通常將新生代劃分爲一塊較大的Eden空間和兩個比較小的Surviror空間(FromSpace,ToSpace),每次使用Eden空間和其中的一塊Surivor空間,當進行回收時,將該兩塊空間中還存活的對象複製到另一塊Survivor空間中。
6.2 老年代與標記複製算法 而老年代由於每次只回收少許的對象,所以採用Mark-Compact算法。
JAVA虛擬機提到過的處於方法區的永生帶,它用來存儲class類,常量、方法描述等。對永生代的回收主要包括廢棄常量和無用的類
對象的內存分配主要在新生代的EdenSpace和SurvivorSpace的FormSpace(Survivor目前存放對象的那一塊),少數狀況會直接分配到老生代。
當新生代的EdenSpace和FromSpace空間不足時就會發生一次GC,進行GC後,EdenSpace和FromSpace區的存活對象會被移動到ToSpace,而後將EdenSpace和FromSpace進行清理。
若是ToSpace沒法足夠存儲某個對象,則將這個對象存儲到老生代。
進行GC後,使用的即是EdenSpace和ToSpace了,如此反覆循環。
當對象在Survivor區躲過一次GC後,其年齡就會+1。默認狀況下年齡達到15的對象就會移動到老生代中
在Java中最多見的就是強引用,把一個對象賦值給一個引用變量,這個引用變量就是一個強引用。 當一個對象被強引用變量引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即便該對象之後永遠都不會被用到,JVM也不會回收。所以強引用是形成Java內存泄漏主要緣由之一。
軟引用須要使用SoftReference類來實現,對於只有軟引用的對象來講,當系統內存足夠時他不會被回收,當系統內存足夠用時,它不會被回收,當系統內存不足時它會被回收。軟引用一般用在對內存敏感的程序中。
弱引用須要用WeakReference類來實現,它比軟引用的生存期更短,對於只有弱引用的對象來講,只要垃圾回收機制一運行,無論JVM的內存空間足夠,總會回收該對象佔用的內存。
**虛引用須要PhantomReference類來實現,它不能單獨使用,必須和引用隊列聯合使用。**虛引用的主要做用是跟蹤對象被垃圾回收的狀態。