1. 程式人生 > >面試直通阿里:Java必考系列之JVM經典面試題目及答案

面試直通阿里:Java必考系列之JVM經典面試題目及答案

1.堆:存放物件例項,幾乎所有的物件例項都在這裡分配記憶體

 

 

 

  • 堆得記憶體由-Xms指定,預設是實體記憶體的1/64;最大的記憶體由-Xmx指定,預設是實體記憶體的1/4。
  • 預設空餘的堆記憶體小於40%時,就會增大,直到-Xmx設定的記憶體。具體的比例可以由-XX:MinHeapFreeRatio指定
  • 空餘的記憶體大於70%時,就會減少記憶體,直到-Xms設定的大小。具體由-XX:MaxHeapFreeRatio指定。

2.虛擬機器棧

虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於儲存區域性變量表、操作棧、動態連結、方法出口等資訊本地方法棧:本地方法棧則是為虛擬機器使用到的Native方法服務。

3.方法區:儲存已被虛擬機器載入的類元資料資訊(元空間)

1)有時候也成為永久代,在該區內很少發生垃圾回收,但是並不代表不發生GC,在這裡進行的GC主要是對方法區裡的常量池和對型別的解除安裝

2)方法區主要用來儲存已被虛擬機器載入的類的資訊、常量、靜態變數和即時編譯器編譯後的程式碼等資料。

該區域是被執行緒共享的。

3)方法區裡有一個執行時常量池,用於存放靜態編譯產生的字面量和符號引用。該常量池具有動態性,也就是說常量並不一定是編譯時確定,執行時生成的常量也會存在這個常量池中。

4.程式計數器:當前執行緒所執行的位元組碼的行號指示器

 

JVM垃圾回收演算法

1.標記-清除: 這是垃圾收集演算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被回收的物件,然後統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的記憶體碎片,導致以後程式在分配較大的物件時,由於沒有充足的連續記憶體而提前觸發一次GC動作。

2.複製演算法: 為了解決效率問題,複製演算法將可用記憶體按容量劃分為相等的兩部分,然後每次只使用其中的一塊,當一塊記憶體用完時,就將還存活的物件複製到第二塊記憶體上,然後一次性清楚完第一塊記憶體,再將第二塊上的物件複製到第一塊。但是這種方式,記憶體的代價太高,每次基本上都要浪費一般的記憶體。 於是將該演算法進行了改進,記憶體區域不再是按照1:1去劃分,而是將記憶體劃分為8:1:1三部分,較大那份記憶體交Eden區,其餘是兩塊較小的記憶體區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將物件複製到第二塊記憶體區上,然後清除Eden區,如果此時存活的物件太多,以至於Survivor不夠時,會將這些物件通過分配擔保機制複製到老年代中。(java堆又分為新生代和老年代)

3. 標記-整理 該演算法主要是為了解決標記-清除,產生大量記憶體碎片的問題;當物件存活率較高時,也解決了複製演算法的效率問題。它的不同之處就是在清除物件的時候現將可回收物件移動到一端,然後清除掉端邊界以外的物件,這樣就不會產生記憶體碎片了。

4.分代收集 現在的虛擬機器垃圾收集大多采用這種方式,它根據物件的生存週期,將堆分為新生代和老年代。在新生代中,由於物件生存期短,每次回收都會有大量物件死去,那麼這時就採用複製演算法。老年代裡的物件存活率較高,沒有額外的空間進行分配擔保,所以可以使用標記-整理 或者 標記-清除。

JVM垃圾收集器有哪些?以及優劣勢比較?

1.序列收集器

序列收集器是最簡單的,它設計為在單核的環境下工作(32位或者windows),你幾乎不會使用到它。它在工作的時候會暫停整個應用的執行,因此在所有伺服器環境下都不可能被使用。

使用方法:-XX:+UseSerialGC

2.並行收集器

這是JVM預設的收集器,跟它名字顯示的一樣,它最大的優點是使用多個執行緒來掃描和壓縮堆。缺點是在minor和full GC的時候都會暫停應用的執行。並行收集器最適合用在可以容忍程式停滯的環境使用,它佔用較低的CPU因而能提高應用的吞吐(throughput)。

使用方法:-XX:+UseParallelGC

3.CMS收集器

CMS是Concurrent-Mark-Sweep的縮寫,併發的標記與清除。

這個演算法使用多個執行緒併發地(concurrent)掃描堆,標記不使用的物件,然後清除它們回收記憶體。在兩種情況下會使應用暫停(Stop the World, STW):

