1. 程式人生 > 實用技巧 >萬字面試知識點助力金九銀十

萬字面試知識點助力金九銀十

關注微信公眾號:CoderLi,回覆面試獲取PDF版本

說明

本文件為本人整理網上資源以及自己的一些知識點、為面試準備的。當時整理的時候並沒有考慮到釋出出來、所以對於引用整理的網上的一些文章連結可能並沒有列出來、抱歉!如有請評論告知,謝謝

引用

技術

  • JVM 調優
    • cms
    • g1
    • 調優工具
  • Spring
    • 看我自己的 Blog 就好了
    • 面試題
  • Redis
  • Kafka
  • Mysql
  • Zookeeper
  • Java
  • Spring Boot /Cloud

專案相關

  • 重複生單 ZK 分散式鎖 (重複生單事故)
  • 分散式 ID
  • 支付訂單狀態以及預存款扣除
  • Kafka 事故

業務邏輯與資料

  • 虛擬拼接

  • B2B 相關流程

JVM 調優

執行時資料區域

  • 程式計數器
  • 虛擬機器棧
  • 本地方法棧
  • 方法區

垃圾收集器需要做的三件事情

  • 哪些記憶體需要回收
  • 什麼時候回收
  • 怎麼回收

回收方法區

判斷一個型別不再被使用

  • 該類的所有例項都被回收
  • 載入該類的類載入器被回收掉了
  • 該類的 Class 物件沒有被任何地方引用

分代假說

  • 弱分代假說、絕大多數物件都是朝生夕滅的
  • 強分代假說、熬過 GC 次數越多的物件、就很難會消亡
  • 跨代引用相對於同代引用來說僅僅是佔少數

標記-清除

  • 標記階段
  • 清除階段

標記存活的物件、統一回收未被標記的物件

缺點

  • 執行效率不穩定、當堆中存在大量物件、大部分需要被回收、那麼清除階段需要進行大量的工作
  • 記憶體碎片

標記-複製

缺點

  • 不能使用全部的記憶體進行物件的分配
  • 如果按比例的話、就需要進行分配擔保
  • 存活物件較多的時候、需要很多複製、效率會低

標記-整理

  • 移動物件則記憶體回收時更復雜
  • 不移動則記憶體分配時更復雜

從垃圾收集的停頓時間來看、不移動物件停頓時間更加短、甚至可以不停頓、但是從整個程式的吞吐量來看、移動物件會更加划算。因為記憶體分配和訪問相比垃圾收集的頻率要高很多,這部分的耗時增加、吞吐量就會下降了。

Parallel Old 使用的就是標記整理演算法、關注點就是吞吐量

CMS 使用的就是標記-清除演算法、關注點就是停頓時間

還有一種就是和稀泥的方法、就是 CMS 那樣、平時採用標記-清除演算法、暫時容忍記憶體碎片、直到記憶體碎片化程度大到影響物件分配、再採用標記-整理演算法收集一次、以獲得記憶體的規整

記憶集和卡表

垃圾收集器在新生代中建立了名為記憶集 Remember Set 的資料結構、用來避免整個老年代加進 GC Roots 掃描

記憶集是一種用於記錄從非收集區域指向收集區域集合的抽象資料結構

收集器只要通過記憶集判斷出某一塊非收集區域是否存在指向了收集區域的指標就可以了、並不需要了解這些跨代引用的詳細細節。

第三種就是稱為 卡表 的方式去實現的。

記憶集是抽象概念、卡表是具體的實現。

卡表最簡單的形式可以只是一個位元組陣列、HotSpot 確實也是這麼做的。

位元組陣列中每個元素都對應著其標示的記憶體區域中一塊特定大小的記憶體,這一塊記憶體稱為卡頁。

三色標記

  • 白色、表示物件尚未被垃圾收集器訪問過、如果在可達性分析開始階段、所有物件都是白色的、如果在結束階段、物件依然是白色、則代表物件不可達
  • 黑色、表示物件已經被垃圾收集器訪問過了、並且整個物件的所有引用都掃描過了、黑色代表這個物件存活、如果有其他物件指向黑色物件、無須重新掃描一遍、黑色物件不可能直接指向白色物件
  • 灰色、表示物件已經被垃圾收集器訪問過、對這個物件至少存在一個引用沒有被掃描過

