09.09 Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

首先夥伴們週末愉快哈,大週末還在更新文章哈哈哈

一、HashMap的那些事

1.1、HashMap的實現原理

1.1.1、結構

HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。如下圖所示:

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

當新建一個HashMap的時候,就會初始化一個數組。哈希表是由數組+鏈表組成的,一個長度為16的數組中,每個元素存儲的是一個鏈表的頭結點。這些元素一般情況是通過hash(key)%len的規則存儲到數組中,也就是元素的key的哈希值對數組長度取模得到。

1.1.2、核心變量

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

1.1.3、put存儲邏輯

當我們往HashMap中put元素的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在數組中的位置(即下標), 如果數組該位置上已經存放有其他元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。

這裡有一個特殊的地方。在JDK1.6中,HashMap採用位桶+鏈表實現,即使用鏈表處理衝突,同一hash值的鏈表都存儲在一個鏈表裡。但是當位於一個桶中的元素較多,即hash值相等的元素較多時,通過key值依次查找的效率較低。而JDK1.8中,HashMap採用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換為紅黑樹,這樣大大減少了查找時間。

紅黑樹

紅黑樹和平衡二叉樹(AVL樹)類似,都是在進行插入和刪除操作時通過特定操作保持二叉查找樹的平衡,從而獲得較高的查找性能。

紅黑樹和AVL樹的區別在於它使用顏色來標識結點的高度,它所追求的是局部平衡而不是AVL樹中的非常嚴格的平衡。

1.1.4、get讀取邏輯

從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。

如果第一個節點是TreeNode,說明採用的是數組+紅黑樹結構處理衝突,遍歷紅黑樹,得到節點值。

1.1.5、歸納

簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Node 對象。HashMap 底層採用一個 Node[] 數組來保存所有的 key-value 對,當需要存儲一個 Node 對象時,會根據hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,也會根據hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Node。

1.1.6、HashMap的resize(rehash)

當HashMap中的元素越來越多的時候,hash衝突的幾率也就越來越高,因為數組的長度是固定的。所以為了提高查詢的效率,就要對HashMap的數組進行擴容,在對HashMap數組進行擴容時,就會出現性能問題:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。

那麼HashMap什麼時候進行擴容呢?當HashMap中的元素個數超過數組大小loadFactor時,就會進行數組擴容,loadFactor的默認值為0.75,這是一個折中的取值。也就是說,默認情況下,數組大小為16,那麼當HashMap中元素個數超過160.75=12的時候,就把數組的大小擴展為 216=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的性能。

1.2、HashMap在併發場景下的問題和解決方案

1.2.1、多線程put後可能導致get死循環

問題原因就是HashMap是非線程安全的,多個線程put的時候造成了某個key值Entry key List的死循環,問題就這麼產生了。

當另外一個線程get 這個Entry List 死循環的key的時候,這個get也會一直執行。最後結果是越來越多的線程死循環,最後導致服務器dang掉。我們一般認為HashMap重複插入某個值的時候,會覆蓋之前的值,這個沒錯。但是對於多線程訪問的時候,由於其內部實現機制(在多線程環境且未作同步的情況下,對同一個HashMap做put操作可能導致兩個或以上線程同時做rehash動作,就可能導致循環鍵表出現,一旦出現線程將無法終止,持續佔用CPU,導致CPU使用率居高不下),就可能出現安全問題了。

為HashMap以鏈表組形式存在,初始數組16位(為何16位,又是一堆移位算法,下一篇文章再寫吧),如果長度超過75%,長度增加一倍,多線程操作的時候,恰巧兩個線程插入的時候都需要擴容,形成了兩個鏈表,這時候讀取,size不一樣,報錯了。其實這時候報錯都是好事,至少不會陷入死循環讓cpu死了,有種情況,假如兩個線程在讀,還有個線程在寫,恰巧擴容了,這時候你死都不知道咋死的,直接就是死循環,假如你是雙核cpu,cpu佔用率就是50%,兩個線程恰巧都進入死循環了,得!中獎了。

1.2.2、多線程put的時候可能導致元素丟失

