1.對象的創(chuàng)建
1.遇到new指令時,首先檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用募逞,并且檢查這個符號引用代表的類是否已經(jīng)被加載蛋铆、解析和初始化過。如果沒有放接,執(zhí)行相應(yīng)的類加載刺啦。
2.類加載檢查通過之后,為新對象分配內(nèi)存(內(nèi)存大小在類加載完成后便可確認)纠脾。在堆的空閑內(nèi)存中劃分一塊區(qū)域(指針碰撞或空閑列表的分配方式)
指針碰撞:Java堆是規(guī)整的玛瘸,所有用過的內(nèi)存放在一邊,空閑的放在另一邊苟蹈,中間放著一個指針作為分界點的指示器糊渊。分配內(nèi)存只是把指針向空閑空間挪動與對象大小相等的距離。這種分配稱為“指針碰撞”慧脱。
空閑列表:Java堆是不規(guī)整的渺绒,用過的內(nèi)存和空閑的內(nèi)存相互交錯。那就沒辦法進行指針碰撞菱鸥。虛擬機通過維護一個列表宗兼,記錄那些內(nèi)存塊是可用的,在分配時找出一塊足夠大的空間分配給對象實例采缚,并更新表上記錄针炉。這種分配方式稱為“空閑列表”挠他。
使用哪種分配方式由Java堆是否規(guī)整決定扳抽,Java堆規(guī)整不規(guī)整由垃圾收集器是否算法或者是否帶有壓縮整理功能決定。
分配對象保證線程安全的做法:虛擬機使用CAS失敗重試的機制保證更新操作的原子性。
3.每個線程在堆中都會有私有的分配緩沖區(qū)(TLAB),這樣可以很大程度避免在并發(fā)情況下頻繁創(chuàng)建 對象造成的線程不安全贸呢。
4.內(nèi)存空間分配完成后镰烧,會初始化0(不包括對象頭)
5.接下來就是填充對象頭,把對象是哪個類的實例楞陷、如何才能找到類的元數(shù)據(jù)信息怔鳖,對象的哈希碼,對象的GC分代年齡等信息存入對象頭中固蛾。
6.執(zhí)行<init>方法结执,把對象按程序員的意愿進行初始化。執(zhí)行完init方法后才算一份真正可用的對象創(chuàng)建完成艾凯。
2.對象內(nèi)存布局
java對象:對象頭+實例數(shù)據(jù)+對齊填充
對象頭:MarkWord (8字節(jié))+ 類指針(8字節(jié) 開啟壓縮4字節(jié)) + 數(shù)組對象長度數(shù)據(jù)(4字節(jié))
實例數(shù)據(jù):基礎(chǔ)類型占實際大小献幔,引用類型8字節(jié)(開啟壓縮4字節(jié))
對齊填充:填充到8的倍數(shù)。
3.對象的訪問定位
一般來說趾诗,一個Java的引用訪問涉及到3個內(nèi)存區(qū)域蜡感,JVM棧、堆恃泪、方法區(qū)
以最簡單的本地變量引用為例
Object obj = new Object();
Object obj表示一個本地引用郑兴,存儲在JVM棧的本地變量表中,表示一個reference類型數(shù)據(jù)贝乎。
new Object() 作為實例對象數(shù)據(jù)存儲在堆中情连。
堆中還記錄了能夠查詢到此Object對象的類型數(shù)據(jù)(接口、方法览效、field蒙具、對象類型等)的地址,實際的數(shù)據(jù)則存儲在方法區(qū)中朽肥。
在Java虛擬機規(guī)范中禁筏,只規(guī)定了指向?qū)ο蟮囊茫瑢τ趓eference類型引用訪問具體對象的方式并未做規(guī)定衡招。不過目前主流的有兩種:通過句柄飯顧問篱昔,使用直接指針訪問。
- 通過句柄訪問
通過句柄訪問的實現(xiàn)方式中始腾,JVM堆中會劃分單獨一塊內(nèi)存區(qū)域作為句柄池州刽,句柄池中存儲了對象實例數(shù)據(jù)(在堆中)和對象類型數(shù)據(jù)(在方法區(qū)中)的指針,這種實現(xiàn)方式由于用句柄表示地址浪箭,因此十分穩(wěn)定穗椅。Java堆中會分配一塊內(nèi)存作為句柄池。reference存儲的是句柄地址奶栖。詳情見圖匹表。
-
2 使用直接指針訪問
通過直接指針訪問的實現(xiàn)方式中门坷,reference中存儲的就是對象在堆中的實際地址,在堆中存儲的對象信息包含了在方法區(qū)中相應(yīng)類型數(shù)據(jù)袍镀。這種方法最大的優(yōu)勢就是速度快默蚌,HotSpot虛擬機中使用的就是這種方式。
通過直接指針訪問對象
比較
使用句柄的最大好處是reference中存儲的是穩(wěn)定的句柄地址苇羡,在對象移動(GC)是只改變實例數(shù)據(jù)指針地址绸吸,reference不需要修改。
直接指針好處就是速度快设江,節(jié)省了一次指針定位的開銷锦茁。
如果是對象頻繁GC那么句柄訪問好,如果是對象頻繁訪問那么是直接指針訪問好叉存。
4.方法執(zhí)行過程
public class Test {
public static void main (String args[]) {
Student stu = new Student();
stu.setName("John");
System.out.println(stu);
}
}
1.通過java.exe 運行Test.class,Test.class文件會被ApplassLoader加載器(雙親委派蜻势,啟動類加載器和擴展類加載器都不會加載它)加載到JVM中,元空間存儲著類信息(類名鹉胖,方法信息握玛,字段信息)
2.然后JVM找到Test的主函數(shù)入口(main),為main函數(shù)建立棧幀甫菠,開始執(zhí)行main函數(shù)挠铲。
3.main函數(shù)的第一條命令是Student stu = new Student();就是讓JVM創(chuàng)建一個Student對象寂诱,但是這時候方法區(qū)中沒有Student類信息拂苹,所以JVM會馬上加載Student類,把Student類的類型信息放到方法區(qū)中痰洒。
4.加載完Stundent類之后瓢棒,Java在堆區(qū)中為一個新的Student分配內(nèi)存,然后調(diào)用構(gòu)造函數(shù)初始化Student實例丘喻,這個Student實例持有者指向方法區(qū)Student類的類型信息的引用脯宿。
5.當使用stu.setName("John")的時候,JVM根據(jù)引用找到Student對象持有的引用定位到方法區(qū)中Student類的類型信息的方法表泉粉,獲得setName函數(shù)的字節(jié)碼地址连霉。
為setName函數(shù)創(chuàng)建棧幀,開始運行setName函數(shù)嗡靡。
5.HotSpot的GC算法實現(xiàn)
1.HotSpot怎么快速找到GC Root跺撼?
HotSpot使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)。引用會記錄在這個數(shù)據(jù)結(jié)構(gòu)上讨彼。
在類加載完成后歉井,HotSpot就把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來
在JIT編譯過程中,也會在棧和寄存器中哪些位置是引用哈误。
這樣子哩至,在GC掃描的時候躏嚎,就可以直接知道哪些是可達對象了。
2.安全點
hotspot只在特定的位置生成OopMap憨募,這些位置稱為安全點。
程序執(zhí)行過程中并非所有的地方都可以停下來開始GC袁辈,只有在到達安全點才可以暫停菜谣。
安全點的選定基本上以“是否具有讓程序長時間執(zhí)行”的特征選定的。比如說方法調(diào)用晚缩,循環(huán)跳轉(zhuǎn)尾膊,異常跳轉(zhuǎn)等。具有這些功能的指令才會產(chǎn)生Safepoint荞彼。
3.中斷方式
搶占式中斷:在GC發(fā)生時冈敛,首先把所有線程中斷,如果發(fā)現(xiàn)有線程不在安全點上鸣皂,就恢復線程抓谴,讓它跑到安全點上。
主動式中斷:GC需要中斷線程時寞缝,不直接對線程操作癌压,僅僅設(shè)置一個標志,各個線程執(zhí)行時主動去輪詢這個標志荆陆,當發(fā)現(xiàn)中斷標記為true時就自己掛起滩届,輪詢標記的地方和安全點是重合的。
4.安全區(qū)域
一段代碼片段中被啼,對象的引用關(guān)系不會發(fā)生變化帜消,在這個區(qū)域中任何地方開始GC都是安全的。
在線程進入安全區(qū)域時浓体,他首先標記自己已經(jīng)進入啊安全語氣泡挺,在這段時間里,當JVM發(fā)起GC時命浴,就不用管進入安全區(qū)域的線程了粘衬。
在線程將要離開安全區(qū)域時,他檢查系統(tǒng)是否完成了GC過程咳促,如果完成了稚新,就繼續(xù)前行。否則跪腹,他就必須等待收到可以離開安全區(qū)域的信號褂删。
5.GC時為什么要停頓所有Java線程
因為GC需要先進行可達性分析
可達性分析是判斷GC Root對象到其他對象是否可達
加入分析過程中對象引用關(guān)系在不斷變化,分析結(jié)果的準確性就無法得到保證冲茸。
6.值傳遞引用傳遞
1.形參與實參
- 1.形參:方法被調(diào)用是需要傳遞進來的參數(shù)屯阀,如doJob(int a)中的a缅帘,它只有在doJob被調(diào)用期間a才有意義,才會被分配內(nèi)存空間难衰,在方法doJob執(zhí)行完成后钦无,a就會被銷毀釋放空間,也就是不存在的盖袭。
- 2.實參:方法被調(diào)用時傳入的實際值失暂,它在方法被調(diào)用之前就已經(jīng)被初始化并且在方法被調(diào)用時傳入。
例如:
public static void doJob(int a ) {
a = 20;
System.out.println(a);
}
public static void main(String[] args){
int a = 10; //實參
doJob(a);
}
int a = 10;中的a在被調(diào)用之前就已經(jīng)創(chuàng)建并初始化鳄虱,在調(diào)用doJob時,被當做參數(shù)傳入弟塞,所以這個a是實參。
而doJob(int a)中的a,只有在doJob被調(diào)用時拙已,它的生命周期才開始决记,而在doJob結(jié)束之后,他也隨之被JVM釋放掉倍踪,所以這個a是形參系宫。
2.數(shù)據(jù)存儲策略
這里要分情況進行探究
- 1.基本數(shù)據(jù)類型的存儲
- A.基本數(shù)據(jù)類型的局部變量
- B.基本數(shù)據(jù)類型的全局變量
- C.基本數(shù)據(jù)類型的靜態(tài)變量
2.引用數(shù)據(jù)類型的存儲
- A.引用數(shù)據(jù)類型的局部變量
- B.引用數(shù)據(jù)類型的全局變量
- C.引用數(shù)據(jù)類型的靜態(tài)變量
1.基本數(shù)據(jù)類型的局部變量
定義基本數(shù)據(jù)類型的局部變量以及數(shù)據(jù)都是直接存儲在內(nèi)存中的棧上(局部變量表中),也就是運行時數(shù)據(jù)區(qū)的虛擬機棧上建车,數(shù)據(jù)本身值就存儲在局部變量表中笙瑟。
在方法內(nèi)定義的變量直接存儲在棧上,如
int age = 50;
int weight = 50;
int grade = 6;
當我們寫int age = 50癞志;
其實是分為兩步的:
int age;
age = 50;
首先JVM創(chuàng)建一個名為age的變量往枷,存于局部變量表中,然后去棧中查找是否存在有字面量值為50的內(nèi)容凄杯,如果有就直接把age只想這個地址错洁,如果沒有JVM會在棧中開辟一塊空間來存儲"50"這個內(nèi)容.并且把age指向這個地址。
聲明并初始化基本數(shù)據(jù)類型的局部變量時戒突,變量名以及字面量值都是存儲在棧中屯碴,而且是真實內(nèi)容
我們再來看int weight = 50;
按照剛才的思路,字面量為50的內(nèi)容已經(jīng)在棧中存在了膊存,因此weight直接指向這個地址导而。由此可見
棧中的數(shù)據(jù)在當前線程下是共享的
那么如果再執(zhí)行下面的代碼呢?
weight = 40;
當代碼中重新給weight變量進行復制時隔崎,JVM會去棧中尋找字面量為40的內(nèi)容今艺,發(fā)現(xiàn)沒有,就會開辟一塊內(nèi)存空間存儲40這個內(nèi)容爵卒,并且把weight只想這個地址虚缎,由此可知:
基本數(shù)據(jù)類型的局部變量,本身是不會改變的钓株,當重新賦值時实牡,并不是在內(nèi)存中改變字面量的內(nèi)容陌僵,而是重新尋找已存在的相同數(shù)據(jù),若不存在创坞,則重新開辟內(nèi)存存新數(shù)據(jù)碗短。并將引用指向新數(shù)據(jù)所在的地址
2.基本數(shù)據(jù)類型的成員變量
成員變量:就是在類體重定義的非靜態(tài)變量
我們看per的地址指向的是堆內(nèi)存中的一塊區(qū)域,我們來還原一下代碼
public class Person{
private int age;
private String name;
private int grade;
static void run(){
System.out.println("run....");
};
}
//調(diào)用
Person per=new Person();
成員變量age题涨、name偎谁、grade卻被存儲到了堆中為per對象開辟的一塊內(nèi)存空間。因此可知
基本數(shù)據(jù)類型的成員變量名和值都存儲于堆中携栋,其生命周期和對象是一致的搭盾。
3.基本數(shù)據(jù)類型的靜態(tài)變量
基本數(shù)據(jù)類型的靜態(tài)變量名以及值存儲在方法區(qū)的運行時常量池中咳秉,靜態(tài)變量隨類加載而加載婉支,隨類消失而消失。
4.引用數(shù)據(jù)類型
堆是用來存儲對象本身和數(shù)組澜建,而引用存放的是實際內(nèi)容的地址值向挖,因此當我們定義一個對象時
Person per = new Person();
實際上他也是有兩個過程
Person per;
per = new Person();
在執(zhí)行Person per時,JVM先在棧中變量表開辟一塊內(nèi)存存放per變量炕舵,在執(zhí)行per= new Person()時何之,JVM會創(chuàng)建一個Person類的實例對象并在堆中存儲這個實例,同時把實例地址賦值給per變量咽筋。
引用數(shù)據(jù)類型溶推,變量名都在棧中出現(xiàn),棧中的變量值存儲的是對象地址奸攻,并不是實際內(nèi)容蒜危。
5.值傳遞和引用傳遞
- 值傳遞:在方法被調(diào)用時,實參通過形參把它的內(nèi)容副本傳入方法內(nèi)部睹耐,此時形參接收到的內(nèi)容是實參值的一個拷貝辐赞,因此在方法內(nèi)對形參的任何操作,都僅僅是對這個副本的操作硝训,不影響原始值的內(nèi)容响委。
- 引用傳遞:引用也就是指向真實內(nèi)容的地址,在方法調(diào)用時窖梁,實參的地址通過方法調(diào)用被傳遞給相應(yīng)的形參赘风,在方法體內(nèi),形參和實參指向同一塊內(nèi)存地址纵刘,對形參的操作會影響實參的真實內(nèi)容贝次。
來看個例子:
public static void valueCrossTest(int age,float weight){
System.out.println("傳入的age:"+age);
System.out.println("傳入的weight:"+weight);
age=33;
weight=89.5f;
System.out.println("方法內(nèi)重新賦值后的age:"+age);
System.out.println("方法內(nèi)重新賦值后的weight:"+weight);
}
//測試
public static void main(String[] args) {
int a=25;
float w=77.5f;
valueCrossTest(a,w);
System.out.println("方法執(zhí)行后的age:"+a);
System.out.println("方法執(zhí)行后的weight:"+w);
}
輸出結(jié)果
傳入的age:25
傳入的weight:77.5
方法內(nèi)重新賦值后的age:33
方法內(nèi)重新賦值后的weight:89.5
方法執(zhí)行后的age:25
方法執(zhí)行后的weight:77.5
從上面打印結(jié)果可以看到
a和w作為實參傳入valueCrossTest之后,無論在方法內(nèi)做了什么操作彰导,最終a和w都沒變化蛔翅。
我們對上面例子進行詳細分析:
首先程序運行時敲茄,調(diào)用main()方法,此時JVM為main()方法往棧中壓入一個棧幀山析,用來存儲main()方法中的局部變量表堰燎,操作數(shù)棧,方法出口笋轨,動態(tài)鏈接等秆剪,a和w都存在main()方法局部變量表中。
而當執(zhí)行到ValueCrossTest()方法時爵政,JVM會再壓入一個棧幀仅讽,用來存放ValueCrossTest()中的局部變量等信息,因此age和weight是在valueCrossTest方法的局部變量表中钾挟。而它們的值是從a和w的值copy了一份副本而已洁灵。
因而a和age,w和weight對應(yīng)的實際內(nèi)容是不一致的掺出,所以當在方法內(nèi)重新賦值時徽千,實際流程為:
也就是說,age和weight的改動汤锨,只是改變了當前棧幀里的內(nèi)容双抽,當方法執(zhí)行結(jié)束后,這些局部變量都會被銷毀闲礼,main方法棧幀重新回到棧頂牍汹,稱為當前棧幀,再次輸出a和w時柬泽,依然是初始化時的內(nèi)容慎菲。
值傳遞傳遞的是真實內(nèi)容的副本,對副本操作不影響原內(nèi)容聂抢,也就是形參怎么變化钧嘶,不會影響實參的內(nèi)容
舉個例子:
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
寫個示例測試一下
public static void PersonCrossTest(Person person){
System.out.println("傳入的person的name:"+person.getName());
person.setName("我是張小龍");
System.out.println("方法內(nèi)重新賦值后的name:"+person.getName());
}
//測試
public static void main(String[] args) {
Person p=new Person();
p.setName("我是馬化騰");
p.setAge(45);
PersonCrossTest(p);
System.out.println("方法執(zhí)行后的name:"+p.getName());
}
輸出結(jié)果:
傳入的person的name:我是馬化騰
方法內(nèi)重新賦值后的name:我是張小龍
方法執(zhí)行后的name:我是張小龍
可以看出,person結(jié)果personCrossTest()方法的執(zhí)行之后琳疏,內(nèi)容發(fā)生了改變有决,看起來是引用傳遞的。
下面我們對上面的例子稍作修改空盼,加上一行代碼
public static void PersonCrossTest(Person person){
System.out.println("傳入的person的name:"+person.getName());
person=new Person();//加多此行代碼
person.setName("我是張小龍");
System.out.println("方法內(nèi)重新賦值后的name:"+person.getName());
}
輸出結(jié)果:
傳入的person的name:我是馬化騰
方法內(nèi)重新賦值后的name:我是張小龍
方法執(zhí)行后的name:我是馬化騰
為什么會不一樣了呢书幕?
程序執(zhí)行到main()方法中下列代碼時
Person p=new Person();
p.setName("我是馬化騰");
p.setAge(45);
是在main方法棧幀局部變量表中,創(chuàng)建一個引用p揽趾,p的值是地址台汇,是存儲在堆區(qū)中p對象的真實地址挤土。
當執(zhí)行到PersonCrossTest方法時搜骡,因為方法內(nèi)有這么一行代碼:
person=new Person();
JVM需要在堆內(nèi)另外開辟一塊內(nèi)存來存儲new Person(),加入地址為“xo3333”,那么此時形參指向了這個地址懦尝,加入真的是引用傳遞躲雅,那么由上面講到引用傳遞中形參實參指向同一對象劫乱,形參的操作會改變實參對象巡李,可以退出。實參也應(yīng)該指向新創(chuàng)建的person對象的地址笆呆。然而實際上并不是這樣请琳。由此可見引用傳遞在java中并不存在。
那么為什么第一個例子中赠幕,在方法內(nèi)修改了形參的內(nèi)容俄精,會導致原始對象的內(nèi)容發(fā)生改變?
是因為無論是基本類型還是引用類型榕堰,在實參傳入形參時竖慧,都是值傳遞,也就是說傳遞的都是一個副本局冰,而不是內(nèi)容本身
由圖可以看出测蘑,方法實參和形參并無實際關(guān)聯(lián)灌危,只是從p處拷貝了一份指向?qū)ο蟮牡刂房刀4藭r p和person都指向堆中同一個對象。
在第二個例子中勇蝙,當執(zhí)行到new Person()之后沫勿,JVM在堆內(nèi)開辟一塊空間存儲新對象,并且把person改成指向新對象的地址味混,此時 p依舊指向舊的地址产雹,person則指向了新的地址。
有個例子很形象的說明值傳遞和引用傳遞翁锡。
你有一把鑰匙蔓挖,當你的朋友想要去你家的時候,如果你直接把你的鑰匙給他了馆衔,這就是引用傳遞瘟判。這種情況下,如果他對這把鑰匙做了什么事情角溃,比如他在鑰匙上刻下了自己名字拷获,那么這把鑰匙還給你的時候,你自己的鑰匙上也會多出他刻的名字减细。
你有一把鑰匙匆瓜,當你的朋友想要去你家的時候,你復制了一把新鑰匙給他,自己的還在自己手里驮吱,這就是值傳遞茧妒。這種情況下,他對這把鑰匙做什么都不會影響你手里的這把鑰匙左冬。
但是嘶伟,不管上面那種情況,你的朋友拿著你給他的鑰匙又碌,進到你的家里九昧,把你家的電視砸了。那你說你會不會受到影響毕匀?而我們在方法中铸鹰,改變對象的屬性的值的時候,不就是在“砸電視”么皂岔。
總結(jié)
簡單來說:
如果參數(shù)是基本類型蹋笼,傳遞的是基本類型的字面量值的拷貝。
如果參數(shù)是引用類型躁垛,傳遞的是該參量所引用的對象在堆中地址值的拷貝剖毯。