當且僅當都滿足以下兩個條件、會產生物件消失的問題

  • 插入一條或多條從黑色物件到白色物件的新引用
  • 刪除了全部從灰色物件到該白色物件的直接或間接引用

要解決這個併發掃描時物件訊息的問題、只需要破壞其中的任意一條就可以了

  • 增量更新、破壞的是第一條 CMS
  • 原始快照、破壞的是第二條、G1

經典的垃圾收集器

Serial

單執行緒工作的收集器

ParNew

ParNew 收集器出了支援多執行緒並行收集之外、其他與Serial 收集器相比並無太多創新。

還有一個重要的特性 : 除了 Serial 收集器、它是第二個可以與 CMS 收集器配合工作

  • 並行 Parallel:並行描述的是多條垃圾收集器執行緒之間的關係、說明同一時間有多條這樣的執行緒協同工作、通常預設此時使用者執行緒處於等待狀態
  • 併發 Concurrent:併發描述的是垃圾收集器與使用者執行緒之間的關係、說明同一時間垃圾收集器執行緒與使用者執行緒都在工作。

Parallel Scavenge

新生代收集器、同樣是基於標記-複製演算法實現的收集器。

Parallel Scavenge 收集器的目標是達到一個可控制的吞吐量,CMS 收集器則更加關注縮短垃圾收集器使用者執行緒停止的時間。

  • -XX:MaxGCPauseMillis 允許的值是一個大於 0 的毫秒數、收集器將盡力保證記憶體回收花費的時間不超過使用者設定值。垃圾收集器停頓時間縮短是以犧牲吞吐量和新生代空間為代價換取的:系統把新生代調得小一些、收集 300MB 新生代肯定比收集 500MB 新生代快、但也導致了垃圾收集的頻率變得更加頻繁了,原來 10s 收集一次每次停頓 100 毫秒,現在變成5秒收集一次,每次停頓 70 毫秒。停頓確實在下降、但是吞吐量也下來了。
  • -XX:GCTimeRatio引數的值應該是一個大於 0 小於 100 的數、也就是垃圾收集時間佔總時間的比率,相當於吞吐量的倒數。比如設定引數的值為 19 那麼允許的最大垃圾收集時間就佔總時間的 5% 因為 1/(19+1) 。
  • -XX:+UseAdaptiveSizePolicy 當這個引數被啟用後、就不需要人工置頂新生代的大小、Eden、Survivor的比例、晉升老年代物件大小等細節引數。虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或最大吞吐量。這種調節方式稱為垃圾收集的自適應調節策略(GC Ergonomics)

Serial Old

是 Serial 收集器的老年代版本,單執行緒收集器、使用標記整理演算法。

用途

  • JDK5 以及之前的版本中與 Parallel Scavenge 收集器搭配使用
  • 另外一種就是作為 CMS 收集器發生失敗時的後備預案,在併發收集發生 Concurrent Mode Failure 是使用

Parallel Old

是 Parallel Scavenge 收集器的老年代版本、支援多執行緒併發收集,基於標記整理演算法實現。

CMS

concurrent mark sweep 收集器是以獲取最短停頓時間為目標的收集器。是基於標記-清除演算法實現的。

  • 初始標記
  • 併發標記
  • 併發預清理
  • 併發可取消預清理
  • 最終標記
  • 併發清除
  • 併發重置

初始標記

STW 事件、此階段的目標主要是標記老年代中所有存活的物件、包括 GC Roots 的直接引用、以及由年輕代存活物件所引用的物件(這個也很重要、因為老年代是獨立進行回收的)

併發標記 在此階段、垃圾收集器遍歷老年代、標記所有存活的物件。此階段、垃圾收集執行緒與應用執行緒併發執行、不用暫停應用。在此階段、並非所有老年代中存活的物件都在此階段被被標記、因為標記過程中物件的引用關係還在發生變化。

併發預清理 同樣不需要 STW 、此階段記錄在併發標記過程中新增的關係引用。然後以此關係鏈中的黑色物件作為根、重新遍歷其引用關係、標記新增的物件。這個在三色標記中有提及到。