主要問題出在addEntry方法的new Entry (hash, key, value, e),如果兩個線程都同時取得了e,則他們下一個元素都是e,然後賦值給table元素的時候有一個成功有一個丟失。

1.2.3、解決方案

ConcurrentHashMap替換HashMap

Collections.synchronizedMap將HashMap包裝起來

1.3、ConcurrentHashMap PK HashTable

ConcurrentHashMap具體是怎麼實現線程安全的呢,肯定不可能是每個方法加synchronized,那樣就變成了HashTable。

從ConcurrentHashMap代碼中可以看出,它引入了一個“分段鎖”的概念,具體可以理解為把一個大的Map拆分成N個小的HashTable,根據key.hashCode()來決定把key放到哪個HashTable中。

以空間換時間的結構,跟分佈式緩存結構有點像,創建的時候,內存直接分為了16個segment,每個segment實際上還是存儲的哈希表(Segment其實就是一個HashMap ),寫入的時候,先找到對應的segment,然後鎖這個segment,寫完,解鎖,鎖segment的時候,其他segment還可以繼續工作。

ConcurrentHashMap如此的設計,優勢主要在於: 每個segment的讀寫是高度自治的,segment之間互不影響。這稱之為“鎖分段技術”;

二、線程,多線程,線程池的那些事

2.1、線程的各個狀態及切換

Java中的線程的生命週期大體可分為5種狀態:新建、可運行、運行、阻塞、死亡。

1、新建(NEW):新創建了一個線程對象。

2、可運行(RUNNABLE):線程對象創建後,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取cpu 的使用權 。

3、運行(RUNNING):可運行狀態(runnable)的線程獲得了cpu 時間片(timeslice) ,執行程序代碼。

4、阻塞(BLOCKED):阻塞狀態是指線程因為某種原因放棄了cpu 使用權,也即讓出了cpu timeslice,暫時停止運行。直到線程進入可運行(runnable)狀態,才有機會再次獲得cpu timeslice 轉到運行(running)狀態。

阻塞的情況分三種:

1)等待阻塞:運行(running)的線程執行o.wait()方法,JVM會把該線程放入等待隊列(waitting queue)中。

2)同步阻塞:運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池(lock pool)中。

3)其他阻塞:運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入可運行(runnable)狀態。

5、死亡(DEAD):線程run()、main() 方法執行結束,或者因異常退出了run()方法,則該線程結束生命週期。死亡的線程不可再次復生。

幾個方法的比較:

1)Thread.sleep(long millis),一定是當前線程調用此方法,當前線程進入阻塞,但不釋放對象鎖,millis後線程自動甦醒進入可運行狀態。作用:給其它線程執行機會的最佳方式。

2)Thread.yield(),一定是當前線程調用此方法,當前線程放棄獲取的cpu時間片,由運行狀態變會可運行狀態,讓OS再次選擇線程。作用:讓相同優先級的線程輪流執行,但並不保證一定會輪流執行。實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程調度程序再次選中。Thread.yield()不會導致阻塞。

3)t.join()/t.join(long millis),當前線程裡調用其它線程1的join方法,當前線程阻塞,但不釋放對象鎖,直到線程1執行完畢或者millis時間到,當前線程進入可運行狀態。

4)obj.wait(),當前線程調用對象的wait()方法,當前線程釋放對象鎖,進入等待隊列。依靠notify()/notifyAll()喚醒或者wait(long timeout)timeout時間到自動喚醒。

5)obj.notify(),喚醒在此對象監視器上等待的單個線程,選擇是任意性的。notifyAll()喚醒在此對象監視器上等待的所有線程。

2.2、多線程的實現方式Thread、Runnable、Callable

繼承Thread類,實現Runnable接口,實現Callable接口。

這三種方法的介紹和比較:

1、實現Runnable接口相比繼承Thread類有如下優勢:

1)可以避免由於Java的單繼承特性而帶來的侷限

2)增強程序的健壯性,代碼能夠被多個線程共享,代碼與數據是獨立的

3)適合多個相同程序代碼的線程去處理同一資源的情況

2、實現Runnable接口和實現Callable接口的區別

1)Runnable是自從java1.1就有了,而Callable是1.5之後才加上去的