1. 當初次開始標記根物件時initial mark。

2. 當在並行收集時應用又改變了堆的狀態時,需要它從頭再確認一次標記了正確的物件final remark。

這個收集器最大的問題是在年輕代與老年代收集時會出現的一種競爭情況(race condition),稱為提升失敗promotion failure。物件從年輕代複製到老年代稱為提升promotion,但有時侯老年代需要清理出足夠空間來放這些物件,這需要一定的時間,它收集的速度可能趕不上不斷產生的要提升的年輕代物件的速度,這時就需要做STW的收集。STW正是CMS想避免的問題。為了避免這個問題,需要增加老年代的空間大小或者增加更多的執行緒來做老年代的收集以趕上從年輕代複製物件的速度。

除了上文所說的內容之外,CMS最大的問題就是記憶體空間碎片化的問題。CMS只有在觸發FullGC的情況下才會對堆空間進行compact。如果線上應用長時間執行,碎片化會非常嚴重,會很容易造成promotion failed。為了解決這個問題線上很多應用通過定期重啟或者手工觸發FullGC來觸發碎片整理。

對比並行收集器它的一個壞處是需要佔用比較多的CPU。對於大多數長期執行的伺服器應用來說,這通常是值得的,因為它不會導致應用長時間的停滯。但是它不是JVM的預設的收集器。

4.G1收集器

如果你的堆記憶體大於4G的話,那麼G1會是要考慮使用的收集器。它是為了更好支援大於4G堆記憶體在JDK 7 u4引入的。G1收集器把堆分成多個區域,大小從1MB到32MB,並使用多個後臺執行緒來掃描這些區域,優先會掃描最多垃圾的區域,這就是它名稱的由來,垃圾優先Garbage First。

如果在後臺執行緒完成掃描之前堆空間耗光的話,才會進行STW收集。它另外一個優點是它在處理的同時會整理壓縮堆空間,相比CMS只會在完全STW收集的時候才會這麼做。

使用過大的堆記憶體在過去幾年是存在爭議的,很多開發者從單個JVM分解成使用多個JVM的微服務(micro-service)和基於元件的架構。其他一些因素像分離程式元件、簡化部署和避免重新載入類到記憶體的考慮也促進了這樣的分離。

除了這些因素,最大的因素當然是避免在STW收集時JVM使用者執行緒停滯時間過長,如果你使用了很大的堆記憶體的話就可能出現這種情況。另外,像Docker那樣的容器技術讓你可以在一臺物理機器上輕鬆部署多個應用也加速了這種趨勢。

使用方法:-XX:+UseG1GC

面試題及題目

1、一個".java"原始檔中是否可以包括多個類(不是內部類)?有什麼限制?

可以有多個類,但只能有一個public的類,並且public的類名必須與檔名相一致。

2、Java有沒有goto?

java中的保留字,現在沒有在java中使用。

3、說說&和&&的區別。

&和&&都可以用作邏輯與的運算子,表示邏輯與(and),當運算子兩邊的表示式的結果都為true時,整個運算結果才為true,否則,只要有一方為false,則結果為false。

&&還具有短路的功能,即如果第一個表示式為false,則不再計算第二個表示式,例如,對於if(str!= null&& !str.equals(s))表示式,當str為null時,後面的表示式不會執行,所以不會出現NullPointerException如果將&&改為&,則會丟擲NullPointerException異常。If(x==33 &++y>0) y會增長,If(x==33 && ++y>0)不會增長

&還可以用作位運算子,當&操作符兩邊的表示式不是boolean型別時,&表示按位與操作,我們通常使用0x0f來與一個整數進行&運算,來獲取該整數的最低4個bit位,例如,0x31 & 0x0f的結果為0x01。

4、在JAVA中如何跳出當前的多重巢狀迴圈?

在Java中,要想跳出多重迴圈,可以在外面的迴圈語句前定義一個標號,然後在裡層迴圈體的程式碼中使用帶有標號的break語句,即可跳出外層迴圈。

例如:

for(int i=0;i<10;i++){
 for(intj=0;j<10;j++){
 System.out.println(“i=” + i + “,j=” + j);
 if(j == 5) break ok;
 }
}

另外,我個人通常並不使用標號這種方式,而是讓外層的迴圈條件表示式的結果可以受到裡層迴圈體程式碼的控制,例如,要在二維陣列中查詢到某個數字。

