Java崗 面試考點精講(基礎篇02期)

2020-10-23 14:00:31

1. 兩個物件的hashCode相同,則equals也一定為true,對嗎?

不對,答案見下面的程式碼:

@Override
public int hashCode() {
    return 1;
}

兩個物件equals為true,則hashCode也一定相同,對嗎?

這塊肯定是有爭議的。面試的時候這樣答:如果按照官方設計要求來打程式碼的話,hashcode一定相等。但是如果不按官方照設計要求、不重寫hashcode方法,就會出現不相等的情況。

2. java執行緒池用過沒有?

Executors提供了四種方法來建立執行緒池。

  1. newFixedThreadPool() :建立固定大小的執行緒池。
  2. newCachedThreadPool(): 建立無限大小的執行緒池,執行緒池中執行緒數量不固定,可根據需求自動更改。
  3. newSingleThreadPool() : 建立單個執行緒池,執行緒池中只有一個執行緒。
  4. newScheduledThreadPool() 建立固定大小的執行緒池,可以延遲或定時的執行任務。

手寫一個:

public static void main(String[] args) {

    ExecutorService threadPool = Executors.newCachedThreadPool();
    threadPool.execute(() -> {
        for (int i = 0; i< 20;i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    });
    threadPool.shutdown();
}

執行緒池作用

  1. 限制執行緒個數,避免執行緒過多導致系統執行緩慢或崩潰。
  2. 不需要頻繁的建立和銷燬,節約資源、響應更快。

大家覺得本次面試題總結的寫得不錯的朋友,大家可以轉發+關注,然後掃描下方二維條碼獲取更多面試題以及答案— 掃描新增暗號:【CSDN】

 

3. Math.round(-2.5)等於多少?

不要認為它是四捨五入!不要認為它是四捨五入!不要認為它是四捨五入!

口訣:+0.5後向下取整。所以結果是-2。

留個題,Math.round(-2.6)結果和Math.round(2.6)結果

4. 物件導向六大原則

  1. 單一職責原則——SRP

讓每個類只專心處理自己的方法。

  1. 開閉原則——OCP

軟體中的物件(類,模組,函數等)應該對於擴充套件是開放的,但是對於修改是關閉的。

  1. 裡式替換原則——LSP

子類可以去擴充套件父類別,但是不能改變父類別原有的功能。

  1. 依賴倒置原則——DIP

應該通過呼叫介面或抽象類(比較高層),而不是呼叫實現類(細節)。

  1. 介面隔離原則——ISP

把介面分成滿足依賴關係的最小介面,實現類中不能有不需要的方法。

  1. 迪米特原則——LOD

高內聚,低耦合。

5. static和final區別

關鍵詞修飾物影響
final變數分配到常數池中,程式不可改變其值
final方法子類中將不能被重寫
final不能被繼承
static變數分配在記憶體堆上,參照都會指向這一個地址而不會重新分配記憶體
static方法塊虛擬機器器優先載入
static可以直接通過類來呼叫而不需要new

6. String s = "hello"和String s = new String("hello");區別

String s = new String("hello");可能建立兩個物件也可能建立一個物件。如果常數池中有hello字串常數的話,則僅僅在堆中建立一個物件。如果常數池中沒有hello物件,則堆上和常數池都需要建立。

String s = "hello"這樣建立的物件,JVM會直接檢查字串常數池是否已有"hello"字串物件,如沒有,就分配一個記憶體存放"hello",如有了,則直接將字串常數池中的地址返回給棧。(沒有new,沒有堆的操作)

7. 參照型別是佔用幾個位元組?

hotspot在64位元平臺上,佔8個位元組,在32位元平臺上佔4個位元組。

8. `(1<3)?"a":"b")+3+4`和`(1<3)?"a":"b")+(3+4)`區別

System.out.println(((1<3)?"a":"b")+3+4);
System.out.println(((1<3)?"a":"b")+(3+4));

控制檯:

a34
a7

8.1 什麼情況下,加號會變成字串連線符

依據上面的例子來思考。

9. java中的switch選擇結構可以使用資料型別的資料(JDK1.8)

 

  1. char
  2. byte
  3. short
  4. int
  5. Character
  6. Byte
  7. Short
  8. Integer
  9. String
  10. enum

更好的記憶方法:

基本型別中,沒有boolean浮點型別+長型別long.相應的包裝型別也沒有。

外加Stringenum

10. `4&5``4^5``4&10>>1`各等於多少

// 0100 & 0101 = 0100 = 4
System.out.println(4&5);
// 0100 ^ 0101 = 0001 = 1
System.out.println(4^5);
System.out.println(10>>1);
 // 有疑問參考下面的運運算元優先順序
System.out.println(4&10>>1);
4
1
5
4

`4|5`等於多少呢

答案:5

運運算元優先順序

運運算元結合性
[ ] . ( ) (方法呼叫)從左向右
! ~ ++ -- +(一元運算) -(一元運算)從右向左
* / %從左向右
+ -從左向右
<< >> >>>從左向右
< <= > >= instanceof從左向右
== !=從左向右
&從左向右
^從左向右
|從左向右
&&從左向右
||從左向右
?:從右向左
=從右向左

11. 某些java類為什麼要實現Serializable介面

為了網路進行傳輸或者持久化

什麼是序列化

將物件的狀態資訊轉換為可以儲存或傳輸的形式的過程

除了實現Serializable介面還有什麼序列化方式

  • Json序列化
  • FastJson序列化
  • ProtoBuff序列化

12. JVM垃圾處理方法

標記-清除演演算法(老年代)

該演演算法分為「標記」和「清除」兩個階段: 首先標記出所有需要回收的物件(可達性分析), 在標記完成後統一清理掉所有被標記的物件.

 

該演演算法會有兩個問題:

  1. 效率問題,標記和清除效率不高。
  2. 空間問題: 標記清除後會產生大量不連續的記憶體碎片, 空間碎片太多可能會導致在執行過程中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集。

所以它一般用於"垃圾不太多的區域,比如老年代"。

複製演演算法(新生代)

該演演算法的核心是將可用記憶體按容量劃分為大小相等的兩塊, 每次只用其中一塊, 當這一塊的記憶體用完, 就將還存活的物件(非垃圾)複製到另外一塊上面, 然後把已使用過的記憶體空間一次清理掉。

優點:不用考慮碎片問題,方法簡單高效。

缺點:記憶體浪費嚴重。

現代商用VM的新生代均採用複製演演算法,但由於新生代中的98%的物件都是生存週期極短的,因此並不需完全按照1∶1的比例劃分新生代空間,而是將新生代劃分為一塊較大的Eden區和兩塊較小的Survivor區(HotSpot預設Eden和Survivor的大小比例為8∶1), 每次只用Eden和其中一塊Survivor。

當發生MinorGC時,將Eden和Survivor中還存活著的物件一次性地拷貝到另外一塊Survivor上, 最後清理掉Eden和剛才用過的Survivor的空間。當Survivor空間不夠用(不足以儲存尚存活的物件)時,需要依賴老年代進行空間分配擔保機制,這部分記憶體直接進入老年代。

 

複製演演算法的空間分配擔保:
在執行Minor GC前, VM會首先檢查老年代是否有足夠的空間存放新生代尚存活物件, 由於新生代使用複製收集演演算法, 為了提升記憶體利用率, 只使用了其中一個Survivor作為輪換備份, 因此當出現大量物件在Minor GC後仍然存活的情況時, 就需要老年代進行分配擔保, 讓Survivor無法容納的物件直接進入老年代, 但前提是老年代需要有足夠的空間容納這些存活物件.

但存活物件的大小在實際完成GC前是無法明確知道的, 因此Minor GC前, VM會先首先檢查老年代連續空間是否大於新生代物件總大小或歷次晉升的平均大小, 如果條件成立, 則進行Minor GC, 否則進行Full GC(讓老年代騰出更多空間).

然而取歷次晉升的物件的平均大小也是有一定風險的, 如果某次Minor GC存活後的物件突增,遠遠高於平均值的話,依然可能導致擔保失敗(Handle Promotion Failure, 老年代也無法存放這些物件了), 此時就只好在失敗後重新發起一次Full GC(讓老年代騰出更多空間).


標記-整理演演算法(老年代)

標記清除演演算法會產生記憶體碎片問題, 而複製演演算法需要有額外的記憶體擔保空間, 於是針對老年代的特點, 又有了標記整理演演算法. 標記整理演演算法的標記過程與標記清除演演算法相同, 但後續步驟不再對可回收物件直接清理, 而是讓所有存活的物件都向一端移動,然後清理掉端邊界以外的記憶體.

 

13. 新生代、老年代、持久代都儲存哪些東西

新生代:

  1. 方法中new一個物件,就會先進入新生代。

老年代:

  1. 新生代中經歷了N次垃圾回收仍然存活的物件就會被放到老年代中。
  2. 大物件一般直接放入老年代。
  3. 當Survivor空間不足。需要老年代擔保一些空間,也會將物件放入老年代。

永久代:

指的就是方法區。

14. 可達性演演算法中,哪些物件可作為GC Roots物件。

  1. 虛擬機器器棧中參照的物件
  2. 方法區靜態成員參照的物件
  3. 方法區常數參照物件
  4. 本地方法棧JNI參照的物件

15. 什麼時候進行MinGC和FullGC

MinGC:

  1. 當Eden區滿時,觸發Minor GC.

FullGC:

  1. 呼叫System.gc時,系統建議執行Full GC,但是不必然執行
  2. 老年代空間不足
  3. 方法區空間不足
  4. 通過Minor GC後進入老年代的平均大小大於老年代的剩餘空間
  5. 堆中分配很大的物件,而老年代沒有足夠的空間

大家覺得本次面試題總結的寫得不錯的朋友,大家可以轉發+關注,然後掃描下方二維條碼獲取更多面試題以及答案— 掃描新增暗號:【CSDN】

 

16. 如何判定物件為垃圾物件

在堆裡面存放著Java世界中幾乎所有的物件範例, 垃圾收集器在對堆進行回收前, 第一件事就是判斷哪些物件已死(可回收).

參照計數法

在JDK1.2之前,使用的是參照計數器演演算法。

在物件中新增一個參照計數器,當有地方參照這個物件的時候,參照計數器的值就+1,當參照失效的時候,計數器的值就-1,當參照計數器被減為零的時候,標誌著這個物件已經沒有參照了,可以回收了!


問題:如果在A類中呼叫B類的方法,B類中呼叫A類的方法,這樣當其他所有的參照都消失了之後,A和B還有一個相互的參照,也就是說兩個物件的參照計數器各為1,而實際上這兩個物件都已經沒有額外的參照,已經是垃圾了。但是該演演算法並不會計算出該型別的垃圾。

可達性分析法

在主流商用語言(如Java、C#)的主流實現中, 都是通過可達性分析演演算法來判定物件是否存活的: 通過一系列的稱為 GC Roots 的物件作為起點, 然後向下搜尋; 搜尋所走過的路徑稱為參照鏈/Reference Chain, 當一個物件到 GC Roots 沒有任何參照鏈相連時, 即該物件不可達, 也就說明此物件是不可用的, 如下圖:雖然E和F相互關聯, 但它們到GC Roots是不可達的, 因此也會被判定為可回收的物件。

 

注: 即使在可達性分析演演算法中不可達的物件, VM也並不是馬上對其回收, 因為要真正宣告一個物件死亡, 至少要經歷兩次標記過程: 第一次是在可達性分析後發現沒有與GC Roots相連線的參照鏈, 第二次是GC對在F-Queue執行佇列中的物件進行的小規模標記(物件需要覆蓋finalize()方法且沒被呼叫過).

17. 你能說出來幾個垃圾收集器

Serial

Serial收集器是Hotspot執行在Client模式下的預設新生代收集器, 它在進行垃圾收集時,會暫停所有的工作程序,用一個執行緒去完成GC工作

 

特點:簡單高效,適合jvm管理記憶體不大的情況(十兆到百兆)。

Parnew

ParNew收集器其實是Serial的多執行緒版本,回收策略完全一樣,但是他們又有著不同。

 

我們說了Parnew是多執行緒gc收集,所以它配合多核心的cpu效果更好,如果是一個cpu,他倆效果就差不多。(可用-XX:ParallelGCThreads引數控制GC執行緒數)

Cms

CMS(Concurrent Mark Sweep)收集器是一款具有劃時代意義的收集器, 一款真正意義上的並行收集器, 雖然現在已經有了理論意義上表現更好的G1收集器, 但現在主流網際網路企業線上選用的仍是CMS(如Taobao),又稱多並行低暫停的收集器。

 

由他的英文組成可以看出,它是基於標記-清除演演算法實現的。整個過程分4個步驟:

  1. 初始標記(CMS initial mark):僅只標記一下GC Roots能直接關聯到的物件, 速度很快
  2. 並行標記(CMS concurrent mark: GC Roots Tracing過程)
  3. 重新標記(CMS remark):修正並行標記期間因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記記錄
  4. 並行清除(CMS concurrent sweep: 已死物件將會就地釋放)

可以看到,初始標記、重新標記需要STW(stop the world 即:掛起使用者執行緒)操作。因為最耗時的操作是並行標記和並行清除。所以總體上我們認為CMS的GC與使用者執行緒是並行執行的。

優點:並行收集、低停頓

缺點:

  1. CMS預設啟動的回收執行緒數=(CPU數目+3)*4
    當CPU數>4時, GC執行緒最多佔用不超過25%的CPU資源, 但是當CPU數<=4時, GC執行緒可能就會過多的佔用使用者CPU資源, 從而導致應用程式變慢, 總吞吐量降低.
  2. 無法清除浮動垃圾(GC執行到並行清除階段時使用者執行緒產生的垃圾),因為使用者執行緒是需要記憶體的,如果浮動垃圾施放不及時,很可能就造成記憶體溢位,所以CMS不能像別的垃圾收集器那樣等老年代幾乎滿了才觸發,CMS提供了引數-XX:CMSInitiatingOccupancyFraction來設定GC觸發百分比(1.6後預設92%),當然我們還得設定啟用該策略-XX:+UseCMSInitiatingOccupancyOnly
  3. 因為CMS採用標記-清除演演算法,所以可能會帶來很多的碎片,如果碎片太多沒有清理,jvm會因為無法分配大物件記憶體而觸發GC,因此CMS提供了-XX:+UseCMSCompactAtFullCollection引數,它會在GC執行完後接著進行碎片整理,但是又會有個問題,碎片整理不能並行,所以必須單執行緒去處理,所以如果每次GC完都整理使用者執行緒stop的時間累積會很長,所以XX:CMSFullGCsBeforeCompaction引數設定隔幾次GC進行一次碎片整理(預設為0)。

G1

同優秀的CMS垃圾回收器一樣,G1也是關注最小時延的垃圾回收器,也同樣適合大尺寸堆記憶體的垃圾收集,官方也推薦使用G1來代替選擇CMS。G1最大的特點是引入分割區的思路,弱化分代的概念,合理利用垃圾收集各個週期的資源,解決了其他收集器甚至CMS的眾多缺陷。

 

因為每個區都有E、S、O代,所以在G1中,不需要對整個Eden等代進行回收,而是尋找可回收物件比較多的區,然後進行回收(雖然也需要STW操作,但是花費的時間是很少的),保證高效率。

新生代收集

G1的新生代收集跟ParNew類似,如果存活時間超過某個閾值,就會被轉移到S/O區。

年輕代記憶體由一組不連續的heap區組成, 這種方法使得可以動態調整各代區域的大小

老年代收集

分為以下幾個階段:

  1. 初始標記 (Initial Mark: Stop the World Event)
    在G1中, 該操作附著一次年輕代GC, 以標記Survivor中有可能參照到老年代物件的Regions.
  2. 掃描根區域 (Root Region Scanning: 與應用程式並行執行)
    掃描Survivor中能夠參照到老年代的references. 但必須在Minor GC觸發前執行完
  3. 並行標記 (Concurrent Marking : 與應用程式並行執行)
    在整個堆中查詢存活物件, 但該階段可能會被Minor GC中斷
  4. 重新標記 (Remark : Stop the World Event)
    完成堆記憶體中存活物件的標記. 使用snapshot-at-the-beginning(SATB, 起始快照)演演算法, 比CMS所用演演算法要快得多(空Region直接被移除並回收, 並計算所有區域的活躍度).
  5. 清理 (Cleanup : Stop the World Event and Concurrent)
    在含有存活物件和完全空閒的區域上進行統計(STW)、擦除Remembered Sets(使用Remembered Set來避免掃描全堆,每個區都有對應一個Set用來記錄參照資訊、讀寫操作記錄)(STW)、重置空regions並將他們返還給空閒列表(free list)(Concurrent)

詳情請看參考檔案

http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html#t5

18. JVM中物件的建立過程

1. 拿到記憶體建立指令

當虛擬機器器遇到記憶體建立的指令的時候(new 類名),來到了方法區,找 根據new的引數在常數池中定位一個類的符號參照。

2. 檢查符號參照

檢查該符號參照有沒有被載入、解析和初始化過,如果沒有則執行類載入過程,否則直接準備為新的物件分配記憶體

3. 分配記憶體

虛擬機器器為物件分配記憶體(堆)分配記憶體分為指標碰撞和空閒列表兩種方式;分配記憶體還要要保證並行安全,有兩種方式。

3.1. 指標碰撞

所有的儲存空間分為兩部分,一部分是空閒,一部分是佔用,需要分配空間的時候,只需要計算指標移動的長度即可。

3.2. 空閒列表

虛擬機器器維護了一個空閒列表,需要分配空間的時候去查該空閒列表進行分配並對空閒列表做更新。

可以看出,記憶體分配方式是由java堆是否規整決定的,java堆的規整是由垃圾回收機制來決定的

3.2.5 安全性問題的思考

假如分配記憶體策略是指標碰撞,如果在高並行情況下,多個物件需要分配記憶體,如果不做處理,肯定會出現執行緒安全問題,導致一些物件分配不到空間等。

下面是解決方案:

3.3 執行緒同步策略

也就是每個執行緒都進行同步,防止出現執行緒安全。

3.4. 本地執行緒分配緩衝

也稱TLAB(Thread Local Allocation Buffer),在堆中為每一個執行緒分配一小塊獨立的記憶體,這樣以來就不存並行問題了,Java 層面與之對應的是 ThreadLocal 類的實現

4. 初始化

  1. 分配完記憶體後要對物件的頭(Object Header)進行初始化,這新資訊包括:該物件對應類的後設資料、該物件的GC代、物件的雜湊碼。
  2. 抽象資料型別預設初始化為null,基本資料型別為0,布林為false....

5. 呼叫物件的初始化方法

也就是執行構造方法。

以上便是此次分享的面試題以及答案,如果覺得還不過癮,大家可以關注我的公眾號-【Java爛豬皮】,裡面有往期的面試題以及最新的面試分享,關注後回覆:【666】即可免費獲取更多的Java架構進階vip學習資料