2)實現Callable接口的任務線程能返回執行結果,而實現Runnable接口的任務線程不能返回結果

3)Callable接口的call()方法允許拋出異常,而Runnable接口的run()方法的異常只能在內部消化,不能繼續上拋

4)加入線程池運行,Runnable使用ExecutorService的execute方法,Callable使用submit方法

注:Callable接口支持返回執行結果,此時需要調用FutureTask.get()方法實現,此方法會阻塞主線程直到獲取返回結果,當不調用此方法時,主線程不會阻塞

2.3、線程池原理和運行機制

java.uitl.concurrent.ThreadPoolExecutor類是線程池中最核心的一個類。

在ThreadPoolExecutor類中提供了四個構造方法,主要參數包括下面的參數:

1、int corePoolSize:核心池的大小。

線程池的基本大小,即在沒有任務需要執行的時候線程池的大小,並且只有在工作隊列滿了的情況下才會創建超出這個數量的線程。這裡需要注意的是:在剛剛創建ThreadPoolExecutor的時候,線程並不會立即啟動,而是要等到有任務提交時才會啟動,除非調用了prestartCoreThread/prestartAllCoreThreads事先啟動核心線程。再考慮到keepAliveTime和allowCoreThreadTimeOut超時參數的影響,所以沒有任務需要執行的時候,線程池的大小不一定是corePoolSize。

2、int maximumPoolSize:線程池最大線程數,它表示在線程池中最多能創建多少個線程,注意與corePoolSize區分。

線程池中允許的最大線程數,線程池中的當前線程數目不會超過該值。如果隊列中任務已滿,並且當前線程個數小於maximumPoolSize,那麼會創建新的線程來執行任務。

3、long keepAliveTime:表示線程沒有任務執行時最多保持多久時間會終止。

4、TimeUnit unit:參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性。

5、BlockingQueue<runnable> workQueue:一個阻塞隊列,用來存儲等待執行的任務。/<runnable>

6、ThreadFactory threadFactory:線程工廠,主要用來創建線程。

7、RejectedExecutionHandler handler:表示當拒絕處理任務時的策略。

還有一個成員變量比較重要:poolSize

線程池中當前線程的數量,當該值為0的時候,意味著沒有任何線程,線程池會終止。同一時刻,poolSize不會超過maximumPoolSize。

2.4、線程池對任務的處理

1、如果當前線程池中的線程數目小於corePoolSize,則每來一個任務,就會創建一個線程去執行這個任務;

2、如果當前線程池中的線程數目>=corePoolSize,則每來一個任務,會嘗試將其添加到任務緩存隊列當中,若添加成功,則該任務會等待空閒線程將其取出去執行;若添加失敗(一般來說是任務緩存隊列已滿),則會嘗試創建新的線程去執行這個任務(maximumPoolSize);

3、如果當前線程池中的線程數目達到maximumPoolSize(此時線程池的任務緩存隊列已滿),則會採取任務拒絕策略進行處理;

任務拒絕策略,通常有以下四種策略:

1)ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。

2)ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。

3)ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)

4)ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務

4、如果線程池中的線程數量大於 corePoolSize時,如果某線程空閒時間超過keepAliveTime,線程將被終止,直至線程池中的線程數目不大於corePoolSize;如果允許為核心池中的線程設置存活時間,那麼核心池中的線程空閒時間超過keepAliveTime,線程也會被終止。

2.5、線程池的狀態

1、線程池的狀態說明:

RUNNING(運行):接受新任務並處理排隊任務。

SHUTDOWN(關閉):不接受新任務,會處理隊列任務。

STOP(停止):不接受新任務,不處理隊列任務,並且中斷進程中的任務。

TIDYING(整理):所有任務都已終止,工作計數為零,線程將執行terminated()方法終止線程。

TERMINATED(已終止):terminated()方法已完成。

2、各個狀態之間的轉換

RUNNING -> SHUTDOWN:調用shutdown()方法。

RUNNING or SHUTDOWN-> STOP:調用shutdownNow()方法。

SHUTDOWN -> TIDYING:當隊列和池均都為空時。