int arr[][] ={{1,2,3},{4,5,6,7},{9}};
boolean found = false;
for(int i=0;i<arr.length&&!found;i++) {
 for(intj=0;j<arr[i].length;j++){
 System.out.println(“i=” + i + “,j=” + j);
 if(arr[i][j] ==5) {
 found =true;
 break;
 }
 }
}

5、switch語句能否作用在byte上,能否作用在long上,能否作用在String上?

在switch(e)中,e只能是一個整數表示式或者列舉常量(更大字型),整數表示式可以是int基本型別或Integer包裝型別,由於byte,short,char都可以隱含轉換為int,所以,這些型別以及這些型別的包裝型別也是可以的。顯然,long和String型別都不符合switch的語法規定,並且不能被隱式轉換成int型別,所以,它們不能作用於swtich語句中。

switch語句能否作用在String上說錯了,Java1.7之後已經支援這種寫法了!

6、short s1= 1; s1 = (s1+1是int型別,而等號左邊的是short型別,所以需要強轉)1 + 1;有什麼錯? short s1 = 1; s1 += 1;有什麼錯?(沒有錯)

對於short s1= 1; s1 = s1 + 1;由於s1+1運算時會自動提升表示式的型別,所以結果是int型,再賦值給short型別s1時,編譯器將報告需要強制轉換型別的錯誤。

對於short s1= 1; s1 += 1;由於 +=是java語言規定的運算子,java編譯器會對它進行特殊處理,因此可以正確編譯。

7、char型變數中能不能存貯一箇中文漢字?為什麼?

char型變數是用來儲存Unicode編碼的字元的,unicode編碼字符集中包含了漢字,所以,char型變數中當然可以儲存漢字啦。不過,如果某個特殊的漢字沒有被包含在unicode編碼字符集中,那麼,這個char型變數中就不能儲存這個特殊漢字。補充說明:unicode編碼佔用兩個位元組,所以,char型別的變數也是佔用兩個位元組。

8、用最有效率的方法算出2乘以8等於幾?

2<< 3,(左移三位)因為將一個數左移n位,就相當於乘以了2的n次方,那麼,一個數乘以8只要將其左移3位即可,而位運算cpu直接支援的,效率最高,所以,2乘以8等於幾的最效率的方法是2<< 3。

9、使用final關鍵字修飾一個變數時,是引用不能變,還是引用的物件不能變?

使用final關鍵字修飾一個變數時,是指引用變數不能變,引用變數所指向的物件中的內容還是可以改變的。例如,對於如下語句:

 finalStringBuffer a=new StringBuffer("immutable");

執行如下語句將報告編譯期錯誤:

a=new StringBuffer("");

但是,執行如下語句則可以通過編譯:

a.append(" broken!");

有人在定義方法的引數時,可能想採用如下形式來阻止方法內部修改傳進來的引數物件:

public void method(final StringBuffer param){
}

實際上,這是辦不到的,在該方法內部仍然可以增加如下程式碼來修改引數物件:

 param.append("a");

10,靜態變數和例項變數的區別?

在語法定義上的區別:靜態變數前要加static關鍵字,而例項變數前則不加。

在程式執行時的區別:例項變數屬於某個物件的屬性,必須建立了例項物件,其中的例項變數才會被分配空間,才能使用這個例項變數。靜態變數不屬於某個例項物件,而是屬於類,所以也稱為類變數,只要程式載入了類的位元組碼,不用建立任何例項物件,靜態變數就會被分配空間,靜態變數就可以被使用了。總之,例項變數必須建立物件後才可以通過這個物件來使用,靜態變數則可以直接使用類名來引用。

例如,對於下面的程式,無論建立多少個例項物件,永遠都只分配了一個staticVar變數,並且每建立一個例項物件,這個staticVar就會加1;但是,每建立一個例項物件,就會分配一個instanceVar,即可能分配多個instanceVar,並且每個instanceVar的值都只自加了1次。

public class VariantTest{
 publicstatic int staticVar = 0;
 publicint instanceVar = 0;
 publicVariantTest(){
 staticVar++;
 instanceVar++;
 System.out.println(staticVar +instanceVar);
 }
}

喜歡的話請幫忙轉發一下能讓更多有需要的人看到吧。有些技術上的問題大家可以多探討一下,謝謝!
通過不同的面試,記錄下自己的面試過程從而才能更好的提升自己的。也祝你也能找到一個好工作。
更多問題平時可以在QQ群裡交流:909723834。有加群的朋友請記得備註上CSDN,謝謝。