併發可取消預清理 同樣不需要 STW 、嘗試在 最終標記之前儘可能多做一些工作。

最終標記 最後一次 STW 。本階段的目標是完成老年代中所有存活物件的標記。因為之前的預清理階段是併發的、有可能物件之間的引用關係變化沒有很好的記錄下來、所有需要 STW 來處理。

在這個階段之後、老年代中的所有存活物件都被標記了

併發清除 不需要 STW、目標是刪除未使用的物件、並回收他們佔用的空間。

併發重置 不需要 STW 、重置 CMS 相關的內部資料、為下次 GC 準備。

CMS 有三個缺點

  • 對處理器資源非常敏感。在併發階段預應用程式併發執行,雖然不用 STW、但是也會因為佔用了一部分執行緒而導致應用程式變慢,降低吞吐量。CMS 預設啟動對回收執行緒數是:(處理器核心數量 + 3) /4。如果核心數量大於等於4,那麼不會佔用超過 25% 的處理器運算資源,但是當處理器資源不足四個時,CMS 對程式影響就會變得很大。
  • 由於 CMS 收集器無法處理浮動垃圾、有可能出現 “Concurrent Mode Failure“ 進而導致另一次完全 STW 的 Full GC 產生 。浮動垃圾 : 在 CMS 併發標記和併發清除階段、使用者執行緒還是繼續在執行、這個階段肯定會有新的垃圾不斷產生、但這一部分垃圾物件是出現在標記過程結束以後、CMS 無法在當次垃圾收集中處理掉它們,只好留到下一次垃圾收集時再清掉。同樣也是由於在垃圾收集階段使用者執行緒還需要持續執行、需要預留足夠記憶體空間提供給使用者執行緒使用、因此 CMS 收集器不能像其他收集器那樣等待到老年代幾乎完全被填滿了再進行收集、必須預留一部分空間供併發收集時程式運作使用。JDK6 時、CMS 收集器啟動的閾值時 92% ,這樣子會面臨一種風險、要是 CMS 執行期間預留的記憶體無法滿足程式分配物件的需要、就會出現一次併發失敗、這時候虛擬機器將不得不啟動後備預案、暫停使用者現象、臨時啟用 Serial Old 收集器來進行老年年代的垃圾收集,但這樣子的話、停頓的時間就很長了。
  • 記憶體碎片化問題、因為 CMS 時基於標記清除演算法的。空間碎片過多時、將會給打物件分配帶來很大麻煩,往往會出現老年代結束時還有很多剩餘空間、但是無法找到足夠大的連續空間分配當前物件而不得不觸發一次 Full GC。所以預設提供了一個引數、用於在 CMS 不得不進行 Full GC 時開啟記憶體碎片的合併整理、但是記憶體整理也是需要 STW 的、所以停頓時間也會變長了。

G1

是一款面向伺服器的垃圾收集器,支援新生代和老年代的垃圾收集,主要針對配備多核處理器及大容量記憶體多機器。

G1 最主要的設計目標是 : 可預測可配置的 STW 停頓時間。

G1 有一些獨特的實現。首先、堆不再分成連續的年輕代和老年代空間(不再堅持固定大小以及固定數量的分代區域劃分)。而是劃分為多個大小相等的存放物件的小堆區。每個小堆區可能是Eden Survivor 區或者 Old 區、但是在同一時刻只能屬於某個代。

在邏輯上, 所有的Eden區和Survivor區合起來就是新生代,所有的Old區合起來就是老年代,且新生代和老年代各自的記憶體Region區域由G1自動控制,不斷變動

在 G1 收集器出現之前的所有其他收集器、包括 CMS 在內、垃圾收集的目標範圍要麼是整個新生代、要麼是整個老年代、要麼就是整個堆。G1 跳出這個樊籠、 它可以面向堆記憶體任何部分來組成回收集 Collection Set CSet、衡量標準不再是它屬於哪個分代、而是哪塊記憶體的垃圾數量最多、回收收益最大、這就是 G1 的 Mixed GC 模式。

當物件大小超過 Region 的一半、則認為是巨型物件,直接被分配到老年代的巨型物件區 Humongous Regions 。