STOP -> TIDYING:當池為空時。

TIDYING -> TERMINATED:當terminated()方法已完成。

三、JVM的那些事

3.1、JVM的結構

每個JVM都包含:方法區、Java堆、Java棧、本地方法棧、程序計數器等。

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

1、方法區:共享

各個線程共享的區域,存放類信息、常量、靜態變量。

2、Java堆:共享

也是線程共享的區域,我們的類的實例就放在這個區域,可以想象你的一個系統會產生很多實例,因此Java堆的空間也是最大的。如果Java堆空間不足了,程序會拋出OutOfMemoryError異常。

3、Java棧:私有

每個線程私有的區域,它的生命週期與線程相同,一個線程對應一個Java棧,每執行一個方法就會往棧中壓入一個元素,這個元素叫“棧幀”,而棧幀中包括了方法中的局部變量、用於存放中間狀態值的操作棧。如果Java棧空間不足了,程序會拋出StackOverflowError異常,想一想什麼情況下會容易產生這個錯誤,對,遞歸,遞歸如果深度很深,就會執行大量的方法,方法越多Java棧的佔用空間越大。

4、本地方法棧:私有

角色和Java棧類似只不過它是用來表示執行本地方法的,本地方法棧存放的方法調用本地方法接口,最終調用本地方法庫,實現與操作系統、硬件交互的目的。

5、程序計數器:私有

說到這裡我們的類已經加載了,實例對象、方法、靜態變量都去了自己該去的地方,那麼問題來了,程序該怎麼執行,哪個方法先執行,哪個方法後執行,這些指令執行的順序就是程序計數器在管,它的作用就是控制程序指令的執行順序。

6、執行引擎當然就是根據PC寄存器調配的指令順序,依次執行程序指令。

3.2、Java堆的介紹及典型的垃圾回收算法介紹

3.2.1、Java堆的介紹

Java堆是虛擬機管理的最大的一塊內存,堆上的所有線程共享一塊內存區域,在啟動虛擬機時創建。此內存唯一目的就是存放對象實例,幾乎所有對象實例都在這裡分配,這一點Java虛擬機規範中的描述是:所有對象實例及數組都要在堆上分配。

Java堆是垃圾收集器管理的主要區域,也被稱為“GC堆”,由於現在收集器基本都採用分代收集算法,所以Java堆中還可以分為:新生代和老年代。

堆的內存模型大致為:

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

默認的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過參數 –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小,老年代 ( Old ) = 2/3 的堆空間大小。

其中,新生代 ( Young ) 被細分為 Eden 和 兩個 Survivor 區域,這兩個 Survivor 區域分別被命名為 from 和 to以示區分。 默認的,Edem : from : to = 8 : 1 : 1 ( 可以通過參數 –XX:SurvivorRatio 來設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。

JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來為對象服務,所以無論什麼時候,總是有一塊 Survivor 區域是空閒著的。 因此,新生代實際可用的內存空間為 9/10 ( 即90% )的新生代空間。

新生代是 GC 收集垃圾的頻繁區域。

當對象在 Eden ( 包括一個 Survivor 區域,這裡假設是 from 區域 ) 出生後,在經過一次 Minor GC 後,如果對象還存活,並且能夠被另外一塊 Survivor 區域所容納 ( 上面已經假設為 from 區域,這裡應為 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用複製算法將這些仍然還存活的對象複製到另外一塊 Survivor 區域 ( 即 to 區域 ) 中,然後清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),並且將這些對象的年齡設置為1,以後對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,可以通過參數 -XX:MaxTenuringThreshold 來設定 ),這些對象就會成為老年代。

但這也不是一定的,對於一些較大的對象 ( 即需要分配一塊較大的連續內存空間 ) 則是直接進入到老年代。

From Survivor區域與To Survivor區域是交替切換空間,在同一時間內兩者中只有一個不為空。

3.2.2、如何確定某個對象是可回收的(垃圾)

1、引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任何時刻計數器都為0的對象就是不可能再被使用的。

這種方式的問題是無法解決循環引用的問題,當兩個對象循環引用時,就算把兩個對象都設置為null,因為他們的引用計數都不為0,這就會使他們永遠不會被清除。

