不對,答案見下面的程式碼:
@Override
public int hashCode() {
return 1;
}
兩個物件equals為true,則hashCode也一定相同,對嗎?
這塊肯定是有爭議的。面試的時候這樣答:如果按照官方設計要求來打程式碼的話,hashcode一定相等。但是如果不按官方照設計要求、不重寫hashcode方法,就會出現不相等的情況。
Executors提供了四種方法來建立執行緒池。
手寫一個:
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();
}
執行緒池作用
大家覺得本次面試題總結的寫得不錯的朋友,大家可以轉發+關注,然後掃描下方二維條碼獲取更多面試題以及答案— 掃描新增暗號:【CSDN】
不要認為它是四捨五入!不要認為它是四捨五入!不要認為它是四捨五入!
口訣:+0.5後向下取整。所以結果是-2。
留個題,Math.round(-2.6)結果和Math.round(2.6)結果
讓每個類只專心處理自己的方法。
軟體中的物件(類,模組,函數等)應該對於擴充套件是開放的,但是對於修改是關閉的。
子類可以去擴充套件父類別,但是不能改變父類別原有的功能。
應該通過呼叫介面或抽象類(比較高層),而不是呼叫實現類(細節)。
把介面分成滿足依賴關係的最小介面,實現類中不能有不需要的方法。
高內聚,低耦合。
關鍵詞 | 修飾物 | 影響 |
---|---|---|
final | 變數 | 分配到常數池中,程式不可改變其值 |
final | 方法 | 子類中將不能被重寫 |
final | 類 | 不能被繼承 |
static | 變數 | 分配在記憶體堆上,參照都會指向這一個地址而不會重新分配記憶體 |
static | 方法塊 | 虛擬機器器優先載入 |
static | 類 | 可以直接通過類來呼叫而不需要new |
String s = new String("hello");
可能建立兩個物件也可能建立一個物件。如果常數池中有hello
字串常數的話,則僅僅在堆中建立一個物件。如果常數池中沒有hello
物件,則堆上和常數池都需要建立。
String s = "hello"
這樣建立的物件,JVM會直接檢查字串常數池是否已有"hello"字串物件,如沒有,就分配一個記憶體存放"hello",如有了,則直接將字串常數池中的地址返回給棧。(沒有new,沒有堆的操作)
hotspot在64位元平臺上,佔8個位元組,在32位元平臺上佔4個位元組。
System.out.println(((1<3)?"a":"b")+3+4);
System.out.println(((1<3)?"a":"b")+(3+4));
控制檯:
a34
a7
8.1 什麼情況下,加號會變成字串連線符
依據上面的例子來思考。
更好的記憶方法:
基本型別中,沒有boolean
和浮點型別
+長型別long
.相應的包裝型別也沒有。
外加String
和enum
。
// 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 | 從左向右 |
== != | 從左向右 |
& | 從左向右 |
^ | 從左向右 |
| | 從左向右 |
&& | 從左向右 |
|| | 從左向右 |
?: | 從右向左 |
= | 從右向左 |
為了網路進行傳輸或者持久化
什麼是序列化
將物件的狀態資訊轉換為可以儲存或傳輸的形式的過程
除了實現Serializable介面還有什麼序列化方式
標記-清除演演算法(老年代)
該演演算法分為「標記」和「清除」兩個階段: 首先標記出所有需要回收的物件(可達性分析), 在標記完成後統一清理掉所有被標記的物件.
該演演算法會有兩個問題:
所以它一般用於"垃圾不太多的區域,比如老年代"。
複製演演算法(新生代)
該演演算法的核心是將可用記憶體按容量劃分為大小相等的兩塊, 每次只用其中一塊, 當這一塊的記憶體用完, 就將還存活的物件(非垃圾)複製到另外一塊上面, 然後把已使用過的記憶體空間一次清理掉。
優點:不用考慮碎片問題,方法簡單高效。
缺點:記憶體浪費嚴重。
現代商用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(讓老年代騰出更多空間).
標記-整理演演算法(老年代)
標記清除演演算法會產生記憶體碎片問題, 而複製演演算法需要有額外的記憶體擔保空間, 於是針對老年代的特點, 又有了標記整理演演算法. 標記整理演演算法的標記過程與標記清除演演算法相同, 但後續步驟不再對可回收物件直接清理, 而是讓所有存活的物件都向一端移動,然後清理掉端邊界以外的記憶體.
新生代:
老年代:
永久代:
指的就是方法區。
MinGC:
FullGC:
大家覺得本次面試題總結的寫得不錯的朋友,大家可以轉發+關注,然後掃描下方二維條碼獲取更多面試題以及答案— 掃描新增暗號:【CSDN】
在堆裡面存放著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()方法且沒被呼叫過).
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個步驟:
可以看到,初始標記、重新標記需要STW(stop the world 即:掛起使用者執行緒)操作。因為最耗時的操作是並行標記和並行清除。所以總體上我們認為CMS的GC與使用者執行緒是並行執行的。
優點:並行收集、低停頓
缺點:
-XX:CMSInitiatingOccupancyFraction
來設定GC觸發百分比(1.6後預設92%),當然我們還得設定啟用該策略-XX:+UseCMSInitiatingOccupancyOnly
-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區組成, 這種方法使得可以動態調整各代區域的大小
老年代收集
分為以下幾個階段:
詳情請看參考檔案
http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html#t5
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. 初始化
5. 呼叫物件的初始化方法
也就是執行構造方法。
以上便是此次分享的面試題以及答案,如果覺得還不過癮,大家可以關注我的公眾號-【Java爛豬皮】,裡面有往期的面試題以及最新的面試分享,關注後回覆:【666】即可免費獲取更多的Java架構進階vip學習資料