每個 Region 中最多隻有一個巨型物件、巨型物件可以佔多個 Region。

G1 把堆記憶體劃分為一個個 Region 的意義在於

  • 每次 GC 不必都去整理整個堆、而是處理一部分的 Region、實現大容量記憶體的 GC
  • 通過計算每個 Region 的回收價值、包括回收所需要時間、可回收的空間、在有限時間內儘可能回收更多的記憶體,把垃圾回收造成的停頓時間控制在預期配置的時間範圍內。

解決跨 Region 引用

使用記憶集避免全堆作為 GC Roots 掃描、但在 G1 收集器上記憶集的應用要複雜很多、它的每個 Region 都維護有自己的記憶集、這些記憶集都會記錄下別的 Region 指向自己的指標,並標記這些指標分別在哪些卡頁的範圍之內。G1 的記憶集在儲存結構的本質上是一個雜湊表、key是 Region 的起始地址、Value 是一個集合、裡面儲存的元素是卡表的索引號。這種雙向的卡表結構(卡表是我指向誰、這種結構還記錄了誰指向我)。由於每個 Region 都需要維護一個記憶集、所以G1 收集器比其他傳統的垃圾收集器有著更高的記憶體佔用負擔。G1 耗費相當於 Java 堆容量的 10% - 20% 的額外記憶體來維持收集器的工作。

併發標記使用者執行緒與 GC 執行緒互相干擾問題

CMS 採用增量更新、G1 採用原始快照來實現。此外垃圾收集對使用者執行緒的影響還體現在回收過程中新建物件的記憶體分配上、程式要繼續執行肯定會持續有新物件被建立,G1 為每個 Region 建立了兩個名為 TAMS(Top At Mark Start) 的指標、把Region 中的一部分空間劃分出來用於併發回收過程中的新物件分配,併發回收時新分配的物件地址必須要在這兩個指標位置以上。G1 收集器預設在這個地址以上的物件時被隱式標記過的、預設它們是存活的、不納入回收的範圍。與CMS 中的 Concurrent Mode Failure 失敗會導致 Full GC 類似,如果記憶體回收的速度趕不上記憶體分配的速度、G1 收集器也要被凍結使用者執行緒、導致 Full GC 而產生長時間 STW

建立停頓預測模型

使用者通過 -XX:MaxGCPauseMills引數指定停頓時間、這個時間只是垃圾收集器發生前的期望。G1 收集器的停頓預測模式是以衰減均值為理論基礎實現的、在垃圾收集過程中、G1 收集器會記錄每個 Region 的回收耗時、每個Region 裡藏卡數量等各個可測量的步驟花費的成本。

G1 工作模式

針對新生代和老年代、G1 提供了兩種 GC 模式、Young GC 和 Mixed GC 、都會導致 STW

  • Young GC 當新生代空間不足時、G1 觸發 Young GC回收新生代空間、Young GC 主要對 Eden 區進行回收、它在 Eden 空間耗盡時觸發、基於分代回收思想和複製演算法、每次 Young GC 都會選定所有新生代的 Region,同時計算下次 Young GC 所需 Eden 區和 Survivor 區的空間、動態調整新生代佔 Region個數來控制 Young GC 開銷。
  • Mixed GC 當老年代空間達到閾值就會觸發 Mixed GC、選定所有新生代的 Region、根據全域性併發標記階段統計得出收集收益高的若干老年代 Region。在使用者指定的開銷目標範圍內、儘可能選擇收益高的老年代 Region 進行 GC ,通過選擇哪些老年代 Region和選擇多少 Region來控制 Region 來控制 Mixed GC 開銷。

暫停轉移:純年輕代模式 Evacuation Pause Fully Young

在應用程式剛啟動時、G1 還未執行過併發階段、也就沒有獲得額外的資訊,處於初始的 fully young 模式。

在年輕代空間用滿之後、應用執行緒被暫停、年輕代中存活的物件被複制到存活區、如果沒有存活區、則選擇任意一部分空閒的小堆區作為存活區。

複製過程稱為轉移 Evacuation,這和前面講過的年輕代收集器基本是一樣的工作原理。