2、根搜索算法(可達性分析)

為了解決引用計數法的循環引用問題,Java使用了可達性分析的方法。通過一系列的“GC roots”對象作為起點搜索。如果在“GC roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的。要注意的是,不可達對象不等價於可回收對象,不可達對象變為可回收對象至少要經過兩次標記過程。兩次標記後仍然是可回收對象,則將面臨回收。

比較常見的將對象視為可回收對象的原因:

顯式地將對象的唯一強引用指向新的對象。

顯式地將對象的唯一強引用賦值為Null。

局部引用所指向的對象(如,方法內對象)。

只有弱引用與其關聯的對象。

1)強引用(StrongReference)

強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。 ps:強引用其實也就是我們平時A a = new A()這個意思。

2)軟引用(SoftReference)

如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存(下文給出示例)。

軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

3)弱引用(WeakReference)

弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。

弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

4)虛引用(PhantomReference)

“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。

虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之 關聯的引用隊列中。

3.2.3、典型的垃圾回收算法介紹

1、標記-清除算法(Mark-Sweep)

最基礎的垃圾回收算法,分為“標註”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。

標記過程:為了能夠區分對象是live的,可以為每個對象添加一個marked字段,該字段在對象創建的時候,默認值是false。

清除過程:去遍歷堆中所有對象,並找出未被mark的對象,進行回收。與此同時,那些被mark過的對象的marked字段的值會被重新設置為false,以便下次的垃圾回收。

缺點:效率低,空間問題(產生大量不連續的內存碎片),後續可能發生大對象不能找到可利用空間的問題。

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

2、複製算法(Copying)——新生代的收集算法就是這種,但是比例不是1:1,而是(8+1):1

為了解決Mark-Sweep算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分為大小相等的兩塊,每次只使用其中一塊。當這一塊內存滿後將尚存活的對象複製到另一塊上去,把已使用的內存空間一次清理掉。這種算法雖然實現簡單,內存效率高,不易產生碎片,但是最大的問題是可用內存被壓縮到了原本的一半。且存活對象增多的話,Copying算法的效率會大大降低。

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

3、標記-整理算法(Mark-Compact)——老年代的收集算法

結合了以上兩個算法,標記階段和Mark-Sweep算法相同,標記後不是清理對象,而是將所有存活對象移向內存的一端,然後清除端邊界外的對象。如圖:

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

4、分代收集算法(Generational Collection)

分代收集法是目前大部分JVM所採用的方法,其核心思想是根據對象存活的不同生命週期將內存劃分為不同的域,一般情況下將GC堆劃分為老生代(Tenured/Old Generation)和新生代(Young Generation)。

老生代的特點是每次垃圾回收時只有少量對象需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的算法。

目前大部分JVM的GC對於新生代都採取複製算法(Copying),因為新生代中每次垃圾回收都要回收大部分對象,即要複製的操作比較少,但通常並不是按照1:1來劃分新生代。一般將新生代劃分為一塊較大的Eden空間和兩個較小的Survivor空間(From Space, To Space),每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將該兩塊空間中還存活的對象複製到另一塊Survivor空間中。

老年代中的對象存活率高,沒有額外空間對它進行分配,使用“標記-清理”或“標記-整理”算法來進行回收。

3.3、JVM處理FULLGC經驗

3.3.1、內存洩漏

1、產生原因

1)JVM內存過小。

2)程序不嚴密,產生了過多的垃圾。

2、一般情況下,在程序上的體現為:

1)內存中加載的數據量過於龐大,如一次從數據庫取出過多數據。

2)集合類中有對對象的引用,使用完後未清空,使得JVM不能回收。

3)代碼中存在死循環或循環產生過多重複的對象實體。

4)使用的第三方軟件中的BUG。

5)啟動參數內存值設定的過小。

3.3.2、Java內存洩漏的排查案例

1、確定頻繁Full GC現象,找出進程唯一ID

使用jps(jps -l)或ps(ps aux | grep tomat)找出這個進程在本地虛擬機的唯一ID(LVMID,Local Virtual Machine Identifier)

