前言:Jvm 整體組成
Jvm由4個部分
組成,分為2個子系統(tǒng)和2個組件,2個子系統(tǒng)為Class loader(類裝載)、Execution engine(執(zhí)行引擎);2個組件為Runtime Data Area(運行時數(shù)據(jù)區(qū))、Native Interface(本地接口)。
- Class loader(類加載器):根據(jù)給定的全限定名類名(如:Java.lang.Object)來裝載class文件到Runtime data area中的method area。
- Runtime Data Area(運行時數(shù)據(jù)區(qū)域):這就是我們常說的Jvm的內(nèi)存。
- Execution Engine(執(zhí)行引擎) :執(zhí)行classes中的指令。
- Native Interface(本地接口) :與native libraries交互,是其它編程語言交互的接口。
各個組成部分的用途:
- 首先通過編譯器把
Java 代碼轉(zhuǎn)換成字節(jié)碼
(class文件) -
類加載器(ClassLoader) 再把字節(jié)碼加載到內(nèi)存中將其放在 運行時數(shù)據(jù)區(qū)(Runtime data area) 的
方法區(qū)
內(nèi) - 字節(jié)碼文件只是 Jvm 的一套
指令集規(guī)范
,并不能直接交給底層操作系統(tǒng)
去執(zhí)行,因此需要特定的命令解析器執(zhí)行引擎(Execution Engine),將字節(jié)碼翻譯成底層系統(tǒng)指令,再交由CPU 去執(zhí)行
,而這個過程中需要調(diào)用其他語言的 本地庫接口(Native Interface) 來實現(xiàn)整個程序的功能。
一.JDK的內(nèi)存區(qū)域變遷
HotSpot虛擬機是是Sun/OracleJDK和OpenJDK中的默認Java虛擬機,是JVM應(yīng)用最廣泛的一種實現(xiàn)。
-
JDK1.6
時期和我們上面講的JVM內(nèi)存區(qū)域是一致的: -
JDK1.7
時發(fā)生了一些變化,將字符串常量池、靜態(tài)變量,存放在堆上
- 在
JDK1.8
時徹底干掉了方法區(qū)
,而在直接內(nèi)存
中劃出一塊區(qū)域作為元空間
,運行時常量池、類常量池都移動到元空間
。
-
JVM 運行時數(shù)據(jù)區(qū)的 5 個部分中,只有
Java 堆、元空間
是線程共享的,其他三個均為線程私有
Java8虛擬機啟動參數(shù)
-Xms設(shè)置堆的最小空間大小。
堆中 年輕代和年老默認有個比如 是 NewRatio = 2 (默認是 2:1)
年輕代中eden和suvivor默認有個比例 8:1:1 (SurvivorRatio = 8) jps查看進程 jmap -heap 進程編號 查看到改參數(shù)
-Xmx 設(shè)置堆的最大空間大小。
-XX:NewSize 設(shè)置年輕代最小空間大小。
-XX:MaxNewSize 設(shè)置年輕代最大空間大小。
-XX:PermSize 設(shè)置永久代最小空間大小。
-XX:MaxPermSize 設(shè)置永久代最大空間大小。
-Xss 設(shè)置每個線程的堆棧大小 (64位 默認是1M -XX:ThreadStackSize默認是0)。
-Xms:JVM啟動時申請的初始Heap值,默認為操作系統(tǒng)物理內(nèi)存的1/64,例如-Xms20m
-Xmx:JVM可申請的最大Heap值,默認值為物理內(nèi)存的1/4,例如-Xmx20m,我們最好將 -Xms 和 -Xmx 設(shè)為相同值,避免每次垃圾回收完成后JVM重新分配內(nèi)存;
-Xmn:設(shè)置新生代的內(nèi)存大小,-Xmn 是將NewSize與MaxNewSize設(shè)為一致,我們也可以分別設(shè)置這兩個參數(shù)
-XX:PermSize 設(shè)置最小空間
-XX:MaxPermSize 設(shè)置最大空間
-XX:MetaspaceSize :分配給類元數(shù)據(jù)空間(以字節(jié)計)的初始大小。MetaspaceSize的值設(shè)置的過大會延長垃圾回收時間。垃圾回收過后,引起下一次垃圾回收的類元數(shù)據(jù)空間的大小可能會變大。
-XX:MaxMetaspaceSize:分配給類元數(shù)據(jù)空間的最大值,超過此值就會觸發(fā)Full GC,此值默認沒有限制,但應(yīng)取決于系統(tǒng)內(nèi)存的大小。JVM會動態(tài)地改變此值。
-XX:MinMetaspaceFreeRatio:表示一次GC以后,為了避免增加元數(shù)據(jù)空間的大小,空閑的類元數(shù)據(jù)的容量的最小比例,不夠就會導(dǎo)致垃圾回收。
-XX:MaxMetaspaceFreeRatio:表示一次GC以后,為了避免增加元數(shù)據(jù)空間的大小,空閑的類元數(shù)據(jù)的容量的最大比例,不夠就會導(dǎo)致垃圾回收。
二.堆
- 通常需要程序員調(diào)試分析的區(qū)域就是“運行時數(shù)據(jù)區(qū)”,或者更具體的來說就是“運行時數(shù)據(jù)區(qū)”里面的
Heap(堆)模塊
0.堆的概念
在虛擬機啟動時創(chuàng)建 , 堆是被所有線程共享
的最大的一塊內(nèi)存
,幾乎所有的對象實例都在這里分配內(nèi)存(并不是絕對);
- 特點:線程共享
-
異常規(guī)定: 如果在堆中沒有內(nèi)存完成實例分配,并且堆不可以再擴展時,將會拋出OutOfMemoryError。 通過
-Xmx
和-Xms
控制堆大小
1.堆的內(nèi)存分區(qū)
根據(jù)Java回收機制的不同,Java堆有可能擁有不同的結(jié)構(gòu)。最為常見的一種構(gòu)成是將整個java堆分為年輕代和老年代
。其中年輕代存放新生對象或者年齡不大的對象,老年代則存放老年對象。
-
年輕代有分為Eden區(qū)、s0區(qū)、s1區(qū),s0區(qū)和s1區(qū)也被稱為
from和to區(qū)
,他們是兩塊大小相同、可以互換角色
的內(nèi)存空間。-
結(jié)構(gòu):年輕代(Eden區(qū)+2個Survivor區(qū)) 老年代 永久代(HotSpot有)
-
在絕大多數(shù)情況下,對象首先分配在Eden區(qū)
,在一次年輕代回收之后,如果對象還存活,則進入s0或者s1
,每經(jīng)過一次年輕代回收,對象如果存活,它的年齡就會加1
。當對象的年齡達到一定閥值后,就會被認為是老年對象
,從而進入老年代
。
年輕代:新創(chuàng)建的對象——>Eden區(qū)
- GC的時候會將
Eden中存活的對象復(fù)制
?個空的 Survivor中
,并把當前的 Eden和正在使 的Survivor中的不可達對象 清除掉- 再次GC同上,也是將Eden、Survivor存活對象轉(zhuǎn)移到另一個一個空的Survivor中,然后清理剩余的不可達對象
老年代:對象如果在年輕代存活了足夠長的時間而沒有被清理掉
(即在幾次Young GC
后存活了下來),則會被復(fù)制到老年代
- 如果新創(chuàng)建對象比較大(比如
長字符串或大數(shù)組
),且年輕代空間不足
,則大對象會直接分配到老年代上(大對象可能觸發(fā)提前GC,應(yīng)少用,更應(yīng)避免使用短命的大對象) -
老年代的空間一般比年輕代大,能存放更多的對象
,在老年代上發(fā)生的GC次數(shù)也比年輕代少
永久代:可以簡單理解為方法區(qū)(本質(zhì)上兩者并不等價)
- JDK1.6及之前:常量池分配在永久代
- JDK1.7:有,但已經(jīng)逐步“去永久代”
- JDK1.8及之后:沒有永久代(
Java.lang.OutOfMemoryError: PermGen space
,這種錯誤將不會出現(xiàn)在JDK1.8中),通過使用本地內(nèi)存的元空間
來代替永久代
2.堆與GC
2.1.堆的分代結(jié)構(gòu)
- 所有的
對象
和它們相應(yīng)的實例變量
以及數(shù)組
將被存儲在這里。每個Jvm同樣只有一個堆區(qū)。由于方法區(qū)和堆區(qū)的內(nèi)存由線程共享
,所以存儲的數(shù)據(jù)是非線程安全
的。
堆由年輕代和老年代組成,年輕代又分為Eden區(qū)
和survivor(幸存)區(qū),survivor區(qū)
中又有from
區(qū)和to
區(qū).
-
new出來的對象一般都放在Eden區(qū),那當Eden區(qū)滿了之后呢?
- 假設(shè)通過參數(shù)給堆分配
600M內(nèi)存
,那么老年代默認是占2/3
的,也就是差不多400M
,那年輕代就是200M
,Eden區(qū)160M
,Survivor區(qū)40M
。
- 假設(shè)通過參數(shù)給堆分配
2.2.堆的分代GC
一個程序只要在運行,那么就不會不停的new對象,那么總有一刻Eden區(qū)會放滿
,那么一旦Eden區(qū)被放滿之后,虛擬機會干什么呢?
- 沒錯,就是gc,不過這里的gc屬于
minor(咪呢) gc
,就是垃圾收集,來收集垃圾對象并清理的,那么什么是垃圾對象呢?
這里就涉及到了一個GC Root根以及可達性分析算法的概念,也是面試偶爾會被問到的。
-
可達性分析算法
是將GC Roots對象作為起點,從這些起點開始向下搜索引用的對象
,找到的對象都標記為非垃圾對象
,其余未標記的都是垃圾對象
。
加粗樣式那么GC Roots根對象又是什么呢?
-
GC Roots根就是判斷一個對象是否可以回收的依據(jù),只要能通過GC Roots根向下一直搜索能搜索到的對象,那么這個對象就不算垃圾對象,而可以作為GC Roots根的如:
線程棧的本地變量、靜態(tài)變量、本地方法棧的變量等等它們引用的對象
,說白了就是找到和根節(jié)點有聯(lián)系的對象就是有用的對象,其余都認為是垃圾對象來回收
。
- 經(jīng)歷了第一次
minor gc
后,沒有被清理的對象就會被移到From區(qū)
,如上圖。
- 上面在說對象組成的時候有寫到,在對象頭的Mark Word中有
存儲GC分代年齡
,一個對象每經(jīng)歷一次gc,那么它的gc分代年齡就會+1,如上圖。
-
那么如果
第2次
新的對象又把Eden區(qū)放滿了,那么又會執(zhí)行minor gc
,但是這次會連著From區(qū)一起gc
,然后將Eden區(qū)
和From區(qū)
存活的對象都移到To區(qū)域
,對象頭中分代年齡都+1
,如上圖。 -
那么當
第3次
Eden區(qū)又滿的時候,minor gc
就是回收Eden區(qū)
和To
區(qū)域了,TEden區(qū)和To區(qū)域
還活著的對象就會都移到From區(qū)
,如上圖。- 說白了就是
Survivor區(qū)中總有一塊區(qū)域是空著的
,存活的對象存放是在From區(qū)和To區(qū)輪流存放,也就是互相復(fù)制拷貝,這也就是垃圾回收算法中的復(fù)制-回收算法
。
- 說白了就是
如果一個對象經(jīng)歷了一個15次gc
的時候,就會移至老年代。如果還沒有到最大年齡且From區(qū)或者To區(qū)域也慢了,就會直接移到老年代
,這只是舉例了兩種常規(guī)規(guī)則,還有其他規(guī)則也是會把對象存放至老年代的。
- 那么隨著應(yīng)用程序的不斷運行,老年代最終也是會滿的,那么此時也會gc,此時的gc就是
Full gc
了。
那當我們老年代滿了會發(fā)生什么呢?當然是我們上面說過的Full GC
,但是你仔細看我們寫的這個程序,我們所有new出來的HeapTest對象都是存放在heapLists中的,那就會被這個局部變量
所引用,那么Full GC就不會有什么垃圾對象可以回收
,可是內(nèi)存又滿了,那怎么辦?OOM
2.3.堆的GC案例
下面是個死循環(huán),不斷的往list中添加new出來的對象。
public class HeapTest {
byte[] a = new byte[1024 * 100];
public static void main(String[] args) throws InterruptedException {
ArrayList<HeapTest> heapTest = new ArrayList<>();
while(true) {
heapTest.add(new HeapTest());
Thread.sleep(10);
}
}
}
通過JDK自帶的Jvm調(diào)優(yōu)工具jvisualvm
觀察上面代碼執(zhí)行的內(nèi)存結(jié)構(gòu)。
打開visual GC
- 其中 老年代(Old),伊甸園區(qū)(Eden),S0(From),S1(To) 幾個區(qū)的內(nèi)存 和 動態(tài)分配圖都是清晰可見,以一對應(yīng)的
我們選擇中間一張圖給大家對應(yīng)一下上面所講的內(nèi)容:
- 對象放入Eden區(qū)
- Eden區(qū)滿發(fā)生minor gc
- 第二步的存活對象移至From(Survivor 0)區(qū)
- Eden區(qū)再滿發(fā)生minor gc
- 第四步存活的對象移至To(Survivor 1)區(qū)
- 這里可以注意到
From和To區(qū)
和我們上面所說一致,總有一個是空的
。
可以看到老年代這里,都是一段一段的直線,中間是突然的增加,這就是在minor gc
中一批一批符合規(guī)則的對象被批量移入老年代。
2.4.堆垃圾回收方式
-
Minor GC(YGC): 它主要是用來對
年輕代
進行垃圾回收的方式,使用的復(fù)制算法
,因為年輕代的對象大多數(shù)生命周期很短,所以GC的頻率也會比較頻繁,但是回收速度很快。 -
Major GC(YGC): 它是主要用于對
老年代
對象的垃圾回收方式,老年代的對象生命周期都是比較長的,所以對象不會輕易滅亡,Major GC的頻率不會像Minor GC那么頻繁,況且一次Full GC會比Minor GC需要花費更多的時間、消耗更大,通常出現(xiàn)一次Major GC一般也會出現(xiàn)一次Minor GC(但不絕對)。 -
Full GC(): Full GC是針對整
個年輕代、老生代、元空間(metaspace,java8以上版本取代perm gen)的全局范圍的GC,
但是它并不等于Major GC + Minor GC,具體是要看使用什么垃圾收集器組合。一次Full GC 需要花費更多的時間、消耗更大,所以要盡可能減少Full GC的次數(shù)
。 -
特點比較:
- Minor GC使用
復(fù)制算法
,需要一塊空的內(nèi)存空間,所以空間使用效率不高
,但是它不會出現(xiàn)空間碎片的問題。 - 而Full GC一般是采用
標記-清除算法
,容易產(chǎn)生空間碎片
,如果再有對象需要請求連續(xù)的空間而無法提供時,會提前觸發(fā)垃圾回收,
所以它適合存活對象較多
的場景使用也就是老年代
的垃圾回收。
- Minor GC使用
3.什么是內(nèi)存泄露
-
內(nèi)存泄漏是
不再被使用的對象或者變量一直被占據(jù)在內(nèi)存中
。理論上來說,Java是有GC垃圾回收機制的,也就是說,不再被使用的對象,會被GC自動回收掉,自動從內(nèi)存中清除。- 但也有特例即:
長生命周期的對象持有短生命周期對象的引用就很可能發(fā)生內(nèi)存泄露
,盡管短生命周期對象已經(jīng)不再需要,但是因為長生命周期對象持有它的引用而導(dǎo)致不能被回收,這就是Java中內(nèi)存泄露的發(fā)生場景。
- 但也有特例即:
4.堆棧的區(qū)別
物理地址
-
堆的物理地址分配對對象是
不連續(xù)的
。因此性能慢些。在GC的時候也要考慮到不連續(xù)的分配
,所以有各種算法。比如,標記-消除,復(fù)制,標記-壓縮,分代(即年輕代使用復(fù)制算法,老年代使用標記——壓縮) -
棧使用的是
數(shù)據(jù)結(jié)構(gòu)中的棧
,后進先出
的原則,物理地址分配是連續(xù)
的。所以性能快。
內(nèi)存分別
-
堆因為是
不連續(xù)的
,所以分配的內(nèi)存是在運行期
確認的,因此大小不固定
。一般堆大小遠遠大于棧。 -
棧是
連續(xù)的
,所以分配的內(nèi)存大小要在編譯期
就確認,大小是固定的
。
存放的內(nèi)容
-
堆存放的是
對象的實例和數(shù)組
。因此該區(qū)更關(guān)注的是數(shù)據(jù)的存儲
-
棧存放:
局部變量,操作數(shù)棧,返回結(jié)果
。該區(qū)更關(guān)注的是程序方法的執(zhí)行
。
程序的可見度
- 堆對于線程都是共享、可見的。
- 棧只是線程私有的。他的生命周期和線程相同。
5.堆、方法區(qū) 和 棧的關(guān)系
該代碼聲明了一個類,并在main方法中創(chuàng)建了兩個SimpleHeap實例。
public class SimpleHeap {
private int id;
public SimpleHeap(int id){
this.id = id;
}
public void show(){
System.out.println("My id is "+id);
}
public static void main(String[] args) {
SimpleHeap s1 = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
s1.show();
s2.show();
}
}
各對象和局部變量的存放情況如下圖:
-
SimpleHeap實例
本身分配在堆
中,描述SimpleHeap類的信息
存放在方法
區(qū),main函數(shù)中的s1 s2局部變量
存放在java棧
上,并指向堆中2個實例。
三.虛擬機棧
0.虛擬機棧概念
是Java方法執(zhí)行的內(nèi)存模型,每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)
用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口
等信息,每個方法從調(diào)用直至執(zhí)行完成的過程,都對應(yīng)著一個棧幀
在虛擬機棧中入棧到出棧
的過程。
每一個線程都有一個私有的Java棧,一個線程的Java棧在線程創(chuàng)建的時候被創(chuàng)建
java棧中保存著棧幀
信息- 局部變量表:存放了編譯器可知的
各種基本數(shù)據(jù)類型
(boolean、byte、char、short、int、float、long、double)、對象引用(引用指針,并非對象本
),- 局部變量表所需的內(nèi)存空間在
編譯期間完成分配
,當進入一個方法時,這個方法需要在棧幀中分配多大的局部變量是完全確定的,在運行期間棧幀不會改變局部變量表的大小空間
)
- 特點:線程私有
-
異常規(guī)定:StackOverflowError、OutOfMemoryError
- 如果線程
請求的棧深度
大于虛擬機所允許的棧深度
就會拋出StackOverflowError - 如果虛擬機內(nèi)存是可以動態(tài)擴展的,如果擴展時無法申請到足夠的內(nèi)存就會拋出OutOfMemoryError
- 如果線程
JVM 會在線程被創(chuàng)建時,創(chuàng)建一個線程私有
的虛擬機棧,也叫“線程棧
”。該棧的生命周期和線程是一致
,除了Native方法以外,Java方法都是通過Java 虛擬機棧來實現(xiàn)調(diào)用和執(zhí)行
過程的(需要程序計數(shù)器、堆、元空間內(nèi)數(shù)據(jù)的配合)。所以Java虛擬機棧是虛擬機執(zhí)行引擎的核心之一。而Java虛擬機棧中出棧入棧的元素就稱為「棧幀」
。
- 每個線程棧由
多個棧幀(Frame)
組成,對應(yīng)著每個方法
運行時所占用的內(nèi)存
。 - 每個線程只能有一個
活動棧幀
,也叫當前棧幀
,對應(yīng)著當前正在執(zhí)行的方法
,當方法執(zhí)行時壓入棧
,方法執(zhí)行完畢后彈出棧
。 - 方法體中的
基本類型
的變量都在棧上,引用變量
的指針在棧上,實例在堆上
。
1.線程棧的結(jié)構(gòu)
public class Math {
public static int initData = 666;
public static User user = new User();
public int compute() {
int a = 1;
int b = 2;
int c = (a+b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
System.out.println("test");
}
}
- 每個方法都有自己的局部變量,如圖中main方法中的
math,compute
方法中的a b c
,那么Java虛擬機為了區(qū)分不同方法中局部變量作用域范圍的內(nèi)存區(qū)域,每個方法在運行的時候都會分配一塊獨立的棧幀內(nèi)存區(qū)域, 上圖中的程序代碼執(zhí)行的內(nèi)存活動如下。
-
執(zhí)行main方法中的第1行代碼是,棧中會分配
main()
方法的棧幀,并存儲math局部變量,,接著執(zhí)行compute()
方法,那么棧又會分配compute()的棧幀區(qū)域。- 當
compute()
方法執(zhí)行完之后,就會出棧被釋放,也就符合先進后出
的特點,后調(diào)用的方法先出棧。
- 當
2.棧幀
棧幀(Stack Frame)
是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。
-
棧幀存儲了方法的
局部變量表、操作數(shù)棧、動態(tài)連接和方法返回地址等信息
**。 -
每一個方法從調(diào)用至執(zhí)行完成的過程,都對應(yīng)著一個棧幀在虛擬機棧里從
入棧到出棧
的過程**。 -
簡單的理解就是:
棧對應(yīng)線程,棧幀對應(yīng)方法
- 棧幀主要由4個部分組成。
- 棧幀主要由4個部分組成。
局部變量表(Local Variable Table)
局部變量表(Local Variable Table)是一組變量值存儲空間
,用于存放方法參數(shù)和方法內(nèi)定義的局部變量
。包括8種基本數(shù)據(jù)類型、對象引用(reference類型)和returnAddress類型 (指向一條字節(jié)碼指令的地址)。
- 如果線程請求的棧深度大于虛擬機所允許的深度,將拋出
StackOverflowError
異常 - 如果虛擬機棧動態(tài)擴展時無法申請到足夠的內(nèi)存時會拋出
OutOfMemoryError
異常。
直接上代碼
public int test(int a, int b) {
Object obj = new Object();
return a + b;
}
- 如果局部變量是Java的8種基本數(shù)據(jù)類型,則存在局部變量表中,如果是
引用類型
。如new出來的String,局部變量表中存的是引用
,而實例在堆中
。
操作數(shù)棧(Operand Stack)
操作數(shù)棧(Operand Stack) 也稱作操作棧,是一個后入先出棧(LIFO)
。隨著方法執(zhí)行
和字節(jié)碼指令
的執(zhí)行,會從局部變量表或?qū)ο髮嵗淖侄?/code>中復(fù)制常量或變量寫入到操作數(shù)棧,再隨著計算的進行將棧中元素 出棧到局部變量表 或者 返回給方法調(diào)用者,也就是出棧/入棧操作。
public class OperandStackTest {
public int sum(int a, int b) {
return a + b;
}
}
編譯生成.class文件之后,再反匯編查看匯編指令
javac OperandStackTest.java
javap -v OperandStackTest.class
OperandStackTest字節(jié)碼文件
public int sum(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3 // 最大棧深度為2 局部變量個數(shù)為3
0: iload_1 // 局部變量1 壓棧
1: iload_2 // 局部變量2 壓棧
2: iadd // 棧頂兩個元素相加,計算結(jié)果壓棧
3: ireturn
LineNumberTable:
line 10: 0
動態(tài)鏈接
動態(tài)鏈接:Java虛擬機棧中,每個棧幀都包含一個指向運行時常量池
中 該棧所屬方法的符號引用
,持有該引用是為了支持方法調(diào)用過程中的動態(tài)鏈接(Dynamic Linking)
。
方法返回地址/方法出口
方法返回地址/方法出口:無論方法是否正常完成,都需要返回到方法被調(diào)用的位置
,程序才能繼續(xù)進行
- 方法執(zhí)行時有2種退出情況:
- 正常退出,即正常執(zhí)行到任何方法的返回字節(jié)碼指令,如 RETURN、IRETURN、ARETURN等
- 異常退出
無論何種退出情況,都將返回至方法當前被調(diào)用的位置。方法退出的過程相當于彈出當前棧幀,退出可能有3種方式:
- 返回值壓入上層調(diào)用棧幀
- 異常信息拋給能夠處理的棧幀
- PC 計數(shù)器指向方法調(diào)用后的下一條指令
那么要講這個就會涉及到更底層的原理–字節(jié)碼
。我們先看下我們上面代碼的字節(jié)碼文件。
看著就是一個16字節(jié)的文件
,看著像亂碼,其實每個都是有對應(yīng)的含義的,oracle官方是有專門的Jvm字節(jié)碼指令手冊
來查詢每組指令對應(yīng)的含義的。那我們研究的,當然不是這個。
- JDK有自帶一個
javap
的命令,可以將上述class文件生成一種更可讀的字節(jié)碼文件
。 - 我們使用
javap -c
命令將class文件反編譯并輸出到TXT文件中。
Compiled from "Math.java"
public class com.example.demo.test1.Math {
public static int initData;
public static com.example.demo.bean.User user;
public com.example.demo.test1.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/example/demo/test1/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String test
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
static {};
Code:
0: sipush 666
3: putstatic #8 // Field initData:I
6: new #9 // class com/example/demo/bean/User
9: dup
10: invokespecial #10 // Method com/example/demo/bean/User."<init>":()V
13: putstatic #11 // Field user:Lcom/example/demo/bean/User;
16: return
}
其中方法中的指令還是有點懵,我們舉compute()方法來看一下:
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
- 這幾行代碼就是對應(yīng)的我們代碼中
compute()
方法中的四行代碼。大家都知道越底層的代碼,代碼實現(xiàn)的行數(shù)越多,因為他會包含一些java代碼在運行時底層隱藏的一些細節(jié)原理。那么一樣的,這個Jvm指令官方也是有手冊可以查閱的,網(wǎng)上也有很多翻譯版本,大家如果想了解可自行百度。
0. 將int類型常量1壓入操作數(shù)棧
0: iconst_1
1. 將int類型值存入局部變量1
1: istore_1
- 局部變量1,在我們代碼中也就是第一個
局部變量a
,先給a在局部變量表
中分配內(nèi)存,然后將int類型的值,也就是目前唯一的一個1存入局部變量a
2. 將int類型常量2壓入操作數(shù)棧
2: iconst_2
3. 將int類型值存入局部變量2
3: istore_2
4. 從局部變量1中裝載int類型值
4: iload_1
5. 從局部變量2中裝載int類型值
5: iload_2
- 這兩個代碼是將局部變量1和2,也就是a和b的值裝載到
操作數(shù)棧
中
6. 執(zhí)行int類型的加法
6: iadd
-
iadd
指令一執(zhí)行,會將操作數(shù)棧
中的1和2
依次從棧底彈出并相加
,然后把運算結(jié)果3在壓入操作數(shù)棧底。
7. 將一個8位帶符號整數(shù)壓入棧
7: bipush 10
- 這個指令就是將10壓入棧
8. 執(zhí)行int類型的乘法
9: imul
- 這里就類似上面的加法了,將
3和10彈出棧
,把結(jié)果30壓入棧
9. 將將int類型值存入局部變量3
10: istore_3
- 這里大家就不陌生了吧,和第2步第3步是一樣的,
將30存入局部變量3,也就是c
10. 從局部變量3中裝載int類型值
11: iload_3
- 這個前面也說了
11. 返回int類型值
12: ireturn
- 這個就不用多說了,就是
將操作數(shù)棧中的30返回
到這里就把我們compute()方法講解完了,講完有沒有對局部變量表和操作數(shù)棧的理解有所加深呢?說白了賦值號=后面的
就是操作數(shù),在這些操作數(shù)進行賦值,運算的時候需要往內(nèi)存存放,那就是存放在操作數(shù)棧
中,作為臨時存放操作數(shù)的一小塊內(nèi)存區(qū)域。
接下來我們再說說方法出口。
- 方法出口說白了不就是
方法執(zhí)行完了之后要出到哪里
,那么我們知道上面compute()方法執(zhí)行完之后應(yīng)該回到main()方法第三行
那么當main()方法調(diào)用compute()的時候,compute()棧幀
中的方法出口就存儲了當前要回到的位置,那么當compute()方法執(zhí)行完之后,會根據(jù)方法出口中存儲的相關(guān)信息回到main()方法的相應(yīng)位置。
3.棧幀與函數(shù)調(diào)用
- 如下圖:函數(shù)1中調(diào)用函數(shù)2,函數(shù)2中調(diào)用函數(shù)3,函數(shù)3調(diào)用函數(shù)4。當函數(shù)1被調(diào)用時,棧幀1入棧,當函數(shù)2調(diào)用時,棧幀2入棧。。。以此類推。當前正在執(zhí)行的函數(shù)所對應(yīng)的幀就是
當前幀(位于棧頂)
,它保存著當前函數(shù)的局部變量、中間計算結(jié)果等數(shù)據(jù)
。 - 當函數(shù)返回時,棧幀從java棧中被彈出,java方法區(qū)有2種返回函數(shù)的方式,一種是正常的函數(shù)返回,使用
return指令
,另一種是拋出異常
。不管使用哪種方式,都會導(dǎo)致棧幀被彈出。- 每次函數(shù)調(diào)用都會產(chǎn)生對應(yīng)的棧幀,占用一定的棧內(nèi)存,如果棧內(nèi)存不足,當
請求的棧深度大于最大可用棧深度時
,系統(tǒng)會拋出StackOverflowError棧溢出錯誤
。
- 每次函數(shù)調(diào)用都會產(chǎn)生對應(yīng)的棧幀,占用一定的棧內(nèi)存,如果棧內(nèi)存不足,當
使用遞歸,由于遞歸沒有出口,這段代碼可能會拋出棧溢出錯誤,在拋出棧溢出錯誤時,打印最大的調(diào)用深度
public class TestStackDeep {
private static int count =0;
public static void recursion(){
count ++;
recursion();
}
public static void main(String[] args) {
try{
recursion();
}catch(Throwable e){
System.out.println("deep of calling ="+count);
e.printStackTrace();
}
}
}
- 使用參數(shù)-Xss128K執(zhí)行上面代碼
在進行大約1079次調(diào)用之后,發(fā)生了棧溢出錯誤,通過增大-Xss的值,可以獲得更深的層次調(diào)用,嘗試使用參數(shù)-Xss256K執(zhí)行上述代碼,調(diào)用層次有明顯的增加:
結(jié)論:函數(shù)嵌套調(diào)用的層次在很大程度上由棧的大小決定
,棧越大,函數(shù)支持的嵌套調(diào)用次數(shù)就越多。
4.棧幀與局部變量表
局部變量表是棧幀的組成部分之一。用于保存函數(shù)的參數(shù)
以及局部變量
,局部變量表隨著函數(shù)棧幀的彈出而銷毀。
- 如果函數(shù)的參數(shù)和局部變量很多 或 很大,會使得
局部變量表膨脹
,從而每一次函數(shù)調(diào)用就會占用更多的??臻g
,最終導(dǎo)致函數(shù)的嵌套調(diào)用次數(shù)減少
。
例如:一個recursion()函數(shù)含有3個參數(shù)和10個局部變量,因此,其局部變量表含有13個變量,而第二個recursion()函數(shù)不再含有任何參數(shù)和局部變量,當這兩個函數(shù)被嵌套調(diào)用時,第二個recursion函數(shù)可以擁有更深的調(diào)用層次。
public class TestStackDeep2 {
private static int count = 0;
public static void recursion(long a,long b,long c){
long e=1,f=2,g=3,h=4,i=5,k=6,q=7,x=8,y=9,z=10;
count ++;
recursion(a,b,c);
}
public static void recursion(){
count++;
recursion();
}
public static void main(String[] args) {
try{
recursion(0L,0L,0L);
//recursion();
}catch(Throwable e){
System.out.println("deep of calling = "+count);
e.printStackTrace();
}
}
}
- 使用虛擬機參數(shù)
-Xss128K
遞歸執(zhí)行上述代碼中的recursion(long a,long b,long c)函數(shù)
,輸出結(jié)果為: - 使用虛擬機參數(shù)
-Xss128K
遞歸執(zhí)行不帶參數(shù)的recursion()
函數(shù)
四.本地方法棧
與虛擬機棧的作用是一樣的
,只不過虛擬機棧是服務(wù) Java 方法的,而本地方法棧是為虛擬機調(diào)用本地方法(Native方法)服務(wù)的
-
特性和異常: 同虛擬機棧,請參考上面知識點。即
線程私有,StackOverflowError、OutOfMemoryError
。
new Thread().start();
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
其中底層調(diào)用了一個start0()
的方法,本地方法,底層通過C語言
實現(xiàn)的
private native void start0();
那java代碼里為什么會有C語言實現(xiàn)的本地方法呢?
-
大家都知道JAVA出來之前一個公司的系統(tǒng)百分之九十九都是使用C語言實現(xiàn)的,但是java出現(xiàn)后,很多項目都要轉(zhuǎn)為java開發(fā),那么新系統(tǒng)和舊系統(tǒng)就免不了要有交互,那么就需要本地方法來實現(xiàn)了,底層是調(diào)用C語言中的
dll庫文件
,就類似于java中的jar包
,當然,如今跨語言的交互方式就很多了,比如`thrift,http接口方式,webservice等,當時并沒有這些方式,就只能通過本地方法來實現(xiàn)了。- 那么本地方法始終也是方法,每個線程在運行的時候,如果有運行到本地方法,那么必然也要產(chǎn)生局部變量等,那么就需要存儲在本地方法棧了。如果沒有本地方法,也就沒有本地方法棧了。
五.程序計數(shù)器(PC寄存器/指令切換器)
程序計數(shù)器/*PC寄存器(Program Counter Register):可以看做是當前線程所執(zhí)行的字節(jié)碼的行號指示器
,每個線程都有一個程序計數(shù)器來保存當前執(zhí)行指令的地址
,一旦該指令被執(zhí)行,程序計數(shù)器會被更新至下條指令的地址
。程序計數(shù)器是Java虛擬機規(guī)定的唯一不會發(fā)生內(nèi)存溢出的區(qū)域
。
-
這是一塊
較小
的內(nèi)存空間(可忽略不記
),用于記錄當前線程所執(zhí)行的字節(jié)碼的行號指示器
,字節(jié)碼解析器的工作是通過改變這個計數(shù)器的值,來選取下一條需要執(zhí)行的字節(jié)碼指令
,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能,都需要依賴這個計數(shù)器
來完成; -
特點:線程私有
-
異常規(guī)定:無
那么Jvm虛擬機為什么要設(shè)置程序計數(shù)器這個結(jié)構(gòu)呢?
-
因為Jvm的多線程是通過
線程輪流切換并分配處理器執(zhí)行時間(cpu時間片)來的方式來實現(xiàn)的
,也就是任何時刻,一個處理器(對于多核處理器來說是一個內(nèi)核)
都只會執(zhí)行一條線程中的指令。因此為了線程切換后能恢復(fù)到正確的執(zhí)行位置
,每個線程都有獨立的程序計數(shù)器
。它被設(shè)計出來的目的,是為了讓多線程情況下的JAVA程序每個線程都能夠正常的工作,每個線程都有自己的程序計數(shù)器,用于保存線程的執(zhí)行情況,這樣在進行線程切換的時候就可以在上次執(zhí)行的基礎(chǔ)上繼續(xù)執(zhí)行了
六.元空間
注意:方法區(qū)是一種概念,而永久代和元空間是它的2種實現(xiàn)方式。
1.元空間概念
方法區(qū)即我們常說的永久代(Permanent Generation), 也稱為非堆(No-Heap)、是線程共享的一塊內(nèi)存區(qū)域,用于存儲被 JVM 加載的類信息、常量、靜態(tài)變量、JIT即時編譯器編譯后的代碼
等數(shù)據(jù)
- 運?時常量池: 運?時常量池是?法區(qū)的?部分,Class 文件中除了有 類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池,用于存放編譯期生成的各種
字面量和符號引用
,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中 - 字?量 : 字符串(
JDK 8 移動到堆中
)、final常量、基本數(shù)據(jù)類型的值(如Integer,管理-128–127的常量。)。 - 符號引? : 類和結(jié)構(gòu)的完全限定名、字段的名稱和描述符、?法的名稱和描述符。
JDK1.8后永久代被元空間代替,元空間存儲在直接內(nèi)存(系統(tǒng)內(nèi)存)
,而不在虛擬機當中(不受JVM最大運行內(nèi)存的限制,只和本地內(nèi)存的大小有關(guān)
) 其他內(nèi)容比如類元信息、字段、靜態(tài)屬性、方法、常量等都移動到元空間區(qū)
。
-
特點:線程共享
-
異常規(guī)定:當方法無法滿足內(nèi)存分配需求時會拋出OutOfMemoryError異常。
- 默認最小值為16MB,最大值為64MB,可以通
過-XX:PermSize 和 -XX:MaxPermSize
參數(shù)限制方法區(qū)的大小
- 默認最小值為16MB,最大值為64MB,可以通
-
運行時常量池是方法區(qū)的一部分,用于存放編譯器生成的各種
字面量和符號引用
。 -
JDK1.6字符串常量池在
方法區(qū)
中,1.7將放在方法區(qū)
的字符串常量池放到堆
中。**在JDK1.8時徹底去掉了永久代的概念,而在直接內(nèi)存中劃出一塊區(qū)域作為元空間,運行時常量池、類常量池都移動到元空間。
。
2.為什么要使用元空間取代永久代的實現(xiàn)
1.避免OOM:
- 方法區(qū)主要是
存儲類的相關(guān)信息(包括類的字節(jié)碼文件)
, 雖然永久代可以使用PerSize和MaxPerSize
等參數(shù)設(shè)置永久代的空間大小, 但隨著ASM、Cglib等動態(tài)生成字節(jié)碼技術(shù)的出現(xiàn)可以修改對象字節(jié)碼信息后,無法控制類信息的大小, 因此對于永久代的大小指定比較困難,太小容易出現(xiàn)永久代溢出
,太大則容易導(dǎo)致老年代溢出
,即java.lang.OutOfMemoryError: PermGen
。- JDK1.8使用了元空間替換永久代,因為元空間是使用
系統(tǒng)內(nèi)存
,由系統(tǒng)的實際可用空間來控制,在一定程度上可以避免OOM的出現(xiàn),
但是也需要通過指定MaxMetaspaceSize
等參數(shù)來控制大小。
- JDK1.8使用了元空間替換永久代,因為元空間是使用
2.提高GC性能:
-
永久代的垃圾收集是和老年代捆綁在一起的,
所以無論兩者誰滿了,都會觸發(fā)永久代和老年代的垃圾收集。
JDK1.7時永久代的部分數(shù)據(jù)已經(jīng)從Java的永久代中轉(zhuǎn)移到了堆中,如:符號引用、字符串常量池
- 使用元空間替換后,簡化了
Full GC,減少了GC的時間(因為GC時不需要再掃描永久代中的數(shù)據(jù)),提高了GC的性能
。在元空間中,只有少量指針指向堆,如類的元數(shù)據(jù)中指向class對象的指針。
- 使用元空間替換后,簡化了
3.Hotspot和JRockit合并:
- 官方原因,永久代只是Hotspot虛擬機中存在的概念,JRockit中并沒有這個說法,JDK8需要整合Hotspot和JRockit,所以廢棄了永久代,引入了元空間。
七.拓展—直接內(nèi)存
直接內(nèi)存(Direct Memory): 也叫堆外內(nèi)存,直接內(nèi)存并不是Jvm管理的內(nèi)存,可以這樣理解就是Jvm以外的機器內(nèi)存
,比如,你有4G的內(nèi)存,Jvm占用了1G,則其余的3G就是直接內(nèi)存
- 在JDK 1.4中新加入了
NIO(New Input/Output)
類,引入了一種基于通道(Channel)
與緩沖區(qū)(Buffer)
的I/O方式,它可以使用Native函數(shù)庫
直接分配堆外內(nèi)存
,然后通過一個存儲在Java堆
中的DirectByteBuffer對象
作為這塊內(nèi)存的引用進行操作。通- 通常訪問直接內(nèi)存的速度會優(yōu)于Java堆。因此出于性能的考慮,讀寫頻繁的場合可以考慮使用直接內(nèi)存,避免了在
Java堆和Native堆
中來回復(fù)制數(shù)據(jù)。- 但系統(tǒng)內(nèi)存是有限的,Java堆和直接內(nèi)存的總和依然受限于操作系統(tǒng)能給出的最大內(nèi)存。
- 通常訪問直接內(nèi)存的速度會優(yōu)于Java堆。因此出于性能的考慮,讀寫頻繁的場合可以考慮使用直接內(nèi)存,避免了在
八.對象創(chuàng)建
1.對象組成
對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭(Header)
、實例數(shù)據(jù)(Instance Data)
和 對齊填充(Padding)
HotSpot虛擬機的對象頭包括2部分信息:
Mark Word
-
第一部分markword,用于存儲
對象自身的運行時數(shù)據(jù)
,如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID、偏向時間戳等, -
對象頭的另外一部分是klass
類型指針
(Klass Pointer),即對象指向它的Class類元數(shù)據(jù)的指針
,虛擬機通過這個指針來確定這個對象是哪個類的實例. -
數(shù)組長度(只有數(shù)組對象有): 如果對象是一個數(shù)組, 那在對象頭中還必須有一塊數(shù)據(jù)用于記錄數(shù)組長度.
實例數(shù)據(jù):
- 第二部分實例數(shù)據(jù),
是對象真正存儲的有效信息
,也是在程序代碼中所定義的各種類型的字段內(nèi)容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。
對齊填充
- 第三部分對齊填充并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說,就是對象的大小必須是8字節(jié)的整數(shù)倍。而對象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,
當對象實例數(shù)據(jù)部分沒有對齊時,就需要通過對齊填充來補全
。
其中的類型指針
就是那條紅色的線,那是怎么聯(lián)系的呢?
類加載其實最終是以Class對象
的形式存儲在方法區(qū)
中的,math和math2都是由同一個類new出來的,當對象被new時,都會在對象頭中存儲一個指向類元信息的指針,這就是Klass Pointer類型指針
2.Java中提供的幾種對象創(chuàng)建方式
- 使用
new
關(guān)鍵字->
調(diào)用了構(gòu)造方法 - 使用
Class
的newInstance
方法->
調(diào)用了構(gòu)造方法 - 使用
Constructor
類的newInstance
方法->
調(diào)用了構(gòu)造方法 - 使用
clone
方法->
沒有調(diào)用構(gòu)造方法 - 使用
反序列化
->`沒有調(diào)用構(gòu)造方法
3.對象創(chuàng)建的主要流程
虛擬機遇到一條new指令時
,先檢查常量池是否已經(jīng)加載相應(yīng)的類
,如果沒有,必須先執(zhí)行相應(yīng)的類加載
。類加載通過后,接下來分配內(nèi)存。若Java堆中內(nèi)存是絕對規(guī)整的,使用 “指針碰撞“ 方式分配內(nèi)存;如果不是規(guī)整的,就從空閑列表中分配,叫做 ”空閑列表“ 方式。
- 分內(nèi)存時還需要考慮一個問題-并發(fā),也有2種方式:
CAS同步處理
,或者本地線程分配緩沖
(Thread Local Allocation Buffer, TLAB)。然后內(nèi)存空間初始化操作,接著是做一些必要的對象設(shè)置(元信息、哈希碼…),最后執(zhí)行<init>
方法。
4.對象內(nèi)存分配2種方式
類加載完成后,接著會在Java堆中劃分一塊內(nèi)存分配給對象。內(nèi)存分配根據(jù)Java堆是否規(guī)整
,有2種方式:
-
指針碰撞: 如果Java堆的內(nèi)存是
規(guī)整
,即所有用過的內(nèi)存放在一邊,而空閑的的放在另一邊。分配內(nèi)存時將位于中間的指針指示器向空閑的內(nèi)存移動一段與對象大小相等的距離
,這樣便完成分配內(nèi)存工作。 -
空閑列表: 如果Java堆的內(nèi)存是
不規(guī)整
的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯, 那就沒辦法簡單的進行指針碰撞了, 必須由由虛擬機維護一個列表
來記錄那些內(nèi)存是可用的
,在分配的時候從列表找到一塊足夠大的內(nèi)存分配給對象,并在分配后更新列表記錄。
5.處理并發(fā)安全問題
對象的創(chuàng)建在虛擬機中是一個非常頻繁的行為,哪怕只是修改一個指針所指向的位置
,在并發(fā)情況下也是不安全的,可能出現(xiàn)正在給對象 A 分配內(nèi)存,指針還沒來得及修改,對象 B 又同時使用了原來的指針來分配內(nèi)存的情況。
解決這個問題有兩種方案:文章來源:http://www.zghlxwxcb.cn/news/detail-830768.html
- 對分配內(nèi)存空間的動作進行
同步處理
(采用CAS + 失敗重試
來保障更新操作的原子性); - 把內(nèi)存分配的動作
按照線程劃分在不同的空間之中進行
,即每個線程在 Java 堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖
(Thread Local Allocation Buffer, TLAB)。哪個線程要分配內(nèi)存,就在哪個線程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 時,才需要同步鎖。通過-XX:+/-UserTLAB參數(shù)來設(shè)定虛擬機是否使用TLAB。
面試題:(Java實習(xí)生)每日10道面試題打卡——JVM篇文章來源地址http://www.zghlxwxcb.cn/news/detail-830768.html
到了這里,關(guān)于【Jvm】運行時數(shù)據(jù)區(qū)域(Runtime Data Area)原理及應(yīng)用場景的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!