全域性併發標記

主要是為了 Mixed GC 計算找出回收收益較高的 Region 區域。

當堆記憶體的總體使用比例達到一定數值時、會觸發併發標記、預設值是 45% 、可以通過引數 InitiatingHeapOccupancyPercent 來設定。

階段1 初始標記 此階段標記所有從 GC Root直接可達的物件。當達到出發條件時、G1 並不會立即發起併發標記週期、而是等待下一次新生代收集、利用新生代收集的 STW 時間段、完成初始標記、這種方式稱為借道

階段2 Root Region 掃描 在初始標記暫停之後、新生代收集也完成物件複製到 Survivor 的工作、應用執行緒也開始活躍起來、此時為了保證標記演算法的正確性,所有新複製到 Survivor 分割槽的物件、需要找出哪些物件存在對老年代物件的引用,把這些物件標記成根。這個過程稱為根分割槽掃描。同時掃描的 Survivor 分割槽也被稱為根分割槽;根分割槽的掃描必須要在下一次新生代垃圾收集啟動前完成、因為每次 GC 產生新的存活物件集合。

階段3 併發標記 標記執行緒與應用執行緒併發執行、標記各個堆中 Region的存活物件資訊、這個步驟可能會被新的 Young GC 打斷、所有標記任務必須在堆滿前就掃描完成、如果併發標記耗時很長、那麼有可能在併發標記過程中、又經歷幾次新生代收集

階段4 再次標記 STW 以完成標記過程、標記在併發階段發生變化的物件和未被標記的物件、同時完成存活資料計算

階段5 清理

  • 更新每個 Region 各自的 Remember Set。
  • 回收不包含存活物件的 Region
  • 統計計算回收收益高的老年代分割槽集合。

CMS 與 G1 比較

與 CMS 相比、G1 有很多優點、暫不論可以指定最大暫停時間、分Region的記憶體佈局、按收益動態確定收集 這些創新性設計帶來的紅利。單從最傳統的演算法理論看、G1 也更有發展潛力、與CMS 的標記清除演算法不同、G1 從整體上看是基於標記整理演算法實現、但從區域性(兩個 Region 之間) 上看又是基於標記複製演算法實現的、但是無論如何這兩種演算法都以為著 G1 運作期間不會產生記憶體碎片、垃圾收集完成之後能提供規整的可用記憶體。這個特性有利於程式長時間運作

比起 CMS G1 弱項也可以列舉不少、如在使用者程式執行過程中、G1 無論是為了垃圾收集產生的記憶體佔用還是程式執行時的額外執行負載都要比 CMS 高。

Minor GC 的過程

  • Eden 區沒有足夠的記憶體分配給新物件

物件進入老年代

  • 大物件直接在老年代分配
  • 分配擔保、Minor GC 時、to Space 存不下存活的物件
  • 長期存活的物件直接進入老年代
  • 動態年齡判斷、如果在 From Survivor 中的物件大小大於 From 和 To 的和一半、那麼大於的那一部分的年齡的物件將會被放入到老年代

空間分配擔保

在發生 Minor GC 之前、虛擬機器會檢查老年代中最大可用的連續空間是否大於所有新生代物件的總空間、如果大於、那麼 Minor GC 是安全的

如果不成立、則檢查老年代中最大可用連續空間是否大於歷次晉升到老年代的物件的平均大小、如果大於、那麼進行 Minor GC 、如果小於、則進行 Full GC

如果執行完 Minor GC 發現晉升的物件很多、老年代無法存放、還是要進行 Full GC

Full GC 的過程

  • System.gc 建議 JVM 進行 Full GC
  • Minor GC 之前、老年代中的連續空間小於歷次晉升的物件的平均大小
  • Minor GC 執行完發現晉升到老年代的物件大於老年代能存放的空間
  • Metaspace 每次擴容之後大於 MetaspaceSize 引數
  • CMS / G1 併發標記的時候失敗

GC 調優過程

專案相關

調優常用的引數

列印 JVM 初始引數

-XX:+PrintFlagsFinal
# 或者
-XX:+PrintFlagsInitial

檢視 Java 程序

jps

檢視程序中某個引數的值

jinfo -flag XXXX <pid>