2、再使用“虛擬機統計信息監視工具:jstat”(jstat -gcutil 20954 1000)查看已使用空間站總空間的百分比,可以看到FGC的頻次。

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

3、找出導致頻繁Full GC的原因,找出出現問題的對象

分析方法通常有兩種:

1)把堆dump下來再用MAT等工具進行分析,但dump堆要花較長的時間,並且文件巨大,再從服務器上拖回本地導入工具,這個過程有些折騰,不到萬不得已最好別這麼幹。

2)更輕量級的在線分析,使用“Java內存影像工具:jmap”生成堆轉儲快照(一般稱為headdump或dump文件)。

jmap命令格式:jmap -histo:live 20954

4、一個工具:BTrace,沒有使用過

四、MySQL的那些事

4.1、找出慢SQL的方法

4.1.1、開啟慢查詢日誌

4.1.2、MySQL基準測試方法

4.2、索引的優缺點及實現原理

4.2.1、索引的優缺點

4.2.2、MySQL索引的實現原理

4.3、對於一個SQL的性能優化過程

4.4、MySQL數據庫的分庫分表方式以及帶來的問題處理

4.5、MySQL主從複製數據一致性問題處理

五、HTTP、HTTPS、協議相關

5.1、HTTP請求報文和響應報文

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

5.2、HTTPS為什麼是安全的?HTTPS的加密方式有哪些?

5.2.1、HTTPS的工作原理說明HTTPS是安全的

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

客戶端在使用HTTPS方式與Web服務器通信時有以下幾個步驟,如圖所示。

1、客戶使用https的URL訪問Web服務器,要求與Web服務器建立SSL連接。

2、Web服務器收到客戶端請求後,會將網站的證書信息(證書中包含公鑰)傳送一份給客戶端。

3、客戶端的瀏覽器與Web服務器開始協商SSL連接的安全等級,也就是信息加密的等級。

4、客戶端的瀏覽器根據雙方同意的安全等級,建立會話密鑰,然後利用網站的公鑰將會話密鑰加密,並傳送給網站。

5、Web服務器利用自己的私鑰解密出會話密鑰。

6、Web服務器利用會話密鑰加密與客戶端之間的通信。

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

5.2.2、HTTPS的加密方式有哪些?

1、對稱加密

對稱加密是指加密和解密使用相同密鑰的加密算法。它要求發送方和接收方在安全通信之前,商定一個密鑰。對稱算法的安全性依賴於密鑰,洩漏密鑰就意味著任何人都可以對他們發送或接收的消息解密,所以密鑰的保密性對通信至關重要。

2、更多的加密方式的瞭解

5.3、TCP三次握手協議,四次揮手

第一次握手:主機A發送位碼為syn=1,隨機產生seq number=1234567的數據包到服務器,主機B由SYN=1知道,A要求建立聯機;

第二次握手:主機B收到請求後要確認聯機信息,向A發送ack number=(主機A的seq+1),syn=1,ack=1,隨機產生seq=7654321的包;

第三次握手:主機A收到後檢查ack number是否正確,即第一次發送的seq number+1,以及位碼ack是否為1,若正確,主機A會再發送ack number=(主機B的seq+1),ack=1,主機B收到後確認seq值與ack=1則連接建立成功。

完成三次握手,主機A與主機B開始傳送數據。

5.4、OAuth協議介紹

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

5.5、防盜鏈Referer

Referer請求頭: 代表當前訪問時從哪個網頁連接過來的。

當Referer未存在或者是從其他站點訪問我們資源的時候就直接重定向到我們的主頁,這樣既可以防止我們的資源被竊取。

六、Spring

6.1、AOP的實現原理

spring框架對於這種編程思想的實現基於兩種動態代理模式,分別是JDK動態代理 及 CGLIB的動態代理,這兩種動態代理的區別是 JDK動態代理需要目標對象實現接口,而 CGLIB的動態代理則不需要。下面我們通過一個實例來實現動態代理,進而幫助我們理解面向切面編程。

JDK的動態代理要使用到一個類 Proxy 用於創建動態代理的對象,一個接口 InvocationHandler用於監聽代理對象的行為,其實動態代理的本質就是對代理對象行為的監聽。

