之前我們文章提到過(guò)反射,說(shuō)的比較淺顯,我們這里來(lái)理解JVM直秆。
一個(gè)標(biāo)準(zhǔn)的JVM是這樣的
JVM負(fù)責(zé)裝載class文件并執(zhí)行,我們首先來(lái)了解類加載和執(zhí)行的機(jī)制鞭盟。
類加載機(jī)制
JVM將.class文件加載到JVM圾结,并形成Class對(duì)象,之后就可以對(duì)Class對(duì)象進(jìn)行實(shí)例化并調(diào)用齿诉。
該過(guò)程分為三個(gè)步驟:
- 裝載筝野。
- 鏈接。
- 初始化粤剧。
- 裝載
負(fù)責(zé)找到二進(jìn)制字節(jié)碼并加載到JVM中歇竟。
JVM通過(guò)類的全限定名以及類加載器完成類的加載。
比如Object[] o=new Object[10]
抵恋,o的全限定名:[Ljava.lang.Object
焕议,并由數(shù)組型中的元素類型所在的ClassLoader進(jìn)行加載。
- 鏈接
鏈接過(guò)程負(fù)責(zé)對(duì)二進(jìn)制字節(jié)碼進(jìn)行校驗(yàn)弧关、初始化裝載類中的靜態(tài)變量以及解析類中調(diào)用的接口盅安、類。
- 初始化
初始化過(guò)程即執(zhí)行類中的靜態(tài)初始化代碼世囊,構(gòu)造器代碼以及靜態(tài)屬性的初始化别瞭。
類執(zhí)行機(jī)制
在完成將class文件信息加載到JVM并產(chǎn)生Class對(duì)象后,就可執(zhí)行Class對(duì)象的靜態(tài)方法或?qū)嵗瘜?duì)象進(jìn)行調(diào)用了株憾。在源碼編譯階段蝙寨,將源碼編譯為JVM字節(jié)碼,JVM字節(jié)碼是一種中間代碼的方式嗤瞎,要由JVM在運(yùn)行期間對(duì)其進(jìn)行解釋并執(zhí)行籽慢。這種方式稱為:字節(jié)碼解釋執(zhí)行方式。
字節(jié)碼解釋執(zhí)行
由于采用JVM字節(jié)碼猫胁,也就是說(shuō)JVM有一套自己的指令來(lái)執(zhí)行中間碼:
- invokestatic
調(diào)用static方法 - invokevirtual
調(diào)用對(duì)象實(shí)例的方法 - invokeinterface
調(diào)用接口 - invokespecial
調(diào)用private方法和<init>對(duì)象初始化方法
比如下面這一段代碼:
public class Demo{
public void execute(){
A.execute();
A a=new A();
a.bar();
IFoo b=new B();
b.bar();
}
}
class A{
public static int execute(){
return 1+2;
}
public int bar(){
return 1+2;
}
}
class B implements IFoo{
public int bar(){
return 1+2;
}
}
public interface IFoo{
public int bar();
}
通過(guò)javac 編譯上面的代碼后箱亿,使用javap -c Demo
查看其execute方法的字節(jié)碼:
Compiled from "Demo.java"
public class Demo {
public Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void execute();
Code:
0: invokestatic #2 // Method A.execute:()I
3: pop
4: new #3 // class A
7: dup
8: invokespecial #4 // Method A."<init>":()V
11: astore_1
12: aload_1
13: invokevirtual #5 // Method A.bar:()I
16: pop
17: new #6 // class B
20: dup
21: invokespecial #7 // Method B."<init>":()V
24: astore_2
25: aload_2
26: invokeinterface #8, 1 // InterfaceMethod IFoo.bar:()I
31: pop
32: return
}
從上面的栗子可以看出,四種指令對(duì)應(yīng)調(diào)用方法的情況弃秆。
Sun JDK基于棧的體系結(jié)構(gòu)來(lái)執(zhí)行字節(jié)碼届惋,基于棧方式的好處就是代碼緊湊,體積小菠赚。
線程在創(chuàng)建后脑豹,都會(huì)產(chǎn)生程序計(jì)數(shù)器(PC或者稱為PC registers)和棧(Stack);PC存放了下一條要執(zhí)行的指令在方法內(nèi)的偏移量衡查;棧中存放了棧幀(StackFrame)瘩欺,每個(gè)方法每次調(diào)用都會(huì)產(chǎn)生棧幀,棧幀主要分為局部變量區(qū)和操作數(shù)棧兩個(gè)部分,局部變量區(qū)用于存放方法體中的局部變量和參數(shù)俱饿,操作數(shù)棧中用于存放方法執(zhí)行過(guò)程中產(chǎn)生的中間結(jié)果歌粥,棧幀中還有一些其他空間,例如只想方法已解析的常量池的引用拍埠、其他一些VM內(nèi)部需要的數(shù)據(jù)等失驶,具體結(jié)構(gòu)如下圖所示:
下面來(lái)看一個(gè)方法執(zhí)行時(shí)過(guò)程的栗子:
public class Demo2{
public static void foo(){
int a=1;
int b=2;
int c=(a+b)*5;
}
}
同樣的方法獲得JVM字節(jié)碼:
public class Demo2 {
public Demo2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void foo();
Code:
0: iconst_1
1: istore_0
2: iconst_2
3: istore_1
4: iload_0
5: iload_1
6: iadd
7: iconst_5
8: imul
9: istore_2
10: return
}
每條字節(jié)碼及其對(duì)應(yīng)的解釋如下:
對(duì)于方法的指令解釋執(zhí)行,執(zhí)行方式為經(jīng)典馮諾伊曼體系中的FDX循環(huán)方式枣购,即獲取下一條指令嬉探,解碼并分派,然后執(zhí)行棉圈。在實(shí)現(xiàn)FDX循環(huán)時(shí)有switch-threading涩堤、token-threading、direct-threading等多種方式分瘾。
第一種swith-threading胎围,代碼大致如下:
while(true){
int code=fetchNextCode();//下一條指令
switch(code){
case IADD: //do add
case ...: //do sth
}
}
每次執(zhí)行完都得重新回到循環(huán)開(kāi)始點(diǎn),然后重新獲取下一條指令芹敌,并繼續(xù)switch痊远,這導(dǎo)致了大部分時(shí)間都花在了跳轉(zhuǎn)和獲取下一條指令上垮抗,真的的業(yè)務(wù)邏輯代碼非常短氏捞。
token-threading在上面的基礎(chǔ)上稍微有所修改:
IADD:{
//do add
fetchNextCode();//下一條指令
dispatch();
}
ICONST_0:{
push(0);
fetchNextCode();下一條指令
dispatch();
}
...
該方法相對(duì)第一種switch-threading而言,冗余了fetch next code和dispatch冒版,相對(duì)比較消耗內(nèi)存液茎,但是由于去除了switch,因此性能會(huì)稍微好一些辞嗡。
其他的xxx-threading做了更多的優(yōu)化捆等,在此不做贅述。Sun JDK的重點(diǎn)為編譯成機(jī)器碼续室,并沒(méi)有在解釋器上做太復(fù)雜的處理栋烤,因此采用了token-threading方法,為了讓解釋執(zhí)行能夠更加高效挺狰,Sun JDK還做了一些其他的優(yōu)化明郭,主要是:棧頂緩存(top-of-stack caching)和部分棧幀共享。
棧頂緩存
在方法執(zhí)行過(guò)程中丰泊,可以看到有很多操作要將值放入操作數(shù)棧薯定,這導(dǎo)致了寄存器和內(nèi)存要不斷的交換數(shù)據(jù),Sun JDK采用了一個(gè)棧頂緩存瞳购,即將本來(lái)位于操作數(shù)棧頂?shù)闹抵苯泳彺娴郊拇嫫魃匣爸叮芍苯釉诩拇嫫饔?jì)算,然后放回操作數(shù)棧。
部分棧幀共享
當(dāng)一個(gè)方法調(diào)用另一個(gè)方法時(shí)年堆,通常傳入另一個(gè)方法的參數(shù)為已存放在操作數(shù)棧的數(shù)據(jù)吞杭,Sun JDK采用:當(dāng)調(diào)用方法時(shí),后一個(gè)方法將前一個(gè)方法的操作數(shù)棧作為當(dāng)前方法的局部變量嘀韧,從而節(jié)省數(shù)據(jù)copy帶來(lái)的消耗篇亭。
(運(yùn)行時(shí))編譯執(zhí)行
由于解釋執(zhí)行的效率太低,Sun JDK提供將字節(jié)碼編譯為機(jī)器碼锄贷,在執(zhí)行過(guò)程中译蒂,對(duì)執(zhí)行頻率高的代碼進(jìn)行編譯執(zhí)行,對(duì)執(zhí)行不頻繁的代碼采用解釋執(zhí)行谊却,因此Sun JDK也稱為Hotspot VM柔昼,在編譯上Sun JDK提供了兩種模式,client compiler(-client) 和 server compiler(-server)炎辨。
- client compiler
client compiler比較輕量級(jí)捕透,制作少量性能開(kāi)銷比較高的優(yōu)化,它占用內(nèi)存較少碴萧,適合于桌面交互式應(yīng)用乙嘀,主要的優(yōu)化有:方法內(nèi)聯(lián)、去虛擬化破喻、冗余消除等虎谢。
1.方法內(nèi)聯(lián)
例如這樣一段代碼:
public void bar(){
...
bar2();
...
}
public void bar2(){
//bar2執(zhí)行代碼
}
當(dāng)編譯時(shí),如果bar2代碼編譯后的字節(jié)數(shù)小雨等于35個(gè)字節(jié)(可以通過(guò)啟動(dòng)參數(shù)-XX:MaxInlineSize=35來(lái)控制)曹质,那么會(huì)演變稱為這樣的結(jié)構(gòu):
public void bar(){
...
//bar2執(zhí)行代碼
...
}
可在debug版本的JDK的啟動(dòng)參數(shù)上加上-XX:+PrintInlining來(lái)查看方法內(nèi)聯(lián)信息婴噩。
2.去虛擬化
去虛擬化是指在裝載class文件后,進(jìn)行類層次的分析羽德,如發(fā)現(xiàn)類中的方法只提供一個(gè)實(shí)現(xiàn)類几莽,那么對(duì)于調(diào)用了此方法的代碼,也可進(jìn)行方法內(nèi)聯(lián)宅静,從而提升執(zhí)行的性能章蚣。
例如這樣的代碼:
public interface IFoo{
public void bar();
}
public class Foo implements IFoo{
public void bar(){
//Foo bar method
}
}
public class Demo{
public void execute(IFoo foo){
foo.bar();
}
}
當(dāng)整個(gè)JVM只有Foo實(shí)現(xiàn)了IFoo接口,Demo execute方法被編譯的時(shí)候姨夹,就會(huì)演變成類似這樣的結(jié)構(gòu):
public void execute(){
//Foo bar method
}
3.冗余消除
冗余消除是指在編譯時(shí)纤垂,根據(jù)運(yùn)行時(shí)狀況進(jìn)行折疊或消除代碼。
比如:
private static final Log=log.LogFactory.getLog("BLUEDAVY")匀伏;
private static final boolean isDebug=log.isDebugEnabled();
public void execute(){
if(isDebug){
log.debug(xxx);
}
//do something else
}
如果boolean值是false那么會(huì)演變?yōu)槿缦碌慕Y(jié)構(gòu):
public void execute(){
//do something else
}
- server compiler
server compiler較為重量級(jí)洒忧,采用了大量傳統(tǒng)編譯優(yōu)化技巧,占用內(nèi)存相對(duì)較多够颠,適合服務(wù)端的應(yīng)用熙侍,下面介紹幾個(gè)優(yōu)化:
1.標(biāo)量替換
例如:
Point p=new Point(1,2);
sout("point.x="+p.x+";point.y="+p.y);
當(dāng)p對(duì)象在后面沒(méi)用到的時(shí)候,會(huì)演變成下面的結(jié)構(gòu):
int x=1;
int y=2;
sout("point.x="+x+";point.y="+y);
2.同步消除
如果發(fā)現(xiàn)同步的對(duì)象沒(méi)必要,那么會(huì)直接去掉:
Point p=new Point();
sysnchronized(p){
//do something
}
演變?yōu)椋?/p>
Point p=new Point();
//do somehing
從上面兩種重量級(jí)和輕量級(jí)的編譯來(lái)看蛉抓,它們做了很多努力來(lái)優(yōu)化庆尘。為什么不再一開(kāi)始就編譯稱為機(jī)器碼呢?
主要有下面幾方面的原因:
- 靜態(tài)編譯并不能根據(jù)程序的運(yùn)行狀況來(lái)優(yōu)化執(zhí)行的代碼巷送,server compiler收集運(yùn)行數(shù)據(jù)越長(zhǎng)驶忌,編譯出來(lái)的代碼會(huì)越優(yōu)化。
- 解釋執(zhí)行比編譯執(zhí)行更省內(nèi)存笑跛。
- 啟動(dòng)時(shí)解釋執(zhí)行的啟動(dòng)速度比編譯后再啟動(dòng)更快付魔。
那么什么時(shí)候就需要編譯呢?這需要一個(gè)權(quán)衡值飞蹂,Sun JDK有兩個(gè)計(jì)數(shù)器來(lái)計(jì)算閾值:
- CompileThreshold
當(dāng)方法被調(diào)用多少次后几苍,編譯為機(jī)器碼。通過(guò)-XX:CompileThreshold=10000來(lái)設(shè)置該值陈哑。client默認(rèn)1500次妻坝,server默認(rèn)10000; - OnStackReplacePercentage
棧上替換的百分比惊窖,該值用于/參與計(jì)算是否觸發(fā)OSR編譯的閾值刽宪,通過(guò)-XX:OnStackReplacePercentage=140來(lái)設(shè)置。在client模式下界酒,計(jì)算規(guī)則為:CompileThreshold * (OnStackReplacePercentage/100)圣拄;在server模式下,計(jì)算規(guī)則為:(OnStackReplacePercentage – InterpreterProfilePercentage))/100
反射執(zhí)行
反射執(zhí)行是基于反射來(lái)動(dòng)態(tài)調(diào)用某對(duì)象實(shí)例中對(duì)應(yīng)的方法盾计,訪問(wèn)查看對(duì)象的屬性等等售担,之前的文章寫的很清楚赁遗。
Java中通過(guò)如下的方法調(diào)用:
Class actionClass=Class.forName(外部實(shí)現(xiàn)類);
Method method=actionClass.getMethod(“execute”,null);
Object action=actionClass.newInstance();
method.invoke(action,null);
這樣在創(chuàng)建對(duì)象過(guò)程和方法調(diào)用過(guò)程是動(dòng)態(tài)的署辉,具有很高的靈活性。
內(nèi)存回收
內(nèi)存空間
Sun JDK在實(shí)現(xiàn)時(shí)岩四,將內(nèi)存空間劃分為方法區(qū)哭尝、堆、本地方法棧剖煌、PC寄存器以及JVM方法棧材鹦。如下圖所示:
- 方法區(qū)
方法區(qū)存放了要加載的類的信息、靜態(tài)變量耕姊、final類型常量等信息桶唐。方法區(qū)是全局共享的。
通過(guò)-XX:PermSize和-XX:MaxPermSize來(lái)指定最小最大的值茉兰,保證方法區(qū)內(nèi)存大小尤泽。
- 堆
堆用于存儲(chǔ)對(duì)象實(shí)例及數(shù)組值,可以認(rèn)為Java中所有new的對(duì)象都在此分配。
- 本地方法棧
用于支持native方法的執(zhí)行坯约。在Sun JDK的實(shí)現(xiàn)中本地方法棧和JVM方法棧是同一個(gè)熊咽。
- PC寄存器和JVM方法棧
每個(gè)線程單獨(dú)創(chuàng)建自己的PC寄存器和JVM方法棧(私有的)。當(dāng)方法運(yùn)行完畢時(shí)闹丐,其對(duì)應(yīng)的棧幀所用內(nèi)存也會(huì)自動(dòng)釋放横殴。
收集器
JVM通過(guò)GC來(lái)回收堆和方法區(qū)中的內(nèi)存,GC的基本原理是首先找到程序中不再被使用的對(duì)象卿拴,然后回收這些對(duì)象所占用的內(nèi)存衫仑。
主要的收集器有引用計(jì)數(shù)收集器和跟蹤收集器。
- 引用計(jì)數(shù)收集器
顧名思義堕花,通過(guò)計(jì)數(shù)器記錄對(duì)象引用數(shù)目惑畴,當(dāng)引用數(shù)目為0時(shí)回收對(duì)象。 -
跟蹤收集器
跟蹤收集器采用的為集中式的管理方式航徙,全局記錄數(shù)據(jù)的引用狀態(tài)如贷。基于一定條件觸發(fā)(例如定時(shí)觸發(fā)或者空間不足時(shí)觸發(fā))到踏,執(zhí)行時(shí)需要從根集合來(lái)掃描對(duì)象的引用關(guān)系杠袱,這可能會(huì)造成應(yīng)用程序暫停,主要有復(fù)制窝稿、標(biāo)記-清除楣富、標(biāo)記-壓縮三種實(shí)現(xiàn)算法。
(其實(shí)就是清理內(nèi)存的算法伴榔,計(jì)算機(jī)原理也學(xué)過(guò))
復(fù)制:從根集合中掃描存活的對(duì)象纹蝴,復(fù)制到未使用的空間中。
在這里插入圖片描述
標(biāo)記清除:從根集合中掃描踪少,對(duì)存活的對(duì)象標(biāo)記塘安,然后再掃描整個(gè)空間中未標(biāo)記的對(duì)象,進(jìn)行回收援奢。
在這里插入圖片描述
標(biāo)記壓縮:和標(biāo)記清除一樣也要進(jìn)行標(biāo)記兼犯,不過(guò)第二步在回收不存活的對(duì)象的內(nèi)存后,會(huì)將對(duì)象左移壓縮集漾。
在這里插入圖片描述
Sun JDK可用的GC
以上三種跟蹤收集器各有優(yōu)缺點(diǎn)切黔,Sun JDK認(rèn)為:程序中大部分對(duì)象存活時(shí)間都是較短的,只有少部分是長(zhǎng)期存活的具篇。根據(jù)這一分析纬霞,JVM被劃分為新生代和舊生代,根據(jù)兩代generation有不同的GC實(shí)現(xiàn)驱显。
新生代中對(duì)象存活期短诗芜,因此選用復(fù)制算法進(jìn)行回收侨舆。由于在復(fù)制的時(shí)候,需要一塊未使用的空間來(lái)存放存活的對(duì)象(和固態(tài)硬盤一樣绢陌,也是要預(yù)留空間)挨下,所以新生代又被分為Eden、S0脐湾、S1三塊空間臭笆。
Eden Space存放新創(chuàng)建的對(duì)象,S0或S1其中一塊作為復(fù)制的目標(biāo)空間(輪流):當(dāng)一塊作為復(fù)制的目標(biāo)空間秤掌,另一塊被清空愁铺。因此S0和S1也被稱為:From Space和To Space。
Sun JDK提供了串行GC闻鉴、并行回收GC和并行GC三種方式來(lái)回收茵乱,在此不做贅述。
舊生代與新生代不同孟岛,對(duì)象存活的時(shí)間比較長(zhǎng)瓶竭,比較穩(wěn)定,因此采用標(biāo)記(Mark)算法來(lái)進(jìn)行回收渠羞,所謂標(biāo)記就是掃描出存活的對(duì)象斤贰,然后再進(jìn)行回收未被標(biāo)記的對(duì)象,回收后對(duì)用空出的空間要么進(jìn)行合并次询,要么標(biāo)記出來(lái)便于下次進(jìn)行分配荧恍,總之就是要減少內(nèi)存碎片帶來(lái)的效率損耗。在執(zhí)行機(jī)制上JVM提供了串行 GC(SerialMSC)屯吊、并行GC(parallelMSC)和并發(fā)GC(CMS)送巡,具體算法細(xì)節(jié)還有待進(jìn)一步深入研究。