檢視 Java 程序記憶體的容量和使用量

jstat -gc <pid>

檢視 JVM 內執行緒的情況

jstack <pid>

記憶體抖動

Java

Java 列舉

本質上是一個語法糖、繼承自 Enum 類。會自動生成 valuse 方法、還有 valueOf 方法

Enum 的 == 和 euqal 是一樣的。

Enum 不支援 Clone 方法

Enum 方法支援序列化反序列化,但是反序列化出來的物件還是 JVM 中原來的物件

Java synthetic

由編譯器生成的,在原始碼中沒有出現的,都會被標記為 synthetic。當然有一些例外的情況:預設的建構函式、類的初始化方法、以及列舉類中的 valuevalueOf 方法

Java 序列化/反序列化

序列化就是將物件的狀態資訊轉為可以儲存或者傳輸的形式的過程

  • Serializable & Externalizable
  • transient 不參加序列化/反序列化
  • ObjectOutputStream & ObjectInputStream
  • Externalizable 必須提供一個無參構造方法、寫入順序要與讀取順序一致
  • 序列化 ID 是根據這個類的資訊區生成的、
  • 列舉是直接呼叫它的 Enum.valueOf 方法

https://www.cnblogs.com/-coder-li/p/13100015.html

Java 集合

  • Collection
    • List
    • Set
    • Queue
  • Map

Iterable

Iterator

我們都知道在 ArrayList 中 forEach 中的時候 remove 會導致 ConcurrentModificationException

ArrayList

  • 動態陣列
  • 執行緒不安全
  • 元素允許為 null
  • 連續的記憶體空間
  • 增加和刪除都會導致 modCount 的值改變
  • 擴容預設為 一半

Vector

  • 執行緒安全
  • 擴容是上次的一倍
  • 存在 modCount
  • 每個操作都加上了 synchronized

CopyOnWriteArrayList

  • 寫時複製、加鎖
  • 耗記憶體
  • 實時性不高
  • 不存在 併發修改異常
  • 資料量最好不要太大
  • 使用 ReentrantLock 進行加鎖

Collections.synchronizedList

  • synchronized 程式碼塊
  • 物件鎖可以傳進去
  • 需要傳 List 物件進去

LinkedList

  • ArrayList 增刪效率低、改查效率高、而 LinkedList 剛剛相反
  • 連結串列實現
  • for 迴圈的時候、根據 index 是靠近前半段還是後半段來決定是順序還是逆序
  • 增刪的時候會改變 modCount

HashMap

陣列 + 連結串列+紅黑樹

table 的預設長度是16 loadFactor 的值是 0.75

下標的計算

  • 獲取 key 的 hashCode、然後 hashCode 與 hashCode >>> 16 進行亦或
  • 然後使用陣列的長度對其進行取模運算

JDK 1.7 HashMap 擴容的時候、會因為連結串列元素的倒置進而導致迴圈連結串列

容量為2,負載因子為1、可以使加入5之後進行擴容

再進一步

進而

在 JDK1.8 的時候、在擴容處理連結串列的時候、增加了頭尾兩個元素、將連結串列元素倒置問題解決了、迴圈連結串列的問題也就解決了。

但是無論如何併發的情況下、元素還是會丟失

HashTable

遺留類、很多功能和 HashMap 類似、但是它是執行緒安全的、任意時刻只有一個執行緒寫 HashTable 、併發性不如 ConcurrentHashMap

LinkedHashMap

繼承自 HashMap、在 HashMap 的基礎上、通過維護一條雙向連結串列、解決了 HashMap 不能隨時保持遍歷順序和插入順序一致的問題

我們可以通過重寫 removeEldestEntry 方法來實現一個 LRU 佇列

Set

依賴於 HashMap 實現的

Queue

PriorityQueue

預設最小頂堆

面向物件的三大特性

  • 繼承
  • 封裝
  • 多型

final 關鍵字

  • 修飾類、不能被繼承
  • 修飾方法、不能被重寫
  • 修飾屬性、要麼在宣告變數的時候賦值、要麼在建構函式中賦值、不能再次修改、可見性

訪問修飾符

修飾符 本類 同包 子類 其他
private