6.2、Spring MVC工作原理

Spring的MVC框架主要由DispatcherServlet、處理器映射、處理器(控制器)、視圖解析器、視圖組成。

6.2.1、SpringMVC原理圖

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

6.2.2、SpringMVC運行原理

1、客戶端請求提交到DispatcherServlet

2、由DispatcherServlet控制器查詢一個或多個HandlerMapping,找到處理請求的Controller

3、DispatcherServlet將請求提交到Controller

4、Controller調用業務邏輯處理後,返回ModelAndView

5、DispatcherServlet查詢一個或多個ViewResoler視圖解析器,找到ModelAndView指定的視圖

6、視圖負責將結果顯示到客戶端

6.2.3、SpringMVC核心組件

1、DispatcherServlet:中央控制器,把請求給轉發到具體的控制類

2、Controller:具體處理請求的控制器

3、HandlerMapping:映射處理器,負責映射中央處理器轉發給controller時的映射策略

4、ModelAndView:服務層返回的數據和視圖層的封裝類

5、ViewResolver:視圖解析器,解析具體的視圖

6、Interceptors :攔截器,負責攔截我們定義的請求然後做處理工作

6.2.4、Servlet 生命週期

Servlet 生命週期可被定義為從創建直到毀滅的整個過程。以下是 Servlet 遵循的過程:

1、Servlet 通過調用 init () 方法進行初始化。

2、Servlet 調用 service() 方法來處理客戶端的請求。

3、Servlet 通過調用 destroy() 方法終止(結束)。

4、最後,Servlet 是由 JVM 的垃圾回收器進行垃圾回收的。

6.2.5、Spring容器初始化過程

Spring 啟動時讀取應用程序提供的Bean配置信息,並在Spring容器中生成一份相應的Bean配置註冊表,然後根據這張註冊表實例化Bean,裝配號Bean之間的依賴關係,為上層應用提供準備就緒的運行環境。

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

七、分佈式

7.1、分佈式下如何保證事務一致性

分佈式事務,常見的兩個處理辦法就是兩段式提交和補償。

7.1.1、兩段式提交

分佈式事務將提交分成兩個階段:

prepare;

commit/rollback

在分佈式系統中,每個節點雖然可以知曉自己的操作是成功或者失敗,卻無法知道其他節點的操作的成功或失敗。當一個事務跨越多個節點時,為了保持事務的ACID特性,需要引入一個作為協調者的組件來統一掌控所有節點(參與者)的操作結果並最終指示這些節點是否需要把操作結果進行真正的提交。算法步驟如下:

第一階段:

1、協調者會問所有的參與者,是否可以執行提交操作。

2、各個參與者開始事務執行的準備工作,如:為資源上鎖,預留資源。

3、參與者響應協調者,如果事務的準備工作成功,則回應“可以提交”,否則回應“拒絕提交”。

第二階段:

1、如果所有的參與者都回應“可以提交”。那麼協調者向所有的參與者發送“正式提交”的命令。參與者完成正式提交併釋放所有資源,然後回應“完成”,協調者收集各節點的“完成”回應後結束這個Global Transaction

2、如果有一個參與者回應“拒絕提交”,那麼協調者向所有的參與者發送“回滾操作”,並釋放所有資源,然後回應“回滾完成”,協調者收集各節點的“回滾”回應後,取消這個Global Transaction。

7.1.2、三段式提交

三段提交的核心理念是:在詢問的時候並不鎖定資源,除非所有人都同意了,才開始鎖資源。他把二段提交的第一個段break成了兩段:詢問,然後再鎖資源。最後真正提交。

7.1.2、事務補償,最終一致性

補償比較好理解,先處理業務,然後定時或者回調裡,檢查狀態是不是一致的,如果不一致採用某個策略,強制狀態到某個結束狀態(一般是失敗狀態)。

八、常用的排查問題方法

8.1、Linux日誌分析常用命令

8.1.1、查看文件內容

cat

-n 顯示行號

8.1.2、分頁顯示

more

Enter 顯示下一行

空格 顯示下一頁

