1.前言
這是最近面試騰訊日常實(shí)習(xí)的一個面試題拾因,自己答得不是很好婴氮,故重新分析下。
題目:java的內(nèi)存模型有了解過嗎盾致?說說new 一個對象,并調(diào)用方法返回一個值荣暮,這之中jvm做了什么庭惜,棧楨變化是怎么樣的?
知識點(diǎn)主要是jvm的內(nèi)存模型穗酥、對象加載過程护赊、虛擬機(jī)執(zhí)行引擎,自己雖然也啃過《JVM高級特性與最佳實(shí)踐》這本書砾跃,內(nèi)存模型和對象加載過程答得還行骏啰,棧楨變化的細(xì)節(jié)沒答好,故本篇側(cè)重于棧楨變化的分析抽高,其他兩個問題直接給出答案判耕。
1.1 java內(nèi)存模型
java內(nèi)存模型分為線程公有和線程私有,線程公有包括gc堆和方法區(qū)翘骂,線程私有包括操作計(jì)數(shù)器壁熄、虛擬機(jī)棧、本地方法棧碳竟,如下圖草丧。
1.2 new一個對象,jvm都做了什么
其中莹桅,類加載包括加載昌执、驗(yàn)證、準(zhǔn)備诈泼、解析懂拾、初始化,不是本篇的重點(diǎn)铐达,這里不再展開委粉,有興趣的同學(xué)可以看看《JVM高級特性與最佳實(shí)踐》第九章。
2.分析
接下來主要分析new一個對象并調(diào)用方法娶桦,棧楨變化贾节。
簡單分析代碼如下汁汗,聲明成員變量主要分析init
方法賦值順序:
public class StackObserver {
static class A {
int a1 = 1;
int a2 = 2;
int a3;
int a4;
public A() {
a3 = 3;
}
{
a4 = 4;
}
int b(){
return 1;
}
}
public static void main(String[] args) {
new A().b();
}
}
通過IDE debug簡單分析下棧楨變化,new的時候init
方法壓入棧楨栗涂,執(zhí)行b方法時init
出棧知牌,b()
壓入棧楨。如下圖:
init方法
init
方法是對象初始化的過程斤程,包括成員變量賦值角寸、對象代碼塊即{}
、構(gòu)造方法拼接起來的初始化方法忿墅,拼接過程是由jvm幫我們做的扁藕。對應(yīng)還有一個class init
方法,是在類加載階段執(zhí)行疚脐,包括類變量的賦值亿柑、類靜態(tài)代碼塊的執(zhí)行。成員變量和全局變量是如何傳遞進(jìn)方法中的棍弄?
這個問題之前學(xué)習(xí)jvm的時候真的沒有仔細(xì)思考過望薄,果然細(xì)節(jié)決定成敗。
首先我們先思考下實(shí)例方法中呼畸,this
變量是怎樣傳遞進(jìn)方法的作用域中的痕支。修改下代碼,如下:
public class StackObserver {
static int GLOBAL = 4;
static class A {
...
int b(){
return a1;
}
int c() {
// 返回全局變量
return GLOBAL;
}
}
public static void main(String[] args) {
// = new A().b();
A a = new A();
a.b();
new A().c();
}
}
this
變量的傳遞我們需要先了解下局部變量表蛮原,它屬于棧楨的一部分卧须,用于傳遞變量,存儲單位為Slot
儒陨、32位故慈,存儲類型包括java基本數(shù)據(jù)類型,即int float boolean char之類的框全,用1Slot存儲察绷,但double long用2Slot存儲,reference即對象引用津辩,用1Slot存儲拆撼。
在每個方法執(zhí)行時,方法棧楨都會附帶一個局部變量表喘沿,大小為jvm自動計(jì)算闸度,其中,Slot是可以復(fù)用的(減少不必要的內(nèi)存占用)蚜印。
回到原問題莺禁,我們可以猜測,成員變量的傳遞是通過this
引用傳遞的窄赋,類變量是通過類引用變量傳遞的哟冬。通過查閱資料可知楼熄,在a調(diào)用b方法時,會將自身引用——this
作為第0位Slot傳遞給b方法棧楨的局部變量表浩峡。
那么我們驗(yàn)證下猜測可岂,通過javac編譯源代碼得到class文件。
// 以下為編譯的class文件翰灾,而非java文件
public class StackObserver {
static int GLOBAL = 4;
public StackObserver() {
}
public static void main(String[] args) {
StackObserver.A a = new StackObserver.A();
a.b();
(new StackObserver.A()).c();
}
static class A {
....
int b() {
return this.a1; // 猜測正確
}
int c() {
return StackObserver.GLOBAL; // 猜測正確
}
}
}
- return語句執(zhí)行時缕粹,棧楨的變化
將返回值壓入上層棧楨,這里的情景下上層棧楨是main()纸淮,如果有聲明局部變量賦值的話平斩,返回值會被記錄在局部變量表中;如沒有咽块,會被舍棄掉绘面。引用《JVM高級特性與最佳實(shí)踐》第八章的一段話。
方法退出
方法退出的過程實(shí)際上就等同于把當(dāng)前棧幀出棧糜芳,因此退出時可 能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值 (如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中魄衅,調(diào)整PC計(jì)數(shù)器的值以 指向方法調(diào)用指令后面的一條指令等峭竣。
3.總結(jié)
以上就是我個人對該問題的全部分析和總結(jié)啦,如果還沒了解過jvm的晃虫,強(qiáng)烈建議閱讀《JVM高級特性與最佳實(shí)踐》
這本書皆撩,電子版pdf可以私我,還是希望大家支持正版哲银。
面試的時候我們也不必把所有細(xì)節(jié)都一一列舉出來扛吞,如上題,只要把涉及的知識點(diǎn)java內(nèi)存布局荆责、對象加載過程滥比、虛擬機(jī)執(zhí)行引擎(棧楨部分)簡單說下,然后選一個方向展開(這里可以是面試官的二次提問做院,也可以的個人延伸)盲泛,我在面試時由于對棧楨這部分細(xì)節(jié)掌握不好,就被問傻了键耕。寺滚。。
最后屈雄,分析問題時一定要自己好好思考村视。結(jié)論不是最重要的,思考的過程才是