JVM記憶體模型篇
1、JVM的位置
包含在JRE中,在作業系統之上。
-
結構模型圖:
-
JDK7和JDK8的區別
2、JAVA程式執行順序
-
.java原始碼檔案
-
通過編譯器編譯為位元組碼.class檔案
-
通過類載入器(class loader)把位元組碼檔案載入到記憶體當中
-
通過位元組碼校驗器傳遞給直譯器
-
直譯器對位元組碼進行逐行翻譯,翻譯為系統可以理解的機器碼
-
將機器碼交給作業系統,作業系統以main方法作為入口開始執行程式。
3、類載入器
作用:載入位元組碼.class檔案。
3.1、根載入器(Bootstart Class Loader)
用來載入java的核心類,由C++實現,不是java.lang.ClassLoader的子類。負責載入$JAVA_HOME中jre/lib/rt.jar裡的所有class。
3.2、擴充套件類載入器(Extensions Class Loader)
負責載入JRE的擴充套件目錄,lib/ext或者由java.ext.dirs系統屬性指定的目錄中的JAR包的類。由JAVA語言實現。
3.3、系統(應用)類載入器(System Class Loader)
負責在JVM啟動時載入來自java命令的-classpath選項、java.class.path系統屬性,或者$CLASSPATH將變數所指定的JAR包和類路徑。沒有特別指定,則使用者自定義的類載入器都以此類載入器作為附加在其。由java語言實現,父類載入器為ExtClassLoader。
4、雙親委派機制
說明:一個類載入器收到了類載入請求並不會自己先去載入,而是把這個請求委託給父類載入器去執行,父類載入器若還存在其父類載入器則依次向上委託,直到到達頂層啟動類載入器。若父類載入器可以完成類載入任務,就直接返回,否者子載入器才會嘗試自己去載入。
優勢:具備了優先順序的層級關係,通過這種層級關係可以避免類的重複載入,當父類載入器已經載入了該類時,就沒必要讓子類載入器再載入一次了。其次,考慮到安全因素,java核心api中定義的類不會被隨意替換,當出現自定義的類與java核心api中的類型別一樣時,通過雙親委派機制傳遞到啟動類載入器,而啟動類載入器在java核心api中發現同類型的類已經被載入,就不會載入自定義的類,而是直接返回已經載入了的java核心api中的類,這樣便可以防止java核心api庫被隨意篡改了。
5、沙箱安全機制
將java程式碼限定在jvm特定的執行範圍中,並且嚴格限制程式碼對本地系統資源訪問,通過這樣的措施來保證對程式碼的有效隔離,防止對本地系統造成破壞。
6、Native
用作java和其他語言(如C++)進行協作時用的。
由於java是跨平臺的語言,所以就犧牲了一些對系統底層的控制,而要實現對底層的控制,就需要一些其他語言的幫助,這就是native的作用了。
7、 程式計數器(PC暫存器)
在jvm中,多執行緒是通過執行緒輪流切換來獲取CPU執行時間的,因此,在任一具體時刻,一個CPU的核心只會執行一條執行緒中的指令。因此,為了能夠使得每個執行緒線上程切換後能夠恢復在切換之前的程式執行位置,每個執行緒都需要有自己獨立的程式計數器,並且互不干擾,否則就會影響到程式的正常執行次序。因此,可以這麼說,程式計數器是每個執行緒私有的。由於程式計數器中儲存的資料所佔空間的大小不會隨程式的執行而發生改變,因此,對於程式計數器是不會發生記憶體溢位(OutMemory)現象的。
8、方法區
執行緒共享的區域。儲存了每個類的資訊(包括類的名稱、方法資訊、欄位資訊)、靜態變數、常量以及編譯器編譯後的程式碼等。
在Class檔案中除了類的欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用來儲存編譯期間生成的字面量和符號引用。
在方法區中有一個非常重要的部分就是執行時常量池,它是每一個類或介面的常量池的執行時表示形式,在類和介面被載入到JVM後,對應的執行時常量池就被創建出來。當然並非Class檔案常量池中的內容才能進入執行時常量池,在執行期間也可將新的常量放入執行時常量池中。
9、棧
作為資料結構具有先進後出的一個特性,與之該對應的結構是佇列(先進先出)。
存放的是一個個的棧幀,每個棧幀對應一個被呼叫的方法,在棧幀中包括區域性變量表(Local Variables)、運算元棧(Operand Stack)、
指向當前方法所屬的類的執行時常量池(執行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、
方法返回地址(Return Address)和一些額外的附加資訊。當執行緒執行一個方法時,就會隨之建立一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧。
其實大都存放的一些具體內容的引用(比如物件和例項方法)。
棧是執行緒級別的。對於棧來說,不存在垃圾回收的問題,因為執行緒結束,棧記憶體就釋放。
10、堆
堆是Java虛擬機器所管理的記憶體中最大的一塊儲存區域。堆記憶體被所有執行緒共享。主要存放使用new關鍵字建立的物件。所有物件例項以及陣列都要在堆上分配。垃圾收集器就是根據GC演算法,收集堆上物件所佔用的記憶體空間(收集的是物件佔用的空間而不是物件本身)。
-
堆調優設定常用引數
引數 | 描述 |
---|---|
-Xms | 堆記憶體初始大小 |
-Xmx(MaxHeapSize) | 堆記憶體最大允許大小,一般不要大於實體記憶體的80% |
-XX:NewSize(-Xns) | 年輕代記憶體初始大小 |
-XX:MaxNewSize(-Xmn) | 年輕代記憶體最大允許大小,也可以縮寫 |
-XX:NewRatio | 新生代和老年代的比值 |
-XX:SurvivorRatio=8 | 值為4 表示 新生代:老年代=1:4,即年輕代佔堆的1/5年輕代中Eden區與Survivor區的容量比例值,預設為8 |
-XX:+HeapDumpOnOutOfMemoryError | 表示兩個Survivor :eden=2:8,即一個Survivor佔年輕代的1/10記憶體溢位時,匯出堆資訊到檔案 |
-XX:+HeapDumpPath | 堆Dump路徑-Xmx20m -Xms5m-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=d:/a.dump |
-XX:OnOutOfMemoryError | 當發生OOM記憶體溢位時,執行一個指令碼-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p%p表示執行緒的id pid |
-XX:MaxTenuringThreshold=7 | 表示如果在倖存區移動多少次沒有被垃圾回收,進入老年代 |
10.1、新生區
新生區主要用來存放新生的物件。一般佔據堆空間的1/3。在新生代中,儲存著大量的剛剛建立的物件,但是大部分的物件都是朝生夕死,所以在新生代中會頻繁的進行MinorGC,進行垃圾回收。新生代又細分為三個區:Eden區、SurvivorFrom、ServivorTo區,三個區的預設比例為:8:1:1。
-
Eden區:Java新建立的物件絕大部分會分配在Eden區(如果物件太大,則直接分配到老年代)。當Eden區記憶體不夠的時候,就會觸發MinorGC(新生代採用的是複製演算法),對新生代進行一次垃圾回收。
-
SurvivorFrom區和To區:在GC開始的時候,物件只會存在於Eden區和名為From的Survivor區,To區是空的,一次MinorGC過後,Eden區和SurvivorFrom區存活的物件會移動到SurvivorTo區中,然後會清空Eden區和SurvivorFrom區,並對存活的物件的年齡+1,如果物件的年齡達到15,則直接分配到老年代。MinorGC完成後,SurvivorFrom區和SurvivorTo區的功能進行互換。下一次MinorGC時,會把SurvivorTo區和Eden區存活的物件放入SurvivorFrom區中,並計算物件存活的年齡。
10.2、老年區
老年區主要存放應用中生命週期長的記憶體物件。老年區比較穩定,不會頻繁的進行MajorGC。而在MaiorGC之前才會先進行一次MinorGc,使得新生的物件進入老年代而導致空間不夠才會觸發。當無法找到足夠大的連續空間分配給新建立的較大物件也會提前觸發一次MajorGC進行垃圾回收騰出空間。
在老年區中,MajorGC採用了標記—清除演算法:首先掃描一次所有老年代裡的物件,標記出存活的物件,然後回收沒有標記的物件。MajorGC的耗時比較長。因為要掃描再回收。MajorGC會產生記憶體碎片,當老年代也沒有記憶體分配給新來的物件的時候,就會丟擲OOM(Out of Memory)異常。
10.3、永久區(方法區)
永久區在邏輯上是和堆分開的,但物理上永久代是屬於堆的。永久代指的是永久儲存區域。主要存放Class和Meta(元資料)的資訊。Class在被載入的時候被放入永久區域,它和存放的例項的區域不同。
注意:堆=新生代+老年代,不包括永久代(方法區)。
10.4、JDK7和JDK8的區別
在jdk1.7中開始了為對方法區的移除做準備,jdkk1.7中將常量池移到堆中。
在jdk1.8中徹底移除了方法區,增加元空間MetaSpace(其作用與方法區相似)
在Java8中,永久區已經被移除,取而代之的是一個稱之為“元資料區”(元空間)的區域。元空間和永久區類似,都是對JVM中規範中方法的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體的限制。類的元資料放入native memory,字串池和類的靜態變數放入java堆中。這樣可以載入多少類的元資料就不再由MaxPermSize控制,而由系統的實際可用空間來控制。
採用元空間而不用永久區的原因:
-
為了解決永久代的OOM問題,元資料和class物件存放在永久代中,容易出現效能問題和記憶體溢位。
-
類及方法的資訊等比較難確定其大小,因此對於永久代大小指定比較困難,大小容易出現永久代溢位,太大容易導致老年代溢位(堆記憶體不變,此消彼長)。
-
永久代會為GC帶來不必要的複雜度,並且回收效率偏低。
11、GC垃圾回收器
11.1、三種GC方式
11.1.1、Minor GC
清理新生區。
11.1.2、Major GC
清理老年區。
11.1.3、Full GC
清理整個堆記憶體,包括新生區和老年區。
11.2、GC演算法
11.2.1、判斷物件是否存活的演算法
11.2.1.1、引用計數法
在一個物件被引用時+1,被去除引用時-1,當計數器值為零說明物件沒地方被使用則視為垃圾。Java語言判斷物件是否存活不是用的這樣的方法,因為有個致命的問題就是:當兩個物件相互引用的時候,而這兩個物件在其它任何地方都沒有被引用時,這兩個物件就不會被垃圾回收掉。
11.2.1.2、根搜尋演算法
實有點像一種樹的資料結構,通過一系列名為“GC Roots”的物件(這類物件在下面會給出)作為起始點(根),從這些起點向下搜尋,搜尋的路徑稱為引用鏈,在這條鏈中能搜尋到的物件就代表是存活不會被回收的,當某個物件與這條鏈中不相連的時候就代表該物件需要被回收了,可以看下面的圖,白色區塊的幾個物件是將要被回收掉的。
能作為“GC Roots”的物件包括以下幾種:
-
虛擬機器棧(棧幀中的本地變量表)中的引用的物件。
-
方法區中的類靜態屬性引用的物件。
-
方法區中的常量引用的物件。
-
本地方法棧中JNI(即一般說的Native方法)的引用的物件。
11.2.2、垃圾回收演算法
11.2.2.1、標記清除演算法(適用老年代)
它將垃圾回收分為兩個階段:標記階段和清除階段。
標記可存活物件,清除沒有被標記的物件。
11.2.2.2、複製演算法(適合年輕代)
11.2.2.2、複製演算法(適合年輕代)
將記憶體分為兩部分,每次只使用其中一部分。在垃圾回收時,將正在使用的記憶體中的存活物件複製到未使用的記憶體塊中,之後清除正在使用的記憶體塊中的所有物件,交換兩個記憶體的角色,完成垃圾回收。(From區與To區)
11.2.2.3、標記整理演算法(適合老年代)
對比於標記清除演算法,在清除階段前,它會將所有的存活物件移動到記憶體的另一端進行一個整理。之後清理存活物件端邊界之外的空間。
本文來自部落格園,作者:是老胡啊,轉載請註明原文連結:https://www.cnblogs.com/jetty-9527/p/15906434.html