F 顯示下一屏

B 顯示上一屏

less

/get 查詢"get"字符串並高亮顯示

8.1.3、顯示文件尾

tail

-f 不退出持續顯示

-n 顯示文件最後n行

8.1.4、顯示頭文件

head

-n 顯示文件開始n行

8.1.5、內容排序

sort

-n 按照數字排序

-r 按照逆序排序

-k 表示排序列

-t 指定分隔符

8.1.6、字符統計

wc

-l 統計文件中行數

-c 統計文件字節數

-L 查看最長行長度

-w 查看文件包含多少個單詞

8.1.7、查看重複出現的行

uniq

-c 查看該行內容出現的次數

-u 只顯示出現一次的行

-d 只顯示重複出現的行

8.1.8、字符串查找

grep

8.1.9、文件查找

find

which

whereis

8.1.10、表達式求值

expr

8.1.11、歸檔文件

tar

zip

unzip

8.1.12、URL訪問工具

curl

wget

8.1.13、查看請求訪問量

頁面訪問排名前十的IP

cat access.log | cut -f1 -d " " | sort | uniq -c | sort -k 1 -r | head -10

頁面訪問排名前十的URL

cat access.log | cut -f4 -d " " | sort | uniq -c | sort -k 1 -r | head -10

查看最耗時的頁面

cat access.log | sort -k 2 -n -r | head 10

九、中間件和架構

9.1、kafka消息隊列

1、避免數據丟失

producer:

加大重試次數

同步發送

對於單條數據過大,要設置可接收的單條數據的大小

對於異步發送,通過回調函數來感知丟消息

block.on.buffer.full = true

consumer:

enable.auto.commit=false 關閉自動提交位移

2、避免消息亂序

假設a,b兩條消息,a先發送後由於發送失敗重試,這時順序就會在b的消息後面,可以設置max.in.flight.requests.per.connection=1來避免。

max.in.flight.requests.per.connection:限制客戶端在單個連接上能夠發送的未響應請求的個數,設置此值是1表示kafka broker在響應請求之前client不能再向同一個broker發送請求,但吞吐量會下降。

3、避免消息重複

使用第三方redis的set

9.2、ZooKeeper的原理

9.3、SOA相關,RPC兩種實現方式:基於HTTP和基於TCP

9.4、Netty

Java工程師面試阿里(阿里雲、天貓、菜鳥)涉及到的知識點(一)

image.png

9.5、Dubbo

十、其他

10.1、系統做了哪些安全防護

1、XSS(跨站腳本攻擊)

全稱是跨站腳本攻擊(Cross Site Scripting),指攻擊者在網頁中嵌入惡意腳本程序。

XSS防範:

XSS之所以會發生,是因為用戶輸入的數據變成了代碼。因此,我們需要對用戶輸入的數據進行HTML轉義處理,將其中的“尖括號”、“單引號”、“引號”之類的特殊字符進行轉義編碼。

2、CSRF(跨站請求偽造)

攻擊者盜用了你的身份,以你的名義向第三方網站發送惡意請求。

CSRF的防禦:

1)儘量使用POST,限制GET

2)將cookie設置為HttpOnly

3)增加token

4)通過Referer識別

3、SQL注入

使用預編譯語句(PreparedStatement),這樣的話即使我們使用sql語句偽造成參數,到了服務端的時候,這個偽造sql語句的參數也只是簡單的字符,並不能起到攻擊的作用。

做最壞的打算,即使被’拖庫‘('脫褲,數據庫洩露')。數據庫中密碼不應明文存儲的,可以對密碼使用md5進行加密,為了加大破解成本,所以可以採用加鹽的(數據庫存儲用戶名,鹽(隨機字符長),md5後的密文)方式。

4、DDOS

最直接的方法增加帶寬。但是攻擊者用各地的電腦進行攻擊,他的帶寬不會耗費很多錢,但對於服務器來說,帶寬非常昂貴。

雲服務提供商有自己的一套完整DDoS解決方案,並且能提供豐富的帶寬資源

10.2、項目管理工具

10.2.1、OmniPlan

10.2.2、Excel


分享